Repository navigation
Expand file tree
/
Copy pathdefault.toml
More file actions
303 lines (272 loc) · 15.2 KB
/
Copy pathdefault.toml
File metadata and controls
303 lines (272 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
[strategy]
# Min spread to enter (bps, 1 bps = 0.01%). Round-trip fees fetched on startup:
# Standard Lighter+Extended ~5 bps, Premium Lighter+Extended ~10.6 bps. + target 5-10 bps.
# Optimized 2026-03-23: entry=35, sharpe=0.739, profit=$4.71/51 trades.
min_entry_spread_bps = 50.0
# Exit mode: "spread" (close when entry spread compressed by min_exit_spread_bps)
# or "pnl" (close when estimated PnL% reaches min_exit_pnl_pct). Mathematically equivalent.
exit_mode = "spread"
# Min captured PnL (bps) to close. gross_pnl_bps = entry_spread + close_spread (VWAP).
# include_fees_in_exit=true → compared to NET (after fees), false → GROSS.
min_exit_spread_bps = 30.0
# Min PnL as % of notional to close (when exit_mode = "pnl"). Optimized 2026-03-23: tp=0.300.
min_exit_pnl_pct = 0.200
# Consecutive exit checks above target before closing. Protects against single-tick OB spikes.
min_exit_checks = 1
# Min seconds between open signals for the same symbol.
signal_cooldown_secs = 1.0
# Deduct round-trip fees from VWAP spread before threshold comparison.
# entry: true = net, false = gross. exit: applies to both "spread" and "pnl" modes.
include_fees_in_entry = true
include_fees_in_exit = true
# Min USD on top OB level (ask1/bid1) per exchange. Skip entry if either side thinner.
# Protects against thin tops consumed during order flight (~300ms). 0 = disabled.
min_top_level_usd = 100
# Max absolute price move (bps) per exchange. Skip signals on flash spikes.
# 0 = disabled.
tick_max_spike_bps = 75
# Rolling window (ms) for spike detection. 0 = single-tick comparison.
# When > 0, computes max-min range over last N ms instead of prev vs cur tick.
tick_spike_window_ms = 200
# Max divergence (bps) of recent public trades from the BBO we'd execute against.
# Catches thin DEX books where BBO doesn't reflect executable price. 0 = disabled.
# Filter inspects last TRADE_DIV_FILTER_CAP trades within TRADE_DIV_FILTER_MAX_AGE_NS
# (see common/src/constants.rs). 80 bps tuned from friend sessions 7+8 analysis:
# blocks $8.4/$12.24 (69%) of BAD-entry slippage with 2/14 GOOD FP.
max_trade_divergence_bps = 80
# Max gap (s) between OUR LOCAL recv timestamps of the two legs' last frames.
# Catches "one venue WS went silent". Used by bot at runtime AND optimizer/backtest.
# Tight default (100ms): rejects any cross-leg desync on the recv clock. Use
# Analyze Ticks → Use button for per-symbol overrides on thin coins. 0 = disabled.
max_recv_staleness_secs = 0.1
# Max tick age (ms): now() - oldest(last_exchange_ts) across both legs. Covers
# Tick→Recv (network latency to the venue) + Recv→Sig (local staleness) in a
# single gate. Venues that forward exchange_ts = recv_ts (e.g. ZERO1) degrade
# automatically to Recv→Sig for that leg. Skips entry AND exit (TP only, not SL).
# 0 = disabled.
max_tick_age_ms = 100
[position]
# Leverage (same for both exchanges → liquidation symmetry).
leverage = 3
# Max notional USD per leg (without leverage). With 5x: $25 order = $5 margin.
max_order_size_usd = 10
# Max real margin (USD) deployed across all open positions. Both legs counted:
# each position uses 2 × notional / leverage margin. $100 → up to 10 positions of $25.
max_used_capital_usd = 100
# Max simultaneously open positions (across all symbols).
max_open_positions = 5
# Max open positions per symbol. 1 = no stacking (safest for stop-loss logic).
max_positions_per_symbol = 1
# Stop-loss buffer before liquidation (% of entry). SL trigger = liq + entry × buffer/100.
# Example: entry=$40, liq=$33, buffer=1.0 → trigger=$33.40.
stop_loss_buffer_pct = 1.0
# Auto-move SL when liq price drifts. Threshold = min liq drift (%) to trigger move.
sl_movement_enabled = true
sl_movement_threshold_pct = 0.5
# Max wait for maker close on Extended before falling back to taker (s).
# Only when exit_execution_mode = "sequential" and extended exit_order_type = "limit".
maker_close_timeout_secs = 45
[bot]
# Snapshot in-memory state (PnL, spreads, positions) to DB/dashboard. Cheap (RAM-only).
snapshot_interval_secs = 1
# Poll exchange balances via REST (s). 0 = only on trade events.
balance_refresh_interval_secs = 30
# Enable in-bot tick recording (tick_writer + funding rates poll).
log_ticks = true
# Min free disk space (GB) for tick_writer cleanup. 0 = disabled.
min_free_disk_gb = 5
# Enable public-trade parquet recording (data/trades/<exchange>/<symbol>/*.parquet).
# The detector's recent-trades ring and signals.legs.recent_trades DB snapshot
# are populated unconditionally regardless of this setting — this only toggles
# the on-disk parquet history.
log_public_trades = true
[risk]
# Kill switch: stop NEW entries if daily realized PnL falls below this. Open positions unaffected.
daily_loss_limit_usd = -50
# SL order expiry (days). Lighter rejects very long expiry. After expiry SL auto-cancels.
sl_expiry_days = 7
# ─── Exchange config ──────────────────────────────────────────────────────────
# Common fields (same semantics across all exchanges):
# enabled — toggle exchange on/off
# margin_mode — "isolated" or "cross"
# sim_fill_delay_ms — wall-clock from order submit to bot's
# confirmation of execution (ms). Take this
# value from the live UI's `Order→Confirm`
# column in trade-detail. Backtest skips
# forward by this many ms after each signal
# before reading the fill price, so it
# models the leg-risk window the bot
# actually experiences. Per-venue meaning:
# • ZERO1: synchronous HTTP receipt — full
# RTT (sign + POST + match + response).
# Geographically dominated; ~3ms in Tokyo,
# much more from EU/US.
# • Lighter / Extended: REST submit ack +
# asynchronous WS push back. Includes
# submit-leg + match + fill-propagation-
# leg. Typically 50-200ms trans-Pacific.
# Falls back to SIM_FILL_DELAY_MS_DEFAULT.
# entry_order_type — "market" (Lighter only, guaranteed fill) or "ioc" (Limit+IOC)
# entry_slippage_pct — "max" (per-symbol exchange limitPriceCap) or number in %.
# "max" guarantees fill on both legs. Fixed values clamped to cap.
# entry/exit_confirmation_timeout_ms — safety-net WS+REST fallback timeout. Default 1500ms.
# Override per-exchange only if latency is measurably different.
# exit_order_type — "ioc" (taker, immediate) or "gtt" (maker, rests in book at signal).
# Some Lighter markets reject GTT (error 21706) — use "ioc" for safety.
# exit_slippage_pct — same semantics as entry_slippage_pct
# emergency_slippage_pct — for must-fill closes (SL/forced)
# stop_loss_type — "ioc" (StopLoss, guaranteed fill) or "gtt" (StopLossLimit, may not fill).
# Extended only supports "gtt" (priceType MARKET rejected with error 1133).
# ws_read_timeout_secs — force OB WS reconnect on TCP silence. 0 = disabled.
# ws_ob_mode — "single" (one WS for all markets, lower conn count, good for tick history)
# or "per_market" (one WS per symbol, eliminates TCP HoL blocking, ~80 conns)
# orderbook_depth — "full" (exchange default deep book) or 1 (top-of-book only).
# Supported on Extended (§1) and Lighter (§2). See
# docs/STRATEGY_OPTIMIZE.md for research and measurements.
# ─── Per-exchange allowed_with (optional) ────────────────────────────────────
# Restrict which exchanges a given exchange arbs with. Default: empty = all.
# Example: zero1 arbs only with lighter, not with extended or hyperliquid:
# [exchanges.zero1]
# allowed_with = ["lighter"]
# A pair (A, B) is skipped if A.allowed_with is set and B ∉ it,
# OR B.allowed_with is set and A ∉ it. Hot-reloadable via web UI Apply.
[exchanges.lighter]
enabled = true
margin_mode = "isolated"
# Lighter ZK-rollup: median tick→fill ~215ms.
sim_fill_delay_ms = 215
# "market" ignores entry_slippage_pct; "ioc" uses it for limit price.
entry_order_type = "market"
entry_slippage_pct = "max"
# confirmation_timeout: default 1500ms (config.rs). Override only if exchange is faster/slower.
exit_order_type = "ioc"
stop_loss_type = "ioc"
# Probe 4 (docs/STRATEGY_OPTIMIZE.md §2) — single-WS и per-symbol для Lighter
# латентно эквивалентны (avg Δ +1.55ms на 59650 парах, single-WS быстрее в 62.5%).
# Single = ~118с экономии на старте (1500ms × 79 stagger) и нет всплеска
# consumer-falling-behind пока коннекты ещё стейджатся.
ws_ob_mode = "single"
# Subscribe to ticker/{market_id} (BBO, snapshot on every nonce) instead of
# order_book/{market_id} (full L2, 50ms batched). Measurements: ticker is
# 25ms p50 / 49ms p90 fresher on L1 updates and wins 91.1% of nonce races
# across 12625 paired samples (docs/STRATEGY_OPTIMIZE.md §2 and the probes
# in bot/tests/exchanges/lighter/ticker_freshness_race.rs). Rollback knob:
# set to "full" in config/local.toml to return to the delta stream.
orderbook_depth = 1
[exchanges.extended]
enabled = true
margin_mode = "cross"
# Extended faster than Lighter: median ~170ms.
sim_fill_delay_ms = 170
# Extended sends all orders as LIMIT to API; "ioc" = Limit+IOC (0.025% taker).
entry_order_type = "ioc"
entry_slippage_pct = "max"
# Extended WS sends CANCELLED/FILLED in ~500ms — faster than default 1500ms.
entry_confirmation_timeout_ms = 1000
exit_confirmation_timeout_ms = 1000
exit_order_type = "ioc"
exit_slippage_pct = "max"
emergency_slippage_pct = "max"
# Extended only supports TPSL+LIMIT — always behaves as GTT regardless of setting.
stop_loss_type = "gtt"
# OB stream sends ~270 msgs/sec; 30s silence = dead connection. OB WS only.
ws_read_timeout_secs = 30
# Probe 2 (docs/STRATEGY_OPTIMIZE.md §1) — single-WS и per-symbol для Extended
# латентно эквивалентны (avg Δ +3ms, |p99| 32ms — TCP jitter, не топология).
# Single = меньше коннектов, нет 78-параллельных handshake'ов на старте.
ws_ob_mode = "single"
# Subscribe to top-of-book only (?depth=1). Extended's full-depth delta stream
# has multi-second p99 staleness (probe measurements in docs/STRATEGY_OPTIMIZE.md);
# BBO snapshots come in at ~130ms p99. Set to "full" to go back to the deep
# delta stream if you need full book data for analysis/backtest recording.
orderbook_depth = 1
# ─── zero1 (01.xyz / N1 / Nord) ───────────────────────────────────────────────
# Live-verified end-to-end on mainnet. Depth/liquidity lower than Lighter/Extended.
# Required env vars in `.env`:
# ZERO1_KEYPAIR_B58=<phantom export> OR ZERO1_KEYPAIR_PATH=<id.json>
# ZERO1_ACCOUNT_ID=<engine account_id from /user/{pubkey}>
# Pre-flight: `cargo test --test zero1 -- poke --ignored --nocapture`
# ws_ob_mode "single" = batched up to 10 markets per WS (server limit).
[exchanges.zero1]
enabled = true
margin_mode = "cross"
sim_fill_delay_ms = 5
entry_order_type = "ioc"
entry_slippage_pct = "max"
exit_order_type = "ioc"
exit_slippage_pct = "max"
emergency_slippage_pct = "max"
stop_loss_type = "ioc"
ws_read_timeout_secs = 30
ws_ob_mode = "per_market"
# ─── Hyperliquid ─────────────────────────────────────────────────────────────
# Largest perp DEX by OI ($7-8B). EIP-712 signed actions (secp256k1).
# Required env vars in `.env`:
# HYPERLIQUID_API_PRIVATE_KEY=0x... (agent wallet private key)
# HYPERLIQUID_API_WALLET_ADDRESS=0x... (agent wallet address, cross-check)
# HYPERLIQUID_ACCOUNT_ADDRESS=0x... (main account address)
# Pre-flight: `cargo test --test hyperliquid -- credentials_probe --ignored --nocapture`
# BBO WS channel = primary tick feed (5.6x faster than l2Book).
# Shared-connection model: 3 WS total (BBO + account + trades).
# Min order notional: $10. Price decimals inferred from mark price at startup.
[exchanges.hyperliquid]
enabled = true
margin_mode = "isolated"
sim_fill_delay_ms = 200
entry_order_type = "ioc"
entry_slippage_pct = "max"
exit_order_type = "ioc"
exit_slippage_pct = "max"
emergency_slippage_pct = "max"
stop_loss_type = "ioc"
ws_read_timeout_secs = 30
ws_ob_mode = "per_market"
# Which API carries signed trading actions: "ws" (WS POST with REST fallback)
# or "rest" (REST-only). Used for A/B latency testing across regions.
trading_api = "ws"
# ─── Bybit (V5 USDT Linear Perpetuals) ───────────────────────────────────────
# CEX. Three WS connections: public OB (`orderbook.1.{SYMBOL}USDT`), private
# account (execution / position / order), trade WS (write-only `order.create`).
# Required env vars in `.env`:
# BYBIT_API_KEY=<unified-trading API key>
# BYBIT_API_SECRET=<API secret>
# Pre-flight: `cargo test --test bybit -- rest_probe --ignored --nocapture`
# Default disabled — flip to true after credentials are in `.env`.
[exchanges.bybit]
enabled = false
margin_mode = "isolated"
sim_fill_delay_ms = 150
entry_order_type = "ioc"
entry_slippage_pct = "max"
exit_order_type = "ioc"
exit_slippage_pct = "max"
emergency_slippage_pct = "max"
# Bybit linear supports stopLoss field on order.create (Phase 5 will route to
# native conditional orders). Until then the client falls back to reduce-only
# IOC, which behaves like a market close — adequate for SL semantics.
stop_loss_type = "ioc"
# Bybit kills WS at 30s silence; OB ping every 20s keeps it alive.
ws_read_timeout_secs = 30
ws_ob_mode = "single"
[startup]
# Existing positions on restart: "resume" (detect hedged, close only orphans — saves fees)
# or "clean" (cancel all, close all). Override in local.toml; bot_start.sh sets it automatically.
mode = "resume"
[monitoring]
# Web dashboard with SSE updates: http://127.0.0.1:3030
web_enabled = true
web_addr = "0.0.0.0:3030"
[telegram]
# Notifications. Empty = disabled. Bot via @BotFather, chat_id via @userinfobot.
bot_token = ""
chat_id = ""
[trading_pairs]
# Which exchanges to arb between. All C(N,2) pairs generated automatically.
# Empty = all from [exchanges.*]. 2 exchanges → 1 pair, 3 → 3 pairs, 4 → 6.
exchanges = []
# Exclude specific pairs (optional). Order doesn't matter.
# exclude = [["extended", "hyperliquid"]]
# Per-symbol overrides. All fields optional. Available: min_entry_spread_bps.
# [[pairs.overrides]]
# symbol = "ETH"
# min_entry_spread_bps = 25.0