Skip to content

Commit 299b4b6

Browse files
author
yunjinqi
committed
fix: sync bybit hedge position sides
1 parent f417938 commit 299b4b6

2 files changed

Lines changed: 99 additions & 3 deletions

File tree

backtrader/brokers/btapibroker.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@
108108
"Side",
109109
"position_side",
110110
"positionSide",
111+
"positionIdx",
112+
"position_idx",
111113
"posSide",
112114
"PositionSide",
113115
"position_direction",
@@ -1052,9 +1054,7 @@ def _sync_one_position(
10521054

10531055
size = self._extract_update_value(item, *_POSITION_SIZE_KEYS)
10541056
size = float(size or 0.0)
1055-
direction = self._normalise_position_direction(
1056-
self._extract_update_value(item, *_POSITION_DIRECTION_KEYS)
1057-
)
1057+
direction = self._extract_position_direction(item)
10581058

10591059
price = self._extract_update_value(item, *_POSITION_PRICE_KEYS)
10601060
price = float(price or 0.0)
@@ -1143,6 +1143,30 @@ def _normalise_position_direction(value):
11431143
return "short"
11441144
return text
11451145

1146+
@classmethod
1147+
def _extract_position_direction(cls, item):
1148+
if not isinstance(item, dict):
1149+
return ""
1150+
details = item.get("details")
1151+
if not isinstance(details, dict):
1152+
details = {}
1153+
for key in _POSITION_DIRECTION_KEYS:
1154+
value = item.get(key)
1155+
if value in (None, ""):
1156+
value = details.get(key)
1157+
if value in (None, ""):
1158+
continue
1159+
if key in {"positionIdx", "position_idx"}:
1160+
text = cls._normalise_code_text(value)
1161+
if text == "1":
1162+
return "long"
1163+
if text == "2":
1164+
return "short"
1165+
if text == "0":
1166+
return ""
1167+
return cls._normalise_position_direction(value)
1168+
return ""
1169+
11461170
def _sync_remote_open_orders(self, force=False, raise_errors=False):
11471171
"""Refresh the cached provider-side open-order snapshot."""
11481172
if self.store is None or not self._live_started or not self.store.is_connected:

tests/unit/brokers/test_btapibroker.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,78 @@ def test_sync_positions_accepts_raw_okx_position_aliases():
974974
broker.stop()
975975

976976

977+
def test_sync_positions_accepts_raw_bybit_position_idx_in_net_mode():
978+
"""Bybit hedge snapshots may expose direction only through positionIdx."""
979+
symbol = "BTCUSDT"
980+
client = FakeBtApiClient(
981+
balance={"cash": 1250.0, "value": 1450.0},
982+
positions=[
983+
{
984+
"symbol": symbol,
985+
"positionIdx": "2",
986+
"size": "0.5",
987+
"avgPrice": "60125.5",
988+
}
989+
],
990+
history={symbol: [make_bar(0, 60000.0, 60200.0, 59900.0, 60100.0)]},
991+
)
992+
store = make_store(api=client, provider="bybit")
993+
data = store.getdata(dataname=symbol)
994+
broker = store.getbroker(account_refresh_interval=60.0, positions_refresh_interval=60.0)
995+
996+
data._start()
997+
assert data.load() is True
998+
broker.start()
999+
try:
1000+
position = broker.getposition(data)
1001+
1002+
assert position.size == pytest.approx(-0.5)
1003+
assert position.price == pytest.approx(60125.5)
1004+
assert broker.positions[symbol].size == pytest.approx(-0.5)
1005+
finally:
1006+
broker.stop()
1007+
1008+
1009+
def test_sync_positions_accepts_raw_bybit_position_idx_in_dual_side_mode():
1010+
"""Bybit positionIdx=2 must hydrate the short leg in dual-side mode."""
1011+
symbol = "BTCUSDT"
1012+
client = FakeBtApiClient(
1013+
balance={"cash": 1250.0, "value": 1450.0},
1014+
positions=[
1015+
{
1016+
"symbol": symbol,
1017+
"positionIdx": "2",
1018+
"size": "0.5",
1019+
"avgPrice": "60125.5",
1020+
}
1021+
],
1022+
history={symbol: [make_bar(0, 60000.0, 60200.0, 59900.0, 60100.0)]},
1023+
)
1024+
store = make_store(api=client, provider="bybit", supports_dual_side=True)
1025+
data = store.getdata(dataname=symbol)
1026+
broker = store.getbroker(
1027+
account_refresh_interval=60.0,
1028+
positions_refresh_interval=60.0,
1029+
position_mode="dual_side",
1030+
)
1031+
1032+
data._start()
1033+
assert data.load() is True
1034+
broker.start()
1035+
try:
1036+
net_position = broker.getposition(data)
1037+
short_position = broker.getposition(data, side="short")
1038+
long_position = broker.getposition(data, side="long")
1039+
1040+
assert net_position.size == pytest.approx(-0.5)
1041+
assert net_position.price == pytest.approx(60125.5)
1042+
assert short_position.size == pytest.approx(0.5)
1043+
assert short_position.price == pytest.approx(60125.5)
1044+
assert long_position.size == pytest.approx(0.0)
1045+
finally:
1046+
broker.stop()
1047+
1048+
9771049
def test_sync_positions_accepts_float_string_ctp_position_direction_codes():
9781050
"""CTP numeric-string position direction codes must not flip shorts long."""
9791051
symbol = "IF2506"

0 commit comments

Comments
 (0)