Skip to content

Commit f0e21ee

Browse files
author
yunjinqi
committed
fix: normalize raw exchange fee signs
1 parent 6c42056 commit f0e21ee

2 files changed

Lines changed: 87 additions & 17 deletions

File tree

backtrader/stores/btapistore.py

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,12 @@ def _unwrap_contract_metadata_payload(raw: Any, symbol: Any) -> Dict[str, Any]:
816816
return _flatten_contract_metadata_payload(data)
817817

818818

819-
def _normalise_exchange_commission_rate(key: str, value: Any) -> Optional[float]:
819+
def _normalise_exchange_commission_rate(
820+
key: str,
821+
value: Any,
822+
*,
823+
okx_fee_sign: bool = False,
824+
) -> Optional[float]:
820825
number = _coerce_float(value, None)
821826
if number is None:
822827
return None
@@ -825,13 +830,26 @@ def _normalise_exchange_commission_rate(key: str, value: Any) -> Optional[float]
825830
return number / 10000.0
826831
if key_lower in {"makercommissionrate", "takercommissionrate"} and abs(number) > 1:
827832
return number / 10000.0
828-
if key_lower in {"makeru", "takeru"}:
833+
if key_lower in {"makeru", "takeru"} or (
834+
okx_fee_sign and key_lower in {"maker", "taker"}
835+
):
829836
return -number
830837
if abs(number) > 1:
831838
return number / 100.0
832839
return number
833840

834841

842+
def _first_metadata_item(metadata: Dict[str, Any], *keys: str) -> Tuple[str, Any]:
843+
for key in keys:
844+
if key not in metadata:
845+
continue
846+
value = metadata.get(key)
847+
if value in (None, ""):
848+
continue
849+
return key, value
850+
return "", None
851+
852+
835853
def _normalise_contract_metadata(raw: Any, symbol: Any, *, source: str = "") -> Dict[str, Any]:
836854
data = _unwrap_contract_metadata_payload(raw, symbol)
837855
if not data:
@@ -883,23 +901,46 @@ def _normalise_contract_metadata(raw: Any, symbol: Any, *, source: str = "") ->
883901
metadata["multiplier"] = 1.0
884902
metadata["contract_size"] = 1.0
885903

886-
maker_rate = _normalise_exchange_commission_rate(
904+
okx_fee_sign = "okx" in " ".join(
905+
str(metadata.get(key) or "")
906+
for key in ("source", "fee_source", "exchange", "exchange_id")
907+
).lower() or any(
908+
key in metadata
909+
for key in (
910+
"makerU",
911+
"takerU",
912+
"makerUSDC",
913+
"takerUSDC",
914+
"feeGroup",
915+
)
916+
)
917+
maker_key, maker_value = _first_metadata_item(
918+
metadata,
919+
"maker_commission_rate",
920+
"maker_fee_rate",
887921
"makerCommissionRate",
888-
metadata.get("maker_commission_rate")
889-
or metadata.get("maker_fee_rate")
890-
or metadata.get("makerCommissionRate")
891-
or metadata.get("makerCommission")
892-
or metadata.get("makerU")
893-
or metadata.get("maker"),
922+
"makerCommission",
923+
"makerU",
924+
"maker",
894925
)
895-
taker_rate = _normalise_exchange_commission_rate(
926+
taker_key, taker_value = _first_metadata_item(
927+
metadata,
928+
"taker_commission_rate",
929+
"taker_fee_rate",
896930
"takerCommissionRate",
897-
metadata.get("taker_commission_rate")
898-
or metadata.get("taker_fee_rate")
899-
or metadata.get("takerCommissionRate")
900-
or metadata.get("takerCommission")
901-
or metadata.get("takerU")
902-
or metadata.get("taker"),
931+
"takerCommission",
932+
"takerU",
933+
"taker",
934+
)
935+
maker_rate = _normalise_exchange_commission_rate(
936+
maker_key,
937+
maker_value,
938+
okx_fee_sign=okx_fee_sign,
939+
)
940+
taker_rate = _normalise_exchange_commission_rate(
941+
taker_key,
942+
taker_value,
943+
okx_fee_sign=okx_fee_sign,
903944
)
904945
if maker_rate is not None:
905946
metadata["maker_commission_rate"] = maker_rate

tests/unit/brokers/test_btapibroker.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import backtrader as bt
77

88
from backtrader.brokers.btapibroker import BtApiBroker
9-
from backtrader.stores.btapistore import BtApiStoreError
9+
from backtrader.stores.btapistore import BtApiStoreError, _normalise_contract_metadata
1010
from tests.fixtures.fake_btapi import DEFAULT_SYMBOL, FakeBtApiClient, make_bar, make_store
1111

1212

@@ -2325,6 +2325,35 @@ def get_fee(self, symbol):
23252325
assert comminfo.getcommission(0.5, 60000.0, role="maker") == pytest.approx(6.0)
23262326

23272327

2328+
def test_contract_metadata_normalizes_okx_raw_fee_signs_without_touching_plain_rates():
2329+
"""OKX raw fee signs are opposite to internal commission signs."""
2330+
okx_metadata = _normalise_contract_metadata(
2331+
{
2332+
"instType": "SWAP",
2333+
"maker": "-0.0002",
2334+
"taker": "-0.0005",
2335+
"makerU": "0.00018",
2336+
"takerU": "-0.00045",
2337+
},
2338+
"BTC-USDT-SWAP",
2339+
source="okx_get_fee",
2340+
)
2341+
2342+
assert okx_metadata["maker_commission_rate"] == pytest.approx(-0.00018)
2343+
assert okx_metadata["taker_commission_rate"] == pytest.approx(0.00045)
2344+
assert okx_metadata["commission_rate"] == pytest.approx(0.00045)
2345+
assert okx_metadata["open_commission_rate"] == pytest.approx(0.00045)
2346+
2347+
plain_metadata = _normalise_contract_metadata(
2348+
{"maker": "0.0002", "taker": "0.0006"},
2349+
"BTCUSDT",
2350+
source="get_fee",
2351+
)
2352+
2353+
assert plain_metadata["maker_commission_rate"] == pytest.approx(0.0002)
2354+
assert plain_metadata["taker_commission_rate"] == pytest.approx(0.0006)
2355+
2356+
23282357
def test_contract_metadata_auto_materializes_fixed_margin_amount():
23292358
"""MT5-style per-lot initial margin must not be treated as a margin rate."""
23302359
broker = BtApiBroker(

0 commit comments

Comments
 (0)