Repository navigation
Expand file tree
/
Copy pathconstants.rs
More file actions
552 lines (439 loc) · 30.5 KB
/
Copy pathconstants.rs
File metadata and controls
552 lines (439 loc) · 30.5 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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
//! Crate-wide constants — hardcoded values that are not user-configurable.
//!
//! Config defaults live in `config/default.toml` (user can override via TOML).
//! Constants here are internal implementation details or exchange-imposed limits
//! that should not change without a code review.
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
// ─── Time conversion ─────────────────────────────────────────────────────────
/// Nanoseconds per millisecond. Used for ts_ns → ms conversions throughout the codebase.
pub const NANOS_PER_MS: u64 = 1_000_000;
/// Seconds per day. Used for SL expiry, auth token lifetimes, etc.
pub const SECS_PER_DAY: u64 = 86400;
// ─── Financial / spread ──────────────────────────────────────────────────────
/// Basis points conversion factor: spread_bps = price_diff / price × BPS_FACTOR.
/// 1 bps = 0.01% = 0.0001.
pub const BPS_FACTOR: Decimal = dec!(10_000);
/// Number of legs (exchange sides) per arb position.
/// Used in capital calculations: margin = LEGS_PER_POSITION × notional / leverage.
pub const LEGS_PER_POSITION: Decimal = dec!(2);
/// f64 counterpart of `LEGS_PER_POSITION` for the backtest simulator.
pub const LEGS_PER_POSITION_F64: f64 = 2.0;
// ─── Safe market price (entry market orders) ─────────────────────────────────
/// Multiplier for aggressive BUY price: best_ask × 1.5.
/// Generous ceiling to guarantee fill on exchanges without price limits (e.g. Lighter).
/// Not used for Extended (which has a 15% max deviation — see EXTENDED_MAX_SLIPPAGE_PCT).
pub const SAFE_MARKET_PRICE_BUY_MULT: Decimal = dec!(1.5);
/// Multiplier for aggressive SELL price: best_bid × 0.5.
/// Floor price to guarantee fill on market sell.
pub const SAFE_MARKET_PRICE_SELL_MULT: Decimal = dec!(0.5);
// ─── Entry protection ────────────────────────────────────────────────────────
/// Maximum acceptable price deviation between signal price and fill price.
/// If fill deviates > 50% from signal, the entry is considered suspicious and logged.
pub const MAX_ENTRY_PRICE_DEVIATION: Decimal = dec!(0.50);
// ─── Stop-loss fallbacks ─────────────────────────────────────────────────────
/// Fallback liquidation distance as fraction of entry price.
/// Used when per-symbol liq distance is unavailable (e.g. new symbol, no WS data yet).
/// 0.1 = 10% from entry → simulates ~10x leverage liquidation distance.
pub const FALLBACK_LIQ_DISTANCE: Decimal = dec!(0.1);
/// Fallback SL trigger buffer as fraction of entry price.
/// SL trigger = liq_price ± entry × FALLBACK_SL_BUFFER.
/// 0.01 = 1% buffer before liquidation.
pub const FALLBACK_SL_BUFFER: Decimal = dec!(0.01);
/// SL expiry renewal margin: renew SL order when remaining lifetime drops below this (seconds).
/// With default sl_expiry_days=7 and margin=2 days, SL is renewed after 5 days.
/// Renewal reuses the existing SL move mechanism (same trigger/limit, fresh expiry).
pub const SL_RENEWAL_MARGIN_SECS: u64 = 2 * SECS_PER_DAY;
/// Emergency close timeout: if an SL move fails and position stays unprotected
/// for longer than this, force-close the position to prevent unhedged liquidation.
pub const SL_UNPROTECTED_EMERGENCY_CLOSE_SECS: u64 = 60;
/// On SIGTERM/SIGINT, how long to wait for in-flight money-path executions
/// (entry/exit leg pairs, stop-loss moves) to finish before the process exits,
/// so a deploy-time signal can't cut a two-leg execution in half and leave a
/// one-legged position. The deploy launcher's force-kill grace
/// (`tools/bot_start.sh`) must exceed this. The drain returns immediately when
/// nothing is in flight, so a normal deploy is not slowed.
pub const SHUTDOWN_DRAIN_TIMEOUT_SECS: u64 = 10;
/// Hard timeout on a single low-level HTTP request (connect + TLS + response).
/// A live exchange REST endpoint answers in well under this; exceeding it means
/// a stalled handshake or an unresponsive server, so we fail instead of wedging
/// the calling task forever (the raw hyper client below has no timeout of its own).
pub const HTTP_REQUEST_TIMEOUT_SECS: u64 = 15;
/// Hard timeout on establishing a WebSocket connection (TCP + TLS + upgrade
/// handshake). A stalled handshake would otherwise hang every reconnect attempt
/// forever, so the stream never recovers. This bounds the *connect* only —
/// post-handshake read inactivity is bounded separately by each WS's read
/// timeout (a TimeoutStream), because `read_frame()` is not cancel-safe.
pub const WS_CONNECT_TIMEOUT_SECS: u64 = 10;
// ─── Exchange-imposed limits ─────────────────────────────────────────────────
/// Extended: MINIMUM per-symbol price cap (conservative fallback for config validation).
/// Actual limits are per-symbol via `limitPriceCap` from API (e.g. BTC/SOL=5%, MEGA=15%).
/// This constant is used only for startup config clamping when per-symbol caps aren't loaded yet.
pub const EXTENDED_MAX_SLIPPAGE_PCT: f64 = 5.0;
/// zero1 server hard limit: max 10 market subscriptions per WS connection.
/// With 25 markets and per_market mode off, markets are batched into groups of 10.
pub const ZERO1_MAX_MARKETS_PER_WS: usize = 10;
/// Safety margin when using "max" slippage: use cap × (1 − margin) to avoid
/// rejection when exchange oracle lags behind orderbook after sudden moves.
/// 0.30 covers oracle lag up to ~4% (XAG commodity oracle lagged 1.7%+ on Apr 7 2026).
pub const SLIPPAGE_CAP_MARGIN: f64 = 0.30;
/// Fallback slippage fraction for exchanges without per-symbol price cap (e.g. Lighter).
/// 0.50 = 50%. Generous — Lighter accepts any price, fills at orderbook levels.
pub const NO_CAP_SLIPPAGE_FALLBACK: f64 = 0.50;
/// Extended: maximum order lifetime allowed by the API (days).
/// Per Extended SDK spec. Used for order signature expiry.
pub const EXTENDED_MAX_ORDER_LIFETIME_DAYS: u64 = 90;
// ─── REST health ────────────────────────────────────────────────────────────
/// Consecutive REST failures per exchange before halting trading.
/// Covers the case where WS is alive but REST is blocked (e.g. CloudFront WAF).
/// With balance_refresh_interval_secs=30, 5 failures = 2.5 minutes of REST downtime.
pub const REST_FAILURE_HALT_THRESHOLD: u32 = 5;
/// Maximum REST polling interval during outage (exponential backoff from base 30s).
/// Progression: 5 fails=30s, 6=60s, 7=120s, 8=240s, 9+=300s cap.
/// Keeps recovery detection ≤5 min while reducing noise from 108→~15 requests/hour.
pub const REST_BACKOFF_MAX_INTERVAL_SECS: u64 = 300;
// ─── Reconnect grace period ─────────────────────────────────────────────────
/// Seconds to wait after all exchanges reconnect before allowing new entries.
/// Prevents one-legged trades when one exchange is technically connected but
/// still unstable (e.g. after VPN switch, CloudFront WAF unblock).
/// Open positions (exit/close/SL) are NOT affected — only new entries are delayed.
pub const RECONNECT_GRACE_PERIOD_SECS: u64 = 5;
// ─── WebSocket keepalive ────────────────────────────────────────────────────
/// Client-side WS ping interval (seconds). Used by Lighter heartbeat tasks.
/// NOTE: Extended does NOT use a client-side heartbeat — its server pings the
/// client every 15s and `fastwebsockets` auto-pongs at the WS-protocol layer.
/// Sending application-level `{"type":"ping"}` to Extended silently breaks the
/// orderbook subscription on reconnect (root cause of 2026-04-11 ZORA stall).
pub const WS_PING_INTERVAL_SECS: u64 = 5;
/// Ping interval for per-market WS connections (Lighter).
/// Uses WS-level Ping (OpCode 0x9) — server responds with Pong (verified).
/// MUST be < 60s: server kills inactive subscriptions (thin alts: APEX, MEGA,
/// MNT) at ~61s. Documented "1 frame/2min" keepalive is wrong for inactive
/// channels. 30s gives 2× margin before kill, first ping at +30s (skip-first
/// tick logic in write task).
/// Rate limit math: 78 connections × ping/30s = 156 msg/min (within 200 limit).
pub const PER_MARKET_WS_PING_INTERVAL_SECS: u64 = 30;
/// How many consecutive missed Pongs before declaring connection dead.
/// Dead = no Pong received for PING_INTERVAL × this count.
/// 3 missed × 30s = 90s silence → reconnect. Pong is server's reply to our
/// Ping, independent of market data — works for thin alts (APEX: 48min gaps).
pub const PER_MARKET_WS_MISSED_PONGS_LIMIT: u64 = 3;
/// Maximum allowed staleness of WS data (orderbook channel) before the
/// freshness gate considers data stale in the price-read path
/// (`fresh_price_from_cache` for manual close). Kept at 30s for conservative
/// price validation.
///
/// For the two-tier WS freshness architecture (reconnect + halt), use
/// `WS_STALE_RECONNECT_MS` and `WS_STALE_HALT_MS` instead.
pub const STALE_DATA_THRESHOLD_MS: u64 = 30_000;
/// WS-task freshness watchdog threshold: when `data_age_ms` exceeds this,
/// the WS read task is aborted and the supervisor reconnects. First line of
/// defense — local to each WS session, aggressive (30s).
///
/// Rationale: zero1 incident 2026-04-13 showed that a server sending only
/// WebSocket Ping frames (no market data) defeats the TCP-level `TimeoutStream`
/// check — `read_frame()` keeps returning successfully on pings, but no
/// `mark_data()` happens. An application-level watchdog that reads the same
/// `last_data_ms` atomic as the halt gate catches this immediately.
pub const WS_STALE_RECONNECT_MS: u64 = 30_000;
/// WS public-trades freshness watchdog threshold. Trades are inherently
/// sparser than orderbook updates (~0.2–5 trades/s per symbol vs continuous
/// OB deltas), so the threshold is more generous than `WS_STALE_RECONNECT_MS`.
/// Applied uniformly across all exchanges.
pub const WS_TRADES_STALE_RECONNECT_MS: u64 = 60_000;
/// Executor freshness halt threshold: when `data_age_ms` exceeds this,
/// trading is halted with `HaltReason::DataStale`. Second line of defense —
/// global in `check_data_freshness`, conservative (45s = 30s reconnect
/// threshold + 15s recovery window).
///
/// Tiered above `WS_STALE_RECONNECT_MS` so that in the normal case a transient
/// silence triggers the WS-task reconnect only and halt never fires. Halt is
/// the safety net for when reconnect itself didn't restore data flow in time.
pub const WS_STALE_HALT_MS: u64 = 45_000;
/// Per-market WS stale data threshold (ms). More generous than all-markets
/// (30s) because thin alt symbols may not update for extended periods.
pub const PER_MARKET_WS_STALE_MS: u64 = 120_000;
// ─── Orderbook depth ───────────────────────────────────────────────────────
/// Maximum number of distinct symbols across all exchanges (union).
/// SymbolId is a dense 0-based index into fixed arrays in SpreadDetector.
/// Current union: ~216 with Bybit (Lighter + Extended + Zero1 + Hyperliquid + Bybit).
/// Bump if adding exchanges with many unique symbols.
pub const MAX_SYMBOLS: usize = 1024;
/// Orderbook depth: how many levels per side to use everywhere
/// (WS subscription, L2OrderBook, tick writer, tick reader, VWAP).
/// Parquet schema always has MAX_ORDERBOOK_ARRAY_SIZE slots — unused = 0.0.
pub const ORDERBOOK_DEPTH: usize = 5;
/// Fixed array size for orderbook level storage (BestLevels, Parquet schema).
/// Must be >= ORDERBOOK_DEPTH. Extra slots are zero-filled.
pub const MAX_ORDERBOOK_ARRAY_SIZE: usize = 10;
/// Number of f64 fields per tick row in Parquet: bid + ask levels, each (price, qty).
pub const TICK_PARQUET_OB_FIELDS: usize = MAX_ORDERBOOK_ARRAY_SIZE * 4;
/// Total columns in tick Parquet: ts_ms + exchange_ts_ms + orderbook fields.
pub const TICK_PARQUET_COLS: usize = 2 + TICK_PARQUET_OB_FIELDS;
/// How often to check disk space and clean old tick segments (seconds).
pub const TICK_DISK_CHECK_INTERVAL_SECS: u64 = 3600;
/// How often to rotate tick writers (close + new file) to free metadata memory (seconds).
pub const TICK_ROTATION_INTERVAL_SECS: u64 = 60;
/// Capacity of the compaction worker channel. cap=1 makes any backpressure
/// loud (each skipped cycle warns) and serializes compaction strictly,
/// preventing the FD-leak race that previously killed the bot.
pub const COMPACTION_CHANNEL_CAPACITY: usize = 1;
/// Target full-cycle duration for rolling compaction (rotations).
/// The compaction worker processes `total_dirs / COMPACTION_TARGET_CYCLE`
/// dirs per rotation, cycling through all symbol dirs in ~60 minutes
/// regardless of exchange/symbol count. Always Arrow-merges (daily +
/// accumulated segments → single row group), so no separate consolidation
/// step is needed. Segments accumulate for up to one cycle (~60 min).
pub const COMPACTION_TARGET_CYCLE_ROTATIONS: usize = 60;
/// Every N rotations the funding writer runs Arrow consolidation (collapse
/// row groups into one). Funding data is tiny (~10 KB/dir, 7 columns,
/// hourly rotation), so rewriting all dirs every rotation is fine — this
/// constant only controls how often row groups are collapsed.
pub const COMPACTION_CONSOLIDATE_EVERY_N_ROTATIONS: u64 = 60;
/// Compaction cycles longer than this trigger a `warn!` log line for investigation.
pub const COMPACTION_SLOW_WARN_MS: u64 = 30_000;
/// Number of consecutive `try_send` failures (worker busy) before logging an error.
pub const COMPACTION_BACKPRESSURE_ERROR_THRESHOLD: u64 = 5;
/// Minimum age (mtime, seconds) of an orphan `.parquet.tmp` file before
/// `cleanup_orphan_tmp_files` will remove it. Protects against deleting
/// in-flight tmp files of another bot process during restart overlap
/// (`tools/bot_start.sh` starts a new bot BEFORE killing the old one).
/// Matches `tools/compact_ticks.py --safety-window-secs` default for consistency.
pub const ORPHAN_TMP_MIN_AGE_SECS: u64 = 300;
/// Periodic logging of `/proc/self/fd` count for early FD-leak detection.
pub const FD_COUNT_LOG_INTERVAL_SECS: u64 = 300;
/// Open FD count above this threshold triggers a `warn!` log line.
pub const FD_COUNT_WARN_THRESHOLD: usize = 1000;
/// Minimum NOFILE soft rlimit the bot expects. Below this, log error at startup.
/// `tools/bot_start.sh` sets `ulimit -n 65536`; below 16k indicates the bot was
/// invoked outside the launch script and may hit "Too many open files" under load.
pub const MIN_NOFILE_RLIMIT: u64 = 16384;
/// How often to poll funding rates via REST (seconds).
/// Funding changes every 1h (Lighter) or 8h (Extended), so 60s is plenty.
pub const FUNDING_POLL_INTERVAL_SECS: u64 = 60;
/// Total columns in funding Parquet: ts_ms + exchange_ts_ms + 5 data fields.
pub const FUNDING_PARQUET_COLS: usize = 7;
// ─── Database ──────────────────────────────────────────────────────────────
/// Default SQLite database path.
pub const DEFAULT_DB_PATH: &str = "db/arb_bot.db";
/// Root directory for portable market data (ticks, funding rates).
pub const DATA_DIR: &str = "data";
/// Tick data directory (parquet files per exchange/symbol).
pub const TICK_DIR: &str = "data/ticks";
/// Funding rate data directory (parquet files per exchange/symbol).
pub const FUNDING_DIR: &str = "data/funding";
/// Public trade data directory (parquet files per exchange/symbol).
pub const PUBLIC_TRADE_DIR: &str = "data/trades";
/// Hyperliquid public info endpoint (funding rates, mark prices, OI).
/// Extended exchange wraps Hyperliquid but its REST URL doesn't expose /info.
pub const HYPERLIQUID_INFO_URL: &str = "https://api.hyperliquid.xyz/info";
// ─── Exchange endpoints ───────────────────────────────────────────────────────
//
// These used to live in `config/connections.toml`. They were moved here
// because URLs are tied to the integration code that speaks to each venue,
// not to deploy-time configuration — there is no "staging Lighter" with a
// different URL, changing a URL means shipping a rebuilt binary. Keeping
// them as constants in one place (vs. three TOML files on three servers in
// sync) eliminates a class of silent drift and makes grep-to-endpoint trivial.
pub const LIGHTER_REST_URL: &str = "https://mainnet.zklighter.elliot.ai";
pub const LIGHTER_WS_URL: &str = "wss://mainnet.zklighter.elliot.ai/stream";
/// Max subscriptions per single WS connection (server-enforced).
/// Source: https://apidocs.lighter.xyz/docs/rate-limits — code 23001 on overflow.
pub const LIGHTER_WS_MAX_SUBS_PER_CONN: usize = 100;
pub const EXTENDED_REST_URL: &str = "https://api.starknet.extended.exchange";
pub const EXTENDED_WS_ORDERBOOK_URL: &str =
"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/orderbooks";
pub const EXTENDED_WS_ACCOUNT_URL: &str =
"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/account";
/// Public trades stream. Per-market connections append `/{MARKET}` to this
/// base (e.g. `.../publicTrades/BTC-USD`). Path verified via the
/// `extended_public_trades_probe` 2026-04-14; other candidates
/// (`v1/trades`, `v1/publicTrade`, `v1/trade`) all fail to connect.
pub const EXTENDED_WS_PUBLIC_TRADES_URL: &str =
"wss://api.starknet.extended.exchange/stream.extended.exchange/v1/publicTrades";
pub const ZERO1_REST_URL: &str = "https://zo-mainnet.n1.xyz";
/// zero1 uses a single multi-stream WS endpoint — stream paths like
/// `deltas@SYM`, `trades@SYM`, `account@ID` are appended at runtime by
/// `zero1/ws.rs`.
pub const ZERO1_WS_URL: &str = "wss://zo-mainnet.n1.xyz/ws";
/// Cadence for re-syncing `EngineClock` against `GET /timestamp`. The engine
/// rejects actions whose `current_timestamp` falls outside ±60 s of its own
/// logical clock. We re-sync every 30 s — well within the window even if a
/// single fetch fails — so multi-day uptime cannot accumulate enough drift to
/// trigger `TimestampOutOfThreshold`.
pub const ZERO1_CLOCK_SYNC_INTERVAL_SECS: u64 = 30;
/// Hyperliquid REST — single endpoint for all queries (`POST /info`) and
/// trading (`POST /exchange`). JSON wire format, EIP-712 signed actions.
pub const HYPERLIQUID_REST_URL: &str = "https://api.hyperliquid.xyz";
/// Hyperliquid WS — single endpoint, subscribe via JSON messages.
/// Channels: `bbo`, `l2Book`, `trades`, `userFills`, `userEvents`,
/// `orderUpdates`, `userFundings`, `clearinghouseState`.
pub const HYPERLIQUID_WS_URL: &str = "wss://api.hyperliquid.xyz/ws";
/// Bybit V5 REST base. Linear USDT perpetuals are hosted under `/v5/...`
/// (e.g. `/v5/market/instruments-info`, `/v5/order/create`).
pub const BYBIT_REST_URL: &str = "https://api.bybit.com";
/// Bybit V5 public OB stream — subscribes to `orderbook.1.{SYMBOL}USDT`.
pub const BYBIT_WS_PUBLIC_URL: &str = "wss://stream.bybit.com/v5/public/linear";
/// Bybit V5 private account stream — execution / position / order updates.
pub const BYBIT_WS_PRIVATE_URL: &str = "wss://stream.bybit.com/v5/private";
/// Bybit V5 trade WS — write-only channel for `order.create` / `order.cancel`
/// frames (lower latency than REST place_order).
pub const BYBIT_WS_TRADE_URL: &str = "wss://stream.bybit.com/v5/trade";
/// Delay after each hour boundary before polling Extended funding (seconds).
/// Settlement happens at HH:00:00.150 — poll 10s later to ensure data is available.
pub const EXTENDED_FUNDING_POLL_DELAY_AFTER_HOUR_SECS: u64 = 10;
/// Last known prices snapshot (written by tick_writer_task on every rotation).
pub const LAST_PRICES_PATH: &str = "data/last_prices.json";
/// Backtest results directory (saved JSON).
pub const BACKTEST_DIR: &str = "db/backtest";
/// Optimizer results directory (CSV).
pub const OPTIMIZER_DIR: &str = "db/optimizer";
// ─── Config paths ─────────────────────────────────────────────────────────
/// Main configuration file.
pub const CONFIG_DEFAULT_PATH: &str = "config/default.toml";
/// Local overrides (auto-created, not checked into git).
pub const CONFIG_LOCAL_PATH: &str = "config/local.toml";
// (exchange connection URLs live in config/connections.toml)
/// Optimizer configuration.
pub const CONFIG_OPTIMIZER_PATH: &str = "config/optimizer.toml";
// ─── Logs ─────────────────────────────────────────────────────────────────
/// Bot log directory.
pub const LOG_DIR: &str = "logs/bot";
// ─── Simulation latency defaults ──────────────────────────────────────────
/// Tick-to-signal delay: WS reception latency from exchange tick to bot detection (ms).
pub const SIM_SIGNAL_DELAY_MS: f64 = 2.0;
/// Default order→confirm delay (ms) for exchanges without an explicit
/// `sim_fill_delay_ms` in `[exchanges.<name>]`. Conservative middle-of-the-
/// road value — measure and override per-exchange in `default.toml` when
/// calibrated data exists. Resolution order in `build_sim_params`:
/// `cfg.exchanges[name].sim_fill_delay_ms.unwrap_or(SIM_FILL_DELAY_MS_DEFAULT)`.
///
/// Semantic: wall-clock from the bot sending an order to the bot receiving
/// confirmation that the order was executed. Same metric as the live UI's
/// `Order→Confirm` column. Read it from a recent live trading session and
/// take the median per venue. Per-venue meaning:
/// * Lighter / Extended: REST submit + asynchronous WS push back. Includes
/// network submit-leg + matching + network fill-notification-leg.
/// * ZERO1: synchronous HTTP — full RTT in one round-trip, dominated by
/// geography (very small when colocated, much larger from afar).
///
/// Per-exchange production values live in `config/default.toml`
/// (`[exchanges.lighter] sim_fill_delay_ms`, `[exchanges.extended] sim_fill_delay_ms`).
pub const SIM_FILL_DELAY_MS_DEFAULT: f64 = 200.0;
/// Simulation initial deposit for equity curve (USD). Not real capital.
pub const SIM_INITIAL_DEPOSIT_USD: f64 = 10000.0;
/// Gap close threshold (ms). Force-close open positions when no ticks arrive
/// for longer than this. Only for truly long outages where SL could have triggered
/// during the gap and we have no data to verify. Default 1 hour.
pub const SIM_GAP_CLOSE_THRESHOLD_MS: f64 = 3_600_000.0;
// ─── Market data channel monitoring ─────────────────────────────────────────
/// Capacity of the market data mpsc channel (BestLevels updates from WS → executor).
/// Sized for high-throughput burst tolerance during normal scheduler jitter without
/// dropping ticks. Real backpressure (channel full) is logged from `send_market_update`;
/// steady-state queue depth is sampled by the channel depth monitor task.
pub const MARKET_CHANNEL_CAPACITY: usize = 65_536;
/// Sampling interval for the market data channel depth monitor (seconds).
/// Background task reads `Sender::capacity()` (one atomic load) and warns when
/// depth crosses `MARKET_CHANNEL_DEPTH_WARN_PCT`, surfacing slow consumer
/// degradation that never reaches full saturation.
pub const MARKET_CHANNEL_MONITOR_INTERVAL_SECS: u64 = 1;
/// Queue-depth threshold (fraction of capacity) above which the depth monitor
/// raises an edge-triggered warning. 0.25 = warn when consumer is more than 25%
/// behind. Recovery edge fires at half this threshold (hysteresis).
pub const MARKET_CHANNEL_DEPTH_WARN_PCT: f64 = 0.25;
/// Minimum interval between "channel full" warnings from `send_market_update` (ms).
/// Prevents log flood when a sustained stall fires the warn path on every tick.
pub const MARKET_CHANNEL_FULL_WARN_THROTTLE_MS: u64 = 100;
/// Disk-space check interval for the in-bot tick writer (seconds).
/// Tighter than the recorder's `TICK_DISK_CHECK_INTERVAL_SECS` (1h) because
/// the bot writes 24/7 and may run alongside parallel backtests that consume
/// disk faster — checking every 5 minutes catches fill-up before parquet
/// writes start failing. Recorder's constant is left untouched for backward
/// compatibility with the standalone binary.
pub const BOT_TICK_DISK_CHECK_INTERVAL_SECS: u64 = 300;
/// Capacity of the tick recording mpsc channel (Arc<BestLevels> → tick_writer_task).
/// Smaller than MARKET_CHANNEL_CAPACITY because the writer task does only
/// `Vec::push` per tick (sub-microsecond) and rotation happens in spawn_blocking
/// off the writer task. Arc payload = 8 bytes per slot, so RAM cost is trivial.
pub const TICK_CHANNEL_CAPACITY: usize = 16_384;
/// Minimum interval between "tick channel full" warnings (ms). Same purpose as
/// MARKET_CHANNEL_FULL_WARN_THROTTLE_MS but throttled separately so the two
/// channels can't shadow each other's warnings.
pub const TICK_CHANNEL_FULL_WARN_THROTTLE_MS: u64 = 100;
// ─── Public trades recording ───────────────────────────────────────────────
/// Capacity of the public-trades mpsc channel (Arc<PublicTrade> → trade_writer_task).
/// Sized from probe 2026-04-14: observed peak ~2.4 trades/s/market on ETH-USD;
/// 80 markets × 10× headroom ≈ 2000/s → 10s buffer ≈ 20k. Round to TICK_CHANNEL
/// for symmetry and easy audit.
pub const PUBLIC_TRADE_CHANNEL_CAPACITY: usize = 16_384;
/// Minimum interval between "public-trades channel full" warnings (ms).
pub const PUBLIC_TRADE_CHANNEL_FULL_WARN_THROTTLE_MS: u64 = 100;
/// Number of columns in the public-trades parquet row:
/// ts_ms, exchange_ts_ms, price, size, side (i8), is_liquidation (i8), trade_id (i64)
pub const PUBLIC_TRADE_PARQUET_COLS: usize = 7;
/// How often to rotate the public-trades writer (close + new file) to free
/// metadata memory and enable compaction. Same interval as ticks for a single
/// rotation schedule.
pub const PUBLIC_TRADE_ROTATION_INTERVAL_SECS: u64 = 60;
/// How many public trades to retain per (exchange, symbol) in the detector
/// ring. Sized for the 300s divergence window: even BTC at peak ~2.4 trades/s
/// fills ~720 entries in 300s, but ring eviction by age keeps it bounded.
/// 200 covers the vast majority of symbols; only BTC/ETH at sustained peak
/// would hit the cap (acceptable — oldest entries are least relevant).
pub const RECENT_TRADES_RING_CAPACITY: usize = 200;
/// Max number of recent trades copied into each `LegSnapshot.recent_trades`
/// when a signal is built. Bounds `signals.legs` JSON size (~2 KB/leg).
pub const RECENT_TRADES_SNAPSHOT_COUNT: usize = 20;
/// Max age (nanoseconds) of a trade included in the DB `LegSnapshot.recent_trades`
/// JSON payload. The detector ring itself evicts at `2 × this` (see
/// `on_public_trade`) — wider than the snapshot cutoff so a quiet symbol
/// never hands the snapshot builder an empty tail on a fresh signal.
///
/// 300s (5 min) — sized from Extended public trade frequency analysis:
/// illiquid symbols (EDEN ~9-20 min, LINEA ~9-28 min, SPX ~4-7 min) need
/// a wide window to have any trades at all. At 60s, 6/19 BAD trades had
/// 0 recent trades and were uncatchable by the divergence filter.
/// At 300s, 3 of those 6 become catchable (NEAR, EDEN, EIGEN).
/// Active symbols (BTC/ETH) are bounded by RING_CAPACITY, not age.
pub const RECENT_TRADES_SNAPSHOT_MAX_AGE_NS: u64 = 300_000_000_000;
/// Max age (ns) of a trade considered by the divergence filter
/// (`max_trade_divergence_bps`). **Filter-specific** — distinct from
/// `RECENT_TRADES_SNAPSHOT_MAX_AGE_NS` which bounds the DB JSON snapshot
/// (different purpose, different value).
///
/// Sized from friend sessions 7+8 analysis (2026-04-14…17, 38 entries,
/// 24 BAD / 14 GOOD, no filter applied): p95 age of relevant trades at
/// BAD entries is ~55s, but precision drops fast past 10s. 5s keeps a
/// tight ME-staleness window that captures the "BBO hasn't caught up to
/// last print" pattern without including stale drift.
pub const TRADE_DIV_FILTER_MAX_AGE_NS: u64 = 5_000_000_000;
/// Max number of newest trades the divergence filter inspects per leg.
/// **Filter-specific** — distinct from `RECENT_TRADES_SNAPSHOT_COUNT`
/// (which bounds DB JSON size at 20). Three newest trades is enough to
/// detect ME-vs-BBO disagreement without amplifying noise from a single
/// outlier print.
pub const TRADE_DIV_FILTER_CAP: usize = 3;
/// Public funding rate poll: how many seconds after each hour boundary to poll.
/// Funding settles at HH:00:00 on both Lighter and Hyperliquid; we wait this
/// long to ensure the new rate is published. Mirrors `EXTENDED_FUNDING_POLL_DELAY_AFTER_HOUR_SECS`.
pub const PUBLIC_FUNDING_POLL_DELAY_AFTER_HOUR_SECS: u64 = 10;
/// Max time the startup public-funding poll waits for every exchange health
/// channel (WS orderbook + WS account on every built exchange) to come up
/// before firing its first REST sweep. The first poll otherwise races the
/// per-market WS bootstrap and dogpiles the same backend with REST stats
/// while we're still opening WS handshakes — observed as transient HTTP 500
/// from zero1 `/market/{id}/stats` on 2026-04-13. Polled every
/// `FUNDING_POLL_STARTUP_HEALTH_POLL_MS`.
pub const FUNDING_POLL_STARTUP_HEALTH_WAIT_MAX_SECS: u64 = 30;
pub const FUNDING_POLL_STARTUP_HEALTH_POLL_MS: u64 = 250;
/// How long a symbol stays blocked after a persistent MarketFrozen error.
/// Zero1 freezes are typically 1–10 seconds; 60 s gives headroom before re-entry.
pub const MARKET_FROZEN_COOLDOWN_SECS: u64 = 60;
/// How long `LighterClient::close_position` waits for the fill notification
/// from the Lighter WS write-loop before returning an error to the caller.
/// Covers the worst-case busy window on startup (leverage setup batches
/// sendtx calls on ~80 markets) plus a margin. In the happy path the fill
/// comes back within 100–500 ms. Holding the receiver alive for this long
/// is what stops the WS write-loop's stale-drop guard from silently
/// discarding the order — see the 2026-04-13 LIT incident.
pub const LIGHTER_CLOSE_POSITION_FILL_TIMEOUT_SECS: u64 = 30;