Skip to content

Commit 1852f08

Browse files
authored
Fix binance spot futures sandbox (#2687)
1 parent 3ad3b50 commit 1852f08

File tree

7 files changed

+236
-14
lines changed

7 files changed

+236
-14
lines changed

RELEASES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Released on TBD (UTC).
1818
- Fixed `generate_order_modify_rejected` typo in Binance execution client (#2682), thanks for reporting @etiennepar
1919
- Fixed order status report generation for Polymarket where `venue_order_id` was unbounded
2020
- Fixed Arrow schema registration for `BinanceBar`
21+
- Fixed the Binance data and execution models to be able to run strategies that trade simultaneously on spot and future markets, including the possibility to use sandbox execution.
2122

2223
### Documentation Updates
2324
None

examples/live/binance/binance_spot_and_futures_market_maker.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from nautilus_trader.model.identifiers import ClientId
3434
from nautilus_trader.model.identifiers import InstrumentId
3535
from nautilus_trader.model.identifiers import TraderId
36+
from nautilus_trader.model.venues import Venue
3637

3738

3839
# *** THIS IS A TEST STRATEGY WITH NO ALPHA ADVANTAGE WHATSOEVER. ***
@@ -75,6 +76,7 @@
7576
# streaming=StreamingConfig(catalog_path="catalog"),
7677
data_clients={
7778
"BINANCE_SPOT": BinanceDataClientConfig(
79+
venue=Venue("BINANCE_SPOT"),
7880
api_key=None, # 'BINANCE_API_KEY' env var
7981
api_secret=None, # 'BINANCE_API_SECRET' env var
8082
account_type=BinanceAccountType.SPOT,
@@ -85,6 +87,7 @@
8587
instrument_provider=InstrumentProviderConfig(load_all=True),
8688
),
8789
"BINANCE_FUTURES": BinanceDataClientConfig(
90+
venue=Venue("BINANCE_FUTURES"),
8891
api_key=None, # 'BINANCE_API_KEY' env var
8992
api_secret=None, # 'BINANCE_API_SECRET' env var
9093
account_type=BinanceAccountType.USDT_FUTURE,
@@ -97,6 +100,7 @@
97100
},
98101
exec_clients={
99102
"BINANCE_SPOT": BinanceExecClientConfig(
103+
venue=Venue("BINANCE_SPOT"),
100104
api_key=None, # 'BINANCE_API_KEY' env var
101105
api_secret=None, # 'BINANCE_API_SECRET' env var
102106
account_type=BinanceAccountType.SPOT,
@@ -110,6 +114,7 @@
110114
retry_delay_max_ms=10_000,
111115
),
112116
"BINANCE_FUTURES": BinanceExecClientConfig(
117+
venue=Venue("BINANCE_FUTURES"),
113118
api_key=None, # 'BINANCE_API_KEY' env var
114119
api_secret=None, # 'BINANCE_API_SECRET' env var
115120
account_type=BinanceAccountType.USDT_FUTURE,
@@ -136,9 +141,9 @@
136141
# Configure your strategies
137142
spot_symbol = "ETHUSDT"
138143
strat_config_spot = VolatilityMarketMakerConfig(
139-
instrument_id=InstrumentId.from_str(f"{spot_symbol}.BINANCE"),
140-
external_order_claims=[InstrumentId.from_str(f"{spot_symbol}.BINANCE")],
141-
bar_type=BarType.from_str(f"{spot_symbol}.BINANCE-1-MINUTE-LAST-INTERNAL"),
144+
instrument_id=InstrumentId.from_str(f"{spot_symbol}.BINANCE_SPOT"),
145+
external_order_claims=[InstrumentId.from_str(f"{spot_symbol}.BINANCE_SPOT")],
146+
bar_type=BarType.from_str(f"{spot_symbol}.BINANCE_SPOT-1-MINUTE-LAST-INTERNAL"),
142147
atr_period=20,
143148
atr_multiple=6.0,
144149
trade_size=Decimal("0.010"),
@@ -147,9 +152,9 @@
147152

148153
futures_symbol = "ETHUSDT-PERP"
149154
strat_config_futures = VolatilityMarketMakerConfig(
150-
instrument_id=InstrumentId.from_str(f"{futures_symbol}.BINANCE"),
151-
external_order_claims=[InstrumentId.from_str(f"{futures_symbol}.BINANCE")],
152-
bar_type=BarType.from_str(f"{futures_symbol}.BINANCE-1-MINUTE-LAST-EXTERNAL"),
155+
instrument_id=InstrumentId.from_str(f"{futures_symbol}.BINANCE_FUTURES"),
156+
external_order_claims=[InstrumentId.from_str(f"{futures_symbol}.BINANCE_FUTURES")],
157+
bar_type=BarType.from_str(f"{futures_symbol}.BINANCE_FUTURES-1-MINUTE-LAST-EXTERNAL"),
153158
atr_period=20,
154159
atr_multiple=6.0,
155160
trade_size=Decimal("0.010"),
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#!/usr/bin/env python3
2+
# -------------------------------------------------------------------------------------------------
3+
# Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
4+
# https://nautechsystems.io
5+
#
6+
# Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
7+
# You may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
# -------------------------------------------------------------------------------------------------
16+
17+
import asyncio
18+
import json
19+
from decimal import Decimal
20+
21+
from nautilus_trader.adapters.binance.common.enums import BinanceAccountType
22+
from nautilus_trader.adapters.binance.config import BinanceDataClientConfig
23+
from nautilus_trader.adapters.binance.factories import BinanceLiveDataClientFactory
24+
from nautilus_trader.adapters.binance.futures.types import BinanceFuturesMarkPriceUpdate
25+
from nautilus_trader.adapters.sandbox.config import SandboxExecutionClientConfig
26+
from nautilus_trader.adapters.sandbox.factory import SandboxLiveExecClientFactory
27+
from nautilus_trader.common.enums import LogColor
28+
from nautilus_trader.config import CacheConfig
29+
from nautilus_trader.config import InstrumentProviderConfig
30+
from nautilus_trader.config import LiveExecEngineConfig
31+
from nautilus_trader.config import LoggingConfig
32+
from nautilus_trader.config import TradingNodeConfig
33+
from nautilus_trader.core.data import Data
34+
from nautilus_trader.live.node import TradingNode
35+
from nautilus_trader.model.data import Bar
36+
from nautilus_trader.model.data import DataType
37+
from nautilus_trader.model.data import QuoteTick
38+
from nautilus_trader.model.data import TradeTick
39+
from nautilus_trader.model.identifiers import ClientId
40+
from nautilus_trader.model.identifiers import InstrumentId
41+
from nautilus_trader.model.identifiers import TraderId
42+
from nautilus_trader.model.identifiers import Venue
43+
from nautilus_trader.model.instruments import Instrument
44+
from nautilus_trader.trading import Strategy
45+
from nautilus_trader.trading.config import StrategyConfig
46+
47+
48+
# *** THIS IS A TEST STRATEGY WITH NO ALPHA ADVANTAGE WHATSOEVER. ***
49+
# *** IT IS NOT INTENDED TO BE USED TO TRADE LIVE WITH REAL MONEY. ***
50+
51+
52+
class TestStrategyConfig(StrategyConfig, frozen=True):
53+
futures_client_id: ClientId
54+
futures_instrument_id: InstrumentId
55+
spot_instrument_id: InstrumentId
56+
57+
58+
class TestStrategy(Strategy):
59+
def __init__(self, config: TestStrategyConfig) -> None:
60+
super().__init__(config)
61+
62+
self.futures_instrument: Instrument | None = None # Initialized in on_start
63+
self.spot_instrument: Instrument | None = None # Initialized in on_start
64+
self.futures_client_id = config.futures_client_id
65+
66+
def on_start(self) -> None:
67+
self.futures_instrument = self.cache.instrument(self.config.futures_instrument_id)
68+
if self.futures_instrument is None:
69+
self.log.error(
70+
f"Could not find instrument for {self.config.futures_instrument_id}"
71+
f"\nPossible instruments: {self.cache.instrument_ids()}",
72+
)
73+
self.stop()
74+
return
75+
self.spot_instrument = self.cache.instrument(self.config.spot_instrument_id)
76+
if self.spot_instrument is None:
77+
self.log.error(
78+
f"Could not find futures instrument for {self.config.spot_instrument_id}"
79+
f"\nPossible instruments: {self.cache.instrument_ids()}",
80+
)
81+
self.stop()
82+
return
83+
84+
account = self.portfolio.account(venue=self.futures_instrument.venue)
85+
balances = {str(currency): str(balance) for currency, balance in account.balances().items()}
86+
self.log.info(f"Futures balances\n{json.dumps(balances, indent=4)}", LogColor.GREEN)
87+
account = self.portfolio.account(venue=self.spot_instrument.venue)
88+
balances = {str(currency): str(balance) for currency, balance in account.balances().items()}
89+
self.log.info(f"Spot balances\n{json.dumps(balances, indent=4)}", LogColor.GREEN)
90+
91+
# Subscribe to live data
92+
self.subscribe_quote_ticks(self.config.futures_instrument_id)
93+
self.subscribe_quote_ticks(self.config.spot_instrument_id)
94+
self.subscribe_data(
95+
data_type=DataType(
96+
BinanceFuturesMarkPriceUpdate,
97+
metadata={"instrument_id": self.futures_instrument.id},
98+
),
99+
client_id=self.futures_client_id,
100+
)
101+
102+
def on_data(self, data: Data) -> None:
103+
self.log.info(repr(data), LogColor.CYAN)
104+
105+
def on_quote_tick(self, tick: QuoteTick) -> None:
106+
self.log.info(repr(tick), LogColor.CYAN)
107+
108+
def on_trade_tick(self, tick: TradeTick) -> None:
109+
self.log.info(repr(tick), LogColor.CYAN)
110+
111+
def on_bar(self, bar: Bar) -> None:
112+
self.log.info(repr(bar), LogColor.CYAN)
113+
114+
def on_stop(self) -> None:
115+
# Unsubscribe from data
116+
self.unsubscribe_quote_ticks(self.config.futures_instrument_id)
117+
self.unsubscribe_quote_ticks(self.config.spot_instrument_id)
118+
119+
120+
async def main():
121+
"""
122+
Show how to run a strategy in a sandbox for the Binance venue.
123+
"""
124+
# Configure the trading node
125+
config_node = TradingNodeConfig(
126+
trader_id=TraderId("TESTER-001"),
127+
logging=LoggingConfig(
128+
log_level="INFO",
129+
log_colors=True,
130+
use_pyo3=True,
131+
),
132+
exec_engine=LiveExecEngineConfig(
133+
reconciliation=True,
134+
reconciliation_lookback_mins=1440,
135+
filter_position_reports=True,
136+
),
137+
cache=CacheConfig(
138+
timestamps_as_iso8601=True,
139+
flush_on_start=False,
140+
),
141+
data_clients={
142+
"BINANCE_FUTURES": BinanceDataClientConfig(
143+
venue=Venue("BINANCE_FUTURES"),
144+
api_key=None, # 'BINANCE_API_KEY' env var
145+
api_secret=None, # 'BINANCE_API_SECRET' env var
146+
account_type=BinanceAccountType.USDT_FUTURE,
147+
base_url_http=None, # Override with custom endpoint
148+
base_url_ws=None, # Override with custom endpoint
149+
us=False, # If client is for Binance US
150+
testnet=False, # If client uses the testnet
151+
instrument_provider=InstrumentProviderConfig(load_all=True),
152+
),
153+
"BINANCE_SPOT": BinanceDataClientConfig(
154+
venue=Venue("BINANCE_SPOT"),
155+
api_key=None, # 'BINANCE_API_KEY' env var
156+
api_secret=None, # 'BINANCE_API_SECRET' env var
157+
account_type=BinanceAccountType.SPOT,
158+
base_url_http=None, # Override with custom endpoint
159+
base_url_ws=None, # Override with custom endpoint
160+
us=False, # If client is for Binance US
161+
testnet=False, # If client uses the testnet
162+
instrument_provider=InstrumentProviderConfig(load_all=True),
163+
),
164+
},
165+
exec_clients={
166+
"BINANCE_FUTURES": SandboxExecutionClientConfig(
167+
venue="BINANCE_FUTURES",
168+
account_type="MARGIN",
169+
starting_balances=["10_000 USDC", "0.005 BTC"],
170+
default_leverage=Decimal("5"),
171+
),
172+
"BINANCE_SPOT": SandboxExecutionClientConfig(
173+
venue="BINANCE_SPOT",
174+
account_type="CASH",
175+
starting_balances=["1_000 USDC", "0.001 BTC"],
176+
),
177+
},
178+
timeout_connection=30.0,
179+
timeout_reconciliation=10.0,
180+
timeout_portfolio=10.0,
181+
timeout_disconnection=10.0,
182+
timeout_post_stop=5.0,
183+
)
184+
185+
# Instantiate the node with a configuration
186+
node = TradingNode(config=config_node)
187+
188+
# Configure your strategy
189+
strat_config = TestStrategyConfig(
190+
futures_client_id=ClientId("BINANCE_FUTURES"),
191+
futures_instrument_id=InstrumentId.from_str("BTCUSDT-PERP.BINANCE_FUTURES"),
192+
spot_instrument_id=InstrumentId.from_str("BTCUSDC.BINANCE_SPOT"),
193+
)
194+
# Instantiate your strategy
195+
strategy = TestStrategy(config=strat_config)
196+
197+
# Add your strategies and modules
198+
node.trader.add_strategy(strategy)
199+
200+
# Register your client factories with the node (can take user-defined factories)
201+
node.add_data_client_factory("BINANCE_FUTURES", BinanceLiveDataClientFactory)
202+
node.add_data_client_factory("BINANCE_SPOT", BinanceLiveDataClientFactory)
203+
node.add_exec_client_factory("BINANCE_FUTURES", SandboxLiveExecClientFactory)
204+
node.add_exec_client_factory("BINANCE_SPOT", SandboxLiveExecClientFactory)
205+
node.build()
206+
207+
try:
208+
await node.run_async()
209+
finally:
210+
await node.stop_async()
211+
await asyncio.sleep(1)
212+
node.dispose()
213+
214+
215+
# Stop and dispose of the node with SIGINT/CTRL+C
216+
if __name__ == "__main__":
217+
asyncio.run(main())

nautilus_trader/adapters/binance/data.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import msgspec
2121
import pandas as pd
2222

23-
from nautilus_trader.adapters.binance.common.constants import BINANCE_VENUE
2423
from nautilus_trader.adapters.binance.common.enums import BinanceAccountType
2524
from nautilus_trader.adapters.binance.common.enums import BinanceEnumParser
2625
from nautilus_trader.adapters.binance.common.enums import BinanceErrorCode
@@ -148,8 +147,8 @@ def __init__(
148147
) -> None:
149148
super().__init__(
150149
loop=loop,
151-
client_id=ClientId(name or BINANCE_VENUE.value),
152-
venue=BINANCE_VENUE,
150+
client_id=ClientId(name or config.venue.value),
151+
venue=config.venue,
153152
msgbus=msgbus,
154153
cache=cache,
155154
clock=clock,

nautilus_trader/adapters/binance/execution.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
from nautilus_trader.adapters.binance.common.constants import BINANCE_MAX_CALLBACK_RATE
2020
from nautilus_trader.adapters.binance.common.constants import BINANCE_MIN_CALLBACK_RATE
21-
from nautilus_trader.adapters.binance.common.constants import BINANCE_VENUE
2221
from nautilus_trader.adapters.binance.common.enums import BinanceAccountType
2322
from nautilus_trader.adapters.binance.common.enums import BinanceEnumParser
2423
from nautilus_trader.adapters.binance.common.enums import BinanceFuturesPositionSide
@@ -148,8 +147,8 @@ def __init__(
148147
) -> None:
149148
super().__init__(
150149
loop=loop,
151-
client_id=ClientId(name or BINANCE_VENUE.value),
152-
venue=BINANCE_VENUE,
150+
client_id=ClientId(name or config.venue.value),
151+
venue=config.venue,
153152
oms_type=OmsType.HEDGING if account_type.is_futures else OmsType.NETTING,
154153
instrument_provider=instrument_provider,
155154
account_type=AccountType.CASH if account_type.is_spot else AccountType.MARGIN,
@@ -180,7 +179,7 @@ def __init__(
180179

181180
self._is_dual_side_position: bool | None = None # Initialized on connection
182181
self._set_account_id(
183-
AccountId(f"{name or BINANCE_VENUE.value}-{self._binance_account_type.value}-master"),
182+
AccountId(f"{name or config.venue.value}-{self._binance_account_type.value}-master"),
184183
)
185184

186185
# Enum parser

nautilus_trader/adapters/binance/factories.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ def get_cached_binance_spot_instrument_provider(
170170
account_type=account_type,
171171
is_testnet=is_testnet,
172172
config=config,
173+
venue=venue,
173174
)
174175

175176

nautilus_trader/adapters/sandbox/execution.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def connect(self) -> None:
142142
Connect the client.
143143
"""
144144
self._log.info("Connecting...")
145-
self._msgbus.subscribe("data.*", handler=self.on_data)
145+
self._msgbus.subscribe(f"data.*.{self.venue}.*", handler=self.on_data)
146146

147147
# Load all instruments for venue
148148
for instrument in self.exchange.cache.instruments(venue=self.venue):

0 commit comments

Comments
 (0)