From 44e73710125dd51130c4863979247513fff268b6 Mon Sep 17 00:00:00 2001 From: Sonic Shih Date: Fri, 14 Aug 2026 12:23:51 +0800 Subject: [PATCH 1/2] fix(collector): leave Polymarket API rate headroom --- deployment/aliyun/README.md | 2 +- deployment/aliyun/polymarket-reference-collector.service | 4 ++-- deployment/aliyun/polymarket-rust-health-policy.jq | 2 +- deployment/aliyun/test-polymarket-raw-ops-control-plane.sh | 4 ++-- rust_hft/tools/collector/src/polymarket_raw.rs | 3 ++- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/deployment/aliyun/README.md b/deployment/aliyun/README.md index 0a54d0f38..1b5959854 100644 --- a/deployment/aliyun/README.md +++ b/deployment/aliyun/README.md @@ -358,7 +358,7 @@ advancing. `priority_trade_backlog` must be zero for the shadow gate to accept a health sample, while `deferred_trade_markets` makes bounded historical backfill explicit rather than silently claiming full-cycle trade coverage. Every Data API request, including a second pagination request for the same market, passes through a -shared start-time pacer with at least 100ms between request starts. Up to four requests +shared start-time pacer with at least 125ms between request starts. Up to four requests may remain in flight, and each processing chunk retains at most four market responses, so slow I/O overlaps without creating an unbounded request or memory fan-out. An absolute 180-second cycle deadline cancels stalled network work and fails closed. diff --git a/deployment/aliyun/polymarket-reference-collector.service b/deployment/aliyun/polymarket-reference-collector.service index 9de862e36..7d16f14aa 100644 --- a/deployment/aliyun/polymarket-reference-collector.service +++ b/deployment/aliyun/polymarket-reference-collector.service @@ -10,8 +10,8 @@ Type=simple User=hftcollector Group=hftcollector # --max-trade-polls-per-cycle 200: the 180-second cycle watchdog and the -# 100ms minimum Data API request spacing bound one cycle to 1800 request -# starts; 200 polls need at least 20 seconds of pacing, leaving ~160 seconds +# 125ms minimum Data API request spacing bounds one cycle to 1440 request +# starts; 200 polls need at least 25 seconds of pacing, leaving ~155 seconds # of headroom for metadata and settlement work inside the watchdog. Poll # concurrency stays at the default 4, so the 672MiB/768MiB memory calibration # is unchanged while historical trade backfill throughput rises from the diff --git a/deployment/aliyun/polymarket-rust-health-policy.jq b/deployment/aliyun/polymarket-rust-health-policy.jq index a96399db5..ae39b7072 100644 --- a/deployment/aliyun/polymarket-rust-health-policy.jq +++ b/deployment/aliyun/polymarket-rust-health-policy.jq @@ -26,7 +26,7 @@ and (.market_detail_deferred == (.market_detail_eligible - .market_detail_select and (.market_detail_priority_deferred == ([.market_detail_priority - .market_detail_budget, 0] | max)) and (.trade_poll_budget_after_market_details | type == "number" and floor == . and . >= 0) and (.trade_poll_budget_after_market_details == (.trade_poll_budget - .market_detail_selected)) -and (.trade_request_spacing_ms | type == "number" and floor == . and . >= 100) +and (.trade_request_spacing_ms | type == "number" and floor == . and . >= 125) and (.eligible_trade_markets | type == "number" and floor == . and . >= 0) and (.priority_trade_markets | type == "number" and floor == . and . >= 0) and (.selected_trade_markets | type == "number" and floor == . and . >= 0) diff --git a/deployment/aliyun/test-polymarket-raw-ops-control-plane.sh b/deployment/aliyun/test-polymarket-raw-ops-control-plane.sh index 193164abd..f97c09b8d 100755 --- a/deployment/aliyun/test-polymarket-raw-ops-control-plane.sh +++ b/deployment/aliyun/test-polymarket-raw-ops-control-plane.sh @@ -4966,7 +4966,7 @@ jq -n '{ cycle_started_at:"2026-07-15T00:00:00Z",cycle_duration_ms:1000, target_markets:120, missing_target_symbols:[],api_errors:[],malformed_trade_rows:0, - trade_poll_budget:200,trade_poll_concurrency:4,trade_request_spacing_ms:100, + trade_poll_budget:200,trade_poll_concurrency:4,trade_request_spacing_ms:125, priority_trade_markets_before_market_details:108, market_detail_budget:4,market_detail_eligible:3,market_detail_priority:2, market_detail_selected:3,market_detail_deferred:0,market_detail_priority_deferred:0, @@ -5035,7 +5035,7 @@ for mutation in \ '.market_detail_priority_deferred = 1' \ '.trade_poll_budget_after_market_details = 196' \ 'del(.trade_request_spacing_ms)' \ - '.trade_request_spacing_ms = 99' \ + '.trade_request_spacing_ms = 124' \ '.priority_trade_markets = 107' \ '.priority_trade_backlog = 1' \ '.selected_trade_markets = 196' \ diff --git a/rust_hft/tools/collector/src/polymarket_raw.rs b/rust_hft/tools/collector/src/polymarket_raw.rs index e7f1ed475..81da71327 100644 --- a/rust_hft/tools/collector/src/polymarket_raw.rs +++ b/rust_hft/tools/collector/src/polymarket_raw.rs @@ -52,7 +52,7 @@ pub const DEFAULT_TAPE_MAX_BYTES: u64 = 4 * 1024 * 1024 * 1024; // A nonzero cap below one record's order of magnitude would rotate every // write batch and flood the uploader with trivial tapes. const MIN_TAPE_MAX_BYTES: u64 = 1024 * 1024; -const MIN_TRADE_REQUEST_SPACING: Duration = Duration::from_millis(100); +const MIN_TRADE_REQUEST_SPACING: Duration = Duration::from_millis(125); const TARGET_MARKET_WINDOWS_SECS: [usize; 2] = [300, 900]; const SETTLEMENT_PRICE: Decimal = Decimal::from_parts(999, 0, 0, false, 3); const SETTLEMENT_LOSER_PRICE: Decimal = Decimal::from_parts(1, 0, 0, false, 3); @@ -4213,6 +4213,7 @@ mod tests { default.max_concurrent_trade_polls, DEFAULT_MAX_CONCURRENT_TRADE_POLLS ); + assert_eq!(default.per_market_delay, Duration::from_millis(125)); default.validate().unwrap(); let malformed_market_id = ReferenceConfig { From 526fb201f5bdd2c3f6d0968cc021b3258dd4e426 Mon Sep 17 00:00:00 2001 From: Sonic Shih Date: Fri, 14 Aug 2026 12:44:55 +0800 Subject: [PATCH 2/2] fix(collector): share Polymarket pacing default --- rust_hft/tools/collector/src/bin/polymarket-raw-ops.rs | 8 +++++++- rust_hft/tools/collector/src/polymarket_raw.rs | 8 ++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/rust_hft/tools/collector/src/bin/polymarket-raw-ops.rs b/rust_hft/tools/collector/src/bin/polymarket-raw-ops.rs index e6448b57d..8b874a805 100644 --- a/rust_hft/tools/collector/src/bin/polymarket-raw-ops.rs +++ b/rust_hft/tools/collector/src/bin/polymarket-raw-ops.rs @@ -10,6 +10,7 @@ use hft_collector::polymarket_parity::{ use hft_collector::polymarket_raw::{ finalize_reference_tape, run_reference, ReferenceConfig, DEFAULT_MAX_CONCURRENT_TRADE_POLLS, DEFAULT_MAX_MARKETS_PER_LANE, DEFAULT_MAX_TRADE_POLLS_PER_CYCLE, DEFAULT_TAPE_MAX_BYTES, + DEFAULT_TRADE_REQUEST_SPACING_MS, }; use hft_collector::polymarket_research_import::{ validate_research_segments, ArtifactTriplet, ResearchSegmentValidationConfig, @@ -73,7 +74,7 @@ enum Command { trade_finalization_lag_secs: i64, #[arg(long, default_value_t = 3)] trade_finalization_stable_polls: u64, - #[arg(long, default_value_t = 100)] + #[arg(long, default_value_t = DEFAULT_TRADE_REQUEST_SPACING_MS)] per_market_delay_ms: u64, #[arg(long, default_value_t = DEFAULT_TAPE_MAX_BYTES)] tape_max_bytes: u64, @@ -430,6 +431,7 @@ mod tests { max_markets, max_trade_polls_per_cycle, max_concurrent_trade_polls, + per_market_delay_ms, tape_max_bytes, .. } = command @@ -445,6 +447,10 @@ mod tests { max_concurrent_trade_polls, ReferenceConfig::default().max_concurrent_trade_polls ); + assert_eq!( + Duration::from_millis(per_market_delay_ms), + ReferenceConfig::default().per_market_delay + ); assert_eq!(tape_max_bytes, ReferenceConfig::default().tape_max_bytes); } diff --git a/rust_hft/tools/collector/src/polymarket_raw.rs b/rust_hft/tools/collector/src/polymarket_raw.rs index 81da71327..d84c42f39 100644 --- a/rust_hft/tools/collector/src/polymarket_raw.rs +++ b/rust_hft/tools/collector/src/polymarket_raw.rs @@ -45,6 +45,7 @@ const MAX_TRADE_ROWS_PER_SNAPSHOT: usize = 20_000; pub const DEFAULT_MAX_MARKETS_PER_LANE: usize = 10_000; pub const DEFAULT_MAX_TRADE_POLLS_PER_CYCLE: usize = 112; pub const DEFAULT_MAX_CONCURRENT_TRADE_POLLS: usize = 4; +pub const DEFAULT_TRADE_REQUEST_SPACING_MS: u64 = 125; // Default cap on one active tape's bytes before fail-closed rotation. At // tick-level recording a single UTC-hour tape reached 20-25 GiB (#655), and // upload processing needs ~1.6x the tape size of transient spool disk. @@ -52,7 +53,7 @@ pub const DEFAULT_TAPE_MAX_BYTES: u64 = 4 * 1024 * 1024 * 1024; // A nonzero cap below one record's order of magnitude would rotate every // write batch and flood the uploader with trivial tapes. const MIN_TAPE_MAX_BYTES: u64 = 1024 * 1024; -const MIN_TRADE_REQUEST_SPACING: Duration = Duration::from_millis(125); +const MIN_TRADE_REQUEST_SPACING: Duration = Duration::from_millis(DEFAULT_TRADE_REQUEST_SPACING_MS); const TARGET_MARKET_WINDOWS_SECS: [usize; 2] = [300, 900]; const SETTLEMENT_PRICE: Decimal = Decimal::from_parts(999, 0, 0, false, 3); const SETTLEMENT_LOSER_PRICE: Decimal = Decimal::from_parts(1, 0, 0, false, 3); @@ -4213,7 +4214,10 @@ mod tests { default.max_concurrent_trade_polls, DEFAULT_MAX_CONCURRENT_TRADE_POLLS ); - assert_eq!(default.per_market_delay, Duration::from_millis(125)); + assert_eq!( + default.per_market_delay, + Duration::from_millis(DEFAULT_TRADE_REQUEST_SPACING_MS) + ); default.validate().unwrap(); let malformed_market_id = ReferenceConfig {