Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions rust_hft/apps/live/src/deployment_envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,12 @@ fn validate_instrument_catalog(
}

fn require_supported_cex_execution(costs: &EvaluationCostsV1) -> Result<(), String> {
// Paper uses canonical opposite-side depth for crossing orders.
// ponytail: non-zero funding needs a point-in-time runtime funding feed; admit it only after
// that feed can debit every research bucket deterministically.
// Canonical opposite-side depth is not a supported CEX Paper/Shadow execution
// contract. Non-zero funding still needs a point-in-time runtime funding feed
// that can debit every research bucket deterministically.
if costs.cross_spread {
return Err("CEX Paper/Shadow does not support cross-spread execution".to_string());
}
if costs.funding_bps != 0.0 {
return Err("CEX Paper/Shadow does not support non-zero funding costs".to_string());
}
Expand Down
5 changes: 2 additions & 3 deletions rust_hft/apps/live/tests/deployment_envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,7 @@ fn signed_frozen_model_bundle_loads_native_parameters_and_preserves_limits() {
.clone(),
)
.unwrap();
let mut protocol = protocol.with_independent_selection(30).unwrap();
protocol.costs.cross_spread = true;
let protocol = protocol.with_independent_selection(30).unwrap();
let source_ref = serde_json::json!({"id": "test-source", "content_sha256": "a".repeat(64)});
let frozen: FrozenSupervisedCandidateV1 = serde_json::from_value(serde_json::json!({
"schema_version": FROZEN_SUPERVISED_CANDIDATE_SCHEMA, "artifact_id": "",
Expand Down Expand Up @@ -446,7 +445,7 @@ fn signed_frozen_model_bundle_loads_native_parameters_and_preserves_limits() {
assert_eq!(program.factors.len(), 1);
assert_eq!(*max_order_notional, rust_decimal::Decimal::from(500));
assert_eq!(execution_contract.venue, hft_core::VenueId::BINANCE_FUTURES);
assert!(execution_contract.cross_spread);
assert!(!execution_contract.cross_spread);
assert!(config.venues[0].simulate_execution);
assert_eq!(
config.engine.intent_max_order_notional,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,8 @@ mod tests {
let mut client = BinanceExecutionClient::new(make_test_config(ExecutionMode::Paper));
let lifecycle = ports::OrderIntentLifecycle {
max_slippage_bps: Some(25),
max_order_notional: Some(rust_decimal::Decimal::from(10_000)),
max_order_quantity: Some(rust_decimal::Decimal::from(10)),
..Default::default()
};
let envelope = OrderIntentEnvelope::new(
Expand Down
40 changes: 30 additions & 10 deletions rust_hft/execution-gateway/adapters/adapter-binance/src/usdm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1790,6 +1790,30 @@ mod tests {
}
}

fn cex_bounded_envelope(intent: ports::OrderIntent) -> OrderIntentEnvelope {
let now = hft_core::now_micros();
let mut envelope = OrderIntentEnvelope::new(
intent.clone(),
OrderIntentLifecycle {
created_ts: now,
max_slippage_bps: Some(25),
max_order_notional: Some(Decimal::from(1_000_000)),
max_order_quantity: Some(Decimal::from(10)),
max_latency_us: Some(60_000_000),
..Default::default()
},
);
envelope.price_reference = Some(ports::ExecutionPriceReference {
venue: intent.target_venue.expect("usd-m test intent has a venue"),
symbol: intent.symbol.clone(),
side: intent.side,
price: intent.price.expect("usd-m test intent has a price"),
book_sequence: 1,
received_at: hft_core::LocalReceiveTimestamp::new(now),
});
envelope
}

fn valid_order() -> BinanceUsdMOrder {
BinanceUsdMOrder {
symbol: "BTCUSDT".to_string(),
Expand Down Expand Up @@ -1900,12 +1924,8 @@ mod tests {
BinanceCredentials::new("test-key".to_string(), "test-secret".to_string());
cfg.rest_base_url = base_url;
let mut client = BinanceUsdMExecutionClient::new(cfg);
let lifecycle = OrderIntentLifecycle {
reduce_only: true,
..Default::default()
};
let envelope =
OrderIntentEnvelope::new(perp_intent(), lifecycle).with_client_order_id("client-usdm");
let mut envelope = cex_bounded_envelope(perp_intent()).with_client_order_id("client-usdm");
envelope.lifecycle.reduce_only = true;

let order_id = client.place_order_envelope(&envelope).await.unwrap();
assert_eq!(
Expand Down Expand Up @@ -2102,8 +2122,7 @@ mod tests {
BinanceCredentials::new("test-key".to_string(), "test-secret".to_string());
cfg.rest_base_url = base_url;
let mut client = BinanceUsdMExecutionClient::new(cfg);
let envelope = OrderIntentEnvelope::new(perp_intent(), OrderIntentLifecycle::default())
.with_client_order_id("client-usdm");
let envelope = cex_bounded_envelope(perp_intent()).with_client_order_id("client-usdm");

let error = client.place_order_envelope(&envelope).await.unwrap_err();
assert!(
Expand All @@ -2124,8 +2143,7 @@ mod tests {
BinanceCredentials::new("test-key".to_string(), "test-secret".to_string());
cfg.rest_base_url = base_url;
let mut client = BinanceUsdMExecutionClient::new(cfg);
let envelope = OrderIntentEnvelope::new(perp_intent(), OrderIntentLifecycle::default())
.with_client_order_id("client-usdm");
let envelope = cex_bounded_envelope(perp_intent()).with_client_order_id("client-usdm");

let error = client.place_order_envelope(&envelope).await.unwrap_err();
assert!(matches!(error, HftError::Network(message) if message.contains("outcome unknown")));
Expand Down Expand Up @@ -2320,6 +2338,8 @@ mod tests {
async fn usd_m_both_envelope_entrypoints_apply_cex_gate_before_submission() {
let lifecycle = OrderIntentLifecycle {
max_slippage_bps: Some(25),
max_order_notional: Some(Decimal::from(10_000)),
max_order_quantity: Some(Decimal::from(10)),
..Default::default()
};
let envelope = OrderIntentEnvelope::new(perp_intent(), lifecycle)
Expand Down
43 changes: 31 additions & 12 deletions rust_hft/execution-gateway/adapters/adapter-bybit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2421,20 +2421,37 @@ mod tests {
#[tokio::test]
async fn order_envelope_preserves_the_venue_link_id() {
let mut client = BybitExecutionClient::new(make_test_config(ExecutionMode::Paper)).unwrap();
let envelope = OrderIntentEnvelope::new(
OrderIntent::crypto_spot(
Symbol::new("BTCUSDT"),
Side::Buy,
Quantity::from_f64(0.001).unwrap(),
OrderType::Limit,
Some(Price::from_f64(50_000.0).unwrap()),
TimeInForce::GTC,
"test_strategy".to_string(),
None,
),
OrderIntentLifecycle::default(),
let intent = OrderIntent::crypto_spot(
Symbol::new("BTCUSDT"),
Side::Buy,
Quantity::from_f64(0.001).unwrap(),
OrderType::Limit,
Some(Price::from_f64(50_000.0).unwrap()),
TimeInForce::GTC,
"test_strategy".to_string(),
Some(hft_core::VenueId::BYBIT),
);
let now = hft_core::now_micros();
let mut envelope = OrderIntentEnvelope::new(
intent.clone(),
OrderIntentLifecycle {
created_ts: now,
max_slippage_bps: Some(25),
max_order_notional: Some(Decimal::from(1_000_000)),
max_order_quantity: Some(Decimal::from(10)),
max_latency_us: Some(60_000_000),
..Default::default()
},
)
.with_client_order_id("bybit-link-42");
envelope.price_reference = Some(ports::ExecutionPriceReference {
venue: hft_core::VenueId::BYBIT,
symbol: intent.symbol.clone(),
side: intent.side,
price: intent.price.unwrap(),
book_sequence: 1,
received_at: hft_core::LocalReceiveTimestamp::new(now),
});

assert_eq!(
client.place_order_envelope(&envelope).await.unwrap(),
Expand All @@ -2447,6 +2464,8 @@ mod tests {
let mut client = BybitExecutionClient::new(make_test_config(ExecutionMode::Paper)).unwrap();
let lifecycle = OrderIntentLifecycle {
max_slippage_bps: Some(25),
max_order_notional: Some(Decimal::from(10_000)),
max_order_quantity: Some(Decimal::from(10)),
..Default::default()
};
let envelope = OrderIntentEnvelope::new(
Expand Down
3 changes: 3 additions & 0 deletions rust_hft/market-core/engine/src/execution_queues.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,15 +237,18 @@ impl EngineQueues {
self.stats.intent_max_latency_count += 1;
}
OrderIntentRejectReason::InvalidMaxOrderNotional { .. }
| OrderIntentRejectReason::MissingMaxOrderNotional
| OrderIntentRejectReason::OrderNotionalUnpriceable { .. }
| OrderIntentRejectReason::MaxOrderNotionalExceeded { .. } => {
self.stats.intent_order_notional_count += 1;
}
OrderIntentRejectReason::InvalidMaxOrderQuantity { .. }
| OrderIntentRejectReason::MissingMaxOrderQuantity
| OrderIntentRejectReason::MaxOrderQuantityExceeded { .. } => {
self.stats.intent_order_quantity_count += 1;
}
OrderIntentRejectReason::InvalidMaxSlippage { .. }
| OrderIntentRejectReason::MissingMaxSlippage
| OrderIntentRejectReason::MissingSlippageReference
| OrderIntentRejectReason::SlippageReferenceMismatch
| OrderIntentRejectReason::MissingSlippageReferenceLifetime
Expand Down
1 change: 1 addition & 0 deletions rust_hft/market-core/engine/src/execution_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4547,6 +4547,7 @@ mod tests {
intent.order_type = OrderType::Limit;
let mut life = ports::OrderIntentLifecycle::new(now, now + 1_000_000);
life.max_slippage_bps = Some(25);
life.max_order_notional = Some(rust_decimal::Decimal::from(10_000));
let mut envelope = OrderIntentEnvelope::new(intent, life);
envelope.price_reference = snapshots.load().execution_price_reference(&envelope.intent);
assert!(envelope.validate_pre_execution(now, None).is_ok());
Expand Down
12 changes: 10 additions & 2 deletions rust_hft/market-core/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4057,7 +4057,11 @@ mod tests {
create_execution_queues(ExecutionQueueConfig::default());
engine.set_execution_queues(engine_queues);
engine
.set_intent_execution_limits(Some(25), None, None)
.set_intent_execution_limits(
Some(25),
Some(rust_decimal::Decimal::from(10_000)),
Some(rust_decimal::Decimal::from(10)),
)
.unwrap();
let now = now_micros();
let mut envelope =
Expand All @@ -4080,7 +4084,11 @@ mod tests {
create_execution_queues(ExecutionQueueConfig::default());
engine.set_execution_queues(engine_queues);
engine
.set_intent_execution_limits(Some(25), None, None)
.set_intent_execution_limits(
Some(25),
Some(rust_decimal::Decimal::from(10_000)),
Some(rust_decimal::Decimal::from(10)),
)
.unwrap();
let intent = ports::OrderIntent::prediction_market(
Symbol::new("prediction-token"),
Expand Down
27 changes: 24 additions & 3 deletions rust_hft/market-core/engine/tests/backpressure_safety_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

use engine::dataflow::{BackpressurePolicy, IngestionConfig};
use engine::{create_execution_queues, ExecutionQueueConfig, LifecycleIntentSubmitError};
use hft_core::{AccountId, OrderType, Quantity, Side, Symbol, TimeInForce};
use hft_core::{
AccountId, LocalReceiveTimestamp, OrderType, Price, Quantity, Side, Symbol, TimeInForce,
VenueId,
};
use ports::{
ExecutionEvent, OrderIntent, OrderIntentEnvelope, OrderIntentLifecycle, OrderIntentRejectReason,
ExecutionEvent, ExecutionPriceReference, OrderIntent, OrderIntentEnvelope,
OrderIntentLifecycle, OrderIntentRejectReason,
};

fn test_intent() -> OrderIntent {
Expand Down Expand Up @@ -63,6 +67,8 @@ fn queued_price_protection_reloads_market_identity_and_freshness() {
intent.target_venue = Some(VenueId::MOCK);
let mut life = OrderIntentLifecycle::new(1_000, 2_000);
life.max_slippage_bps = Some(25);
life.max_order_notional = Some(rust_decimal::Decimal::from(10_000));
life.max_order_quantity = Some(rust_decimal::Decimal::from(10));
let mut envelope = OrderIntentEnvelope::new(intent, life);
envelope.price_reference = snapshots.load().execution_price_reference(&envelope.intent);
sender.send_lifecycle_intent(envelope, 1_100).unwrap();
Expand Down Expand Up @@ -288,9 +294,24 @@ fn test_current_lifecycle_intent_enters_execution_queue() {
batch_size: 8,
};
let (mut engine_queues, mut worker_queues) = create_execution_queues(config);
let mut intent = test_intent();
intent.order_type = OrderType::Limit;
intent.price = Some(Price::from_f64(100.0).unwrap());
intent.target_venue = Some(VenueId::MOCK);
let mut lifecycle = OrderIntentLifecycle::new(1_000, 1_100);
lifecycle.max_latency_us = Some(99);
let envelope = OrderIntentEnvelope::new(test_intent(), lifecycle);
lifecycle.max_slippage_bps = Some(25);
lifecycle.max_order_notional = Some(rust_decimal::Decimal::from(10_000));
lifecycle.max_order_quantity = Some(rust_decimal::Decimal::from(10));
let mut envelope = OrderIntentEnvelope::new(intent, lifecycle);
envelope.price_reference = Some(ExecutionPriceReference {
venue: VenueId::MOCK,
symbol: Symbol::new("BTCUSDT"),
side: Side::Buy,
price: Price::from_f64(100.0).unwrap(),
book_sequence: 1,
received_at: LocalReceiveTimestamp::new(1_000),
});

assert!(engine_queues.send_lifecycle_intent(envelope, 1_099).is_ok());

Expand Down
Loading