mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-07-28 21:17:44 +00:00
355 lines
12 KiB
Rust
355 lines
12 KiB
Rust
//! End-to-end local HFT-loop benchmarks.
|
|
//!
|
|
//! These benches intentionally avoid live network I/O. They measure the local loop a trading
|
|
//! process actually controls: receive/apply a book update, make a simple best-ask decision, build
|
|
//! and sign an order, serialize the exact submit body, and build L2 auth headers.
|
|
//!
|
|
//! Run the polyfill benches with:
|
|
//! `cargo bench --bench hft_loop`
|
|
//!
|
|
//! Run the optional rs-clob-client-v2 comparison with:
|
|
//! `cargo bench --bench hft_loop --features official-client-benchmark`
|
|
|
|
use alloy_signer_local::PrivateKeySigner;
|
|
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
|
|
use polyfill_rs::{
|
|
auth::{create_l2_headers_with_body_bytes, PreparedApiCredentials},
|
|
orders::{OrderBuilder, BYTES32_ZERO},
|
|
types::{ApiCredentials, OrderType, PostOrder, PostOrderOptions},
|
|
OrderBookManager, Side, WsBookUpdateProcessor,
|
|
};
|
|
use rust_decimal::Decimal;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
const ASSET_ID: &str = "12345678901234567890";
|
|
const MARKET: &str = "0xabc";
|
|
const CHAIN_ID: u64 = 137;
|
|
const PRIVATE_KEY: &str = "0x1234567890123456789012345678901234567890123456789012345678901234";
|
|
const START_TIMESTAMP: u64 = 1_000_000_000_000_000;
|
|
|
|
fn test_signer() -> PrivateKeySigner {
|
|
PRIVATE_KEY.parse().expect("valid benchmark private key")
|
|
}
|
|
|
|
fn api_credentials() -> ApiCredentials {
|
|
ApiCredentials {
|
|
api_key: "benchmark-api-key".to_string(),
|
|
secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(),
|
|
passphrase: "benchmark-passphrase".to_string(),
|
|
}
|
|
}
|
|
|
|
fn price_decimal_from_ticks(ticks: u32) -> Decimal {
|
|
Decimal::new(ticks as i64, 4)
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct TimestampRange {
|
|
start: usize,
|
|
end: usize,
|
|
}
|
|
|
|
impl TimestampRange {
|
|
fn find(bytes: &[u8]) -> Self {
|
|
let needle = b"\"timestamp\":";
|
|
let Some(pos) = bytes.windows(needle.len()).position(|w| w == needle) else {
|
|
panic!("timestamp field not found in WS template JSON");
|
|
};
|
|
|
|
let start = pos + needle.len();
|
|
let mut end = start;
|
|
while end < bytes.len() && bytes[end].is_ascii_digit() {
|
|
end += 1;
|
|
}
|
|
|
|
Self { start, end }
|
|
}
|
|
|
|
fn write_fixed_width(&self, bytes: &mut [u8], mut value: u64) {
|
|
for idx in (self.start..self.end).rev() {
|
|
bytes[idx] = b'0' + (value % 10) as u8;
|
|
value /= 10;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn price_string_from_ticks(ticks: u32) -> String {
|
|
let whole = ticks / 10_000;
|
|
let frac = ticks % 10_000;
|
|
format!("{whole}.{frac:04}")
|
|
}
|
|
|
|
fn build_ws_book_template(levels_per_side: usize) -> Vec<u8> {
|
|
let mut json = String::new();
|
|
|
|
json.push_str("{\"event_type\":\"book\",\"asset_id\":\"");
|
|
json.push_str(ASSET_ID);
|
|
json.push_str("\",\"market\":\"");
|
|
json.push_str(MARKET);
|
|
json.push_str("\",\"timestamp\":");
|
|
json.push_str(&START_TIMESTAMP.to_string());
|
|
json.push_str(",\"bids\":[");
|
|
|
|
for i in 0..levels_per_side {
|
|
if i != 0 {
|
|
json.push(',');
|
|
}
|
|
let bid_price = price_string_from_ticks(7_500 - i as u32);
|
|
json.push_str("{\"price\":\"");
|
|
json.push_str(&bid_price);
|
|
json.push_str("\",\"size\":\"100.0000\"}");
|
|
}
|
|
|
|
json.push_str("],\"asks\":[");
|
|
for i in 0..levels_per_side {
|
|
if i != 0 {
|
|
json.push(',');
|
|
}
|
|
let ask_price = price_string_from_ticks(7_501 + i as u32);
|
|
json.push_str("{\"price\":\"");
|
|
json.push_str(&ask_price);
|
|
json.push_str("\",\"size\":\"100.0000\"}");
|
|
}
|
|
json.push_str("]}");
|
|
|
|
json.into_bytes()
|
|
}
|
|
|
|
fn build_polyfill_order_context() -> (
|
|
PrivateKeySigner,
|
|
polyfill_rs::orders::PreparedOrderPath,
|
|
ApiCredentials,
|
|
PreparedApiCredentials,
|
|
PostOrderOptions,
|
|
) {
|
|
let signer = test_signer();
|
|
let builder = OrderBuilder::new(signer.clone(), None, None);
|
|
let prepared_order = builder
|
|
.prepare_order_path(
|
|
CHAIN_ID,
|
|
ASSET_ID.to_string(),
|
|
Decimal::new(1, 4),
|
|
false,
|
|
Some(BYTES32_ZERO),
|
|
Some(BYTES32_ZERO),
|
|
)
|
|
.unwrap();
|
|
let api_creds = api_credentials();
|
|
let prepared_api_creds = PreparedApiCredentials::try_new(api_creds.clone()).unwrap();
|
|
let post_options = PostOrderOptions {
|
|
order_type: OrderType::GTD,
|
|
post_only: false,
|
|
defer_exec: false,
|
|
};
|
|
|
|
(
|
|
signer,
|
|
prepared_order,
|
|
api_creds,
|
|
prepared_api_creds,
|
|
post_options,
|
|
)
|
|
}
|
|
|
|
fn benchmark_polyfill_hft_loop(c: &mut Criterion) {
|
|
let (signer, prepared_order, api_creds, prepared_api_creds, post_options) =
|
|
build_polyfill_order_context();
|
|
let books = OrderBookManager::new(64);
|
|
books.get_or_create_book(ASSET_ID).unwrap();
|
|
books
|
|
.with_book_mut(ASSET_ID, |book| {
|
|
book.set_tick_size_ticks(1);
|
|
Ok(())
|
|
})
|
|
.unwrap();
|
|
|
|
let template = build_ws_book_template(16);
|
|
let timestamp_range = TimestampRange::find(&template);
|
|
let timestamp = AtomicU64::new(START_TIMESTAMP + 1);
|
|
let mut processor = WsBookUpdateProcessor::new(template.len());
|
|
|
|
c.bench_function("polyfill_hft_ws_apply_decide_sign_serialize_auth", |b| {
|
|
b.iter_batched_ref(
|
|
|| {
|
|
let mut message = template.clone();
|
|
let next_timestamp = timestamp.fetch_add(1, Ordering::Relaxed);
|
|
timestamp_range.write_fixed_width(&mut message, next_timestamp);
|
|
message
|
|
},
|
|
|message| {
|
|
let stats = processor.process_bytes(black_box(message), &books).unwrap();
|
|
debug_assert_eq!(stats.book_messages, 1);
|
|
|
|
let selected_price = books
|
|
.with_book_mut(ASSET_ID, |book| {
|
|
let best_ask = book.best_ask_fast().expect("seeded ask side");
|
|
Ok(price_decimal_from_ticks(best_ask.price))
|
|
})
|
|
.unwrap();
|
|
|
|
let signed_order = prepared_order
|
|
.create_limit_order(
|
|
black_box(Side::BUY),
|
|
black_box(selected_price),
|
|
black_box(Decimal::new(10_025, 2)),
|
|
black_box(Some(1_900_000_000)),
|
|
)
|
|
.unwrap();
|
|
let body = PostOrder::new(
|
|
black_box(signed_order),
|
|
black_box(api_creds.api_key.clone()),
|
|
black_box(post_options),
|
|
);
|
|
let body_bytes = serde_json::to_vec(black_box(&body)).unwrap();
|
|
let headers = create_l2_headers_with_body_bytes(
|
|
&signer,
|
|
&prepared_api_creds,
|
|
"POST",
|
|
"/order",
|
|
Some(&body_bytes),
|
|
)
|
|
.unwrap();
|
|
|
|
black_box((stats, body_bytes, headers))
|
|
},
|
|
BatchSize::SmallInput,
|
|
)
|
|
});
|
|
}
|
|
|
|
fn benchmark_polyfill_order_decision_to_payload(c: &mut Criterion) {
|
|
let (signer, prepared_order, api_creds, prepared_api_creds, post_options) =
|
|
build_polyfill_order_context();
|
|
let selected_price = Decimal::new(7_501, 4);
|
|
|
|
c.bench_function("polyfill_decide_sign_serialize_prepared", |b| {
|
|
b.iter(|| {
|
|
let signed_order = prepared_order
|
|
.create_limit_order(
|
|
black_box(Side::BUY),
|
|
black_box(selected_price),
|
|
black_box(Decimal::new(10_025, 2)),
|
|
black_box(Some(1_900_000_000)),
|
|
)
|
|
.unwrap();
|
|
let body = PostOrder::new(
|
|
black_box(signed_order),
|
|
black_box(api_creds.api_key.clone()),
|
|
black_box(post_options),
|
|
);
|
|
let body_bytes = serde_json::to_vec(black_box(&body)).unwrap();
|
|
black_box(body_bytes)
|
|
})
|
|
});
|
|
|
|
c.bench_function("polyfill_decide_sign_serialize_auth_prepared", |b| {
|
|
b.iter(|| {
|
|
let signed_order = prepared_order
|
|
.create_limit_order(
|
|
black_box(Side::BUY),
|
|
black_box(selected_price),
|
|
black_box(Decimal::new(10_025, 2)),
|
|
black_box(Some(1_900_000_000)),
|
|
)
|
|
.unwrap();
|
|
let body = PostOrder::new(
|
|
black_box(signed_order),
|
|
black_box(api_creds.api_key.clone()),
|
|
black_box(post_options),
|
|
);
|
|
let body_bytes = serde_json::to_vec(black_box(&body)).unwrap();
|
|
let headers = create_l2_headers_with_body_bytes(
|
|
&signer,
|
|
&prepared_api_creds,
|
|
"POST",
|
|
"/order",
|
|
Some(&body_bytes),
|
|
)
|
|
.unwrap();
|
|
black_box((body_bytes, headers))
|
|
})
|
|
});
|
|
}
|
|
|
|
#[cfg(feature = "official-client-benchmark")]
|
|
fn benchmark_official_order_decision_to_payload(c: &mut Criterion) {
|
|
use polymarket_client_sdk_v2::{
|
|
auth::{Credentials, LocalSigner, Signer, Uuid},
|
|
clob::{
|
|
types::{OrderType as OfficialOrderType, Side as OfficialSide, TickSize},
|
|
Client as OfficialClient, Config as OfficialConfig,
|
|
},
|
|
types::{dec, Decimal as OfficialDecimal, U256},
|
|
POLYGON,
|
|
};
|
|
use std::str::FromStr as _;
|
|
|
|
let mut server = mockito::Server::new();
|
|
let _version_mock = server
|
|
.mock("GET", "/version")
|
|
.with_status(200)
|
|
.with_header("content-type", "application/json")
|
|
.with_body(r#"{"version":2}"#)
|
|
.create();
|
|
|
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
|
let signer = LocalSigner::from_str(PRIVATE_KEY)
|
|
.unwrap()
|
|
.with_chain_id(Some(POLYGON));
|
|
let token_id = U256::from_str(ASSET_ID).unwrap();
|
|
let client = runtime.block_on(async {
|
|
let client = OfficialClient::new(&server.url(), OfficialConfig::default()).unwrap();
|
|
client.set_tick_size(token_id, TickSize::TenThousandth);
|
|
client.set_neg_risk(token_id, false);
|
|
client.version().await.unwrap();
|
|
client
|
|
.authentication_builder(&signer)
|
|
.credentials(Credentials::new(
|
|
Uuid::nil(),
|
|
"dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(),
|
|
"benchmark-passphrase".to_string(),
|
|
))
|
|
.authenticate()
|
|
.await
|
|
.unwrap()
|
|
});
|
|
|
|
c.bench_function("rs_clob_client_v2_decide_sign_serialize_warmed", |b| {
|
|
b.iter(|| {
|
|
let body_bytes = runtime.block_on(async {
|
|
let order = client
|
|
.limit_order()
|
|
.token_id(black_box(token_id))
|
|
.side(black_box(OfficialSide::Buy))
|
|
.price(black_box(dec!(0.7501)))
|
|
.size(black_box(OfficialDecimal::new(10_025, 2)))
|
|
.expiration(chrono::DateTime::from_timestamp(1_900_000_000, 0).unwrap())
|
|
.order_type(OfficialOrderType::GTD)
|
|
.build()
|
|
.await
|
|
.unwrap();
|
|
let signed = client.sign(&signer, order).await.unwrap();
|
|
serde_json::to_vec(black_box(&signed)).unwrap()
|
|
});
|
|
black_box(body_bytes)
|
|
})
|
|
});
|
|
}
|
|
|
|
#[cfg(not(feature = "official-client-benchmark"))]
|
|
criterion_group!(
|
|
benches,
|
|
benchmark_polyfill_hft_loop,
|
|
benchmark_polyfill_order_decision_to_payload
|
|
);
|
|
|
|
#[cfg(feature = "official-client-benchmark")]
|
|
criterion_group!(
|
|
benches,
|
|
benchmark_polyfill_hft_loop,
|
|
benchmark_polyfill_order_decision_to_payload,
|
|
benchmark_official_order_decision_to_payload
|
|
);
|
|
|
|
criterion_main!(benches);
|