Add futures order execution model
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
|
use crate::events::{AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum FuturesDirection {
|
pub enum FuturesDirection {
|
||||||
Long,
|
Long,
|
||||||
@@ -20,6 +24,39 @@ impl FuturesDirection {
|
|||||||
Self::Short => -1.0,
|
Self::Short => -1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_side(&self) -> OrderSide {
|
||||||
|
match self {
|
||||||
|
Self::Long => OrderSide::Buy,
|
||||||
|
Self::Short => OrderSide::Sell,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_side(&self) -> OrderSide {
|
||||||
|
match self {
|
||||||
|
Self::Long => OrderSide::Sell,
|
||||||
|
Self::Short => OrderSide::Buy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum FuturesPositionEffect {
|
||||||
|
Open,
|
||||||
|
Close,
|
||||||
|
CloseToday,
|
||||||
|
CloseYesterday,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FuturesPositionEffect {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Open => "open",
|
||||||
|
Self::Close => "close",
|
||||||
|
Self::CloseToday => "close_today",
|
||||||
|
Self::CloseYesterday => "close_yesterday",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -29,6 +66,72 @@ pub struct FuturesContractSpec {
|
|||||||
pub short_margin_rate: f64,
|
pub short_margin_rate: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FuturesOrderIntent {
|
||||||
|
pub symbol: String,
|
||||||
|
pub direction: FuturesDirection,
|
||||||
|
pub effect: FuturesPositionEffect,
|
||||||
|
pub spec: FuturesContractSpec,
|
||||||
|
pub quantity: u32,
|
||||||
|
pub price: f64,
|
||||||
|
pub transaction_cost: f64,
|
||||||
|
pub reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FuturesOrderIntent {
|
||||||
|
pub fn open(
|
||||||
|
symbol: impl Into<String>,
|
||||||
|
direction: FuturesDirection,
|
||||||
|
spec: FuturesContractSpec,
|
||||||
|
quantity: u32,
|
||||||
|
price: f64,
|
||||||
|
transaction_cost: f64,
|
||||||
|
reason: impl Into<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
symbol: symbol.into(),
|
||||||
|
direction,
|
||||||
|
effect: FuturesPositionEffect::Open,
|
||||||
|
spec,
|
||||||
|
quantity,
|
||||||
|
price,
|
||||||
|
transaction_cost,
|
||||||
|
reason: reason.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(
|
||||||
|
symbol: impl Into<String>,
|
||||||
|
direction: FuturesDirection,
|
||||||
|
effect: FuturesPositionEffect,
|
||||||
|
spec: FuturesContractSpec,
|
||||||
|
quantity: u32,
|
||||||
|
price: f64,
|
||||||
|
transaction_cost: f64,
|
||||||
|
reason: impl Into<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
symbol: symbol.into(),
|
||||||
|
direction,
|
||||||
|
effect,
|
||||||
|
spec,
|
||||||
|
quantity,
|
||||||
|
price,
|
||||||
|
transaction_cost,
|
||||||
|
reason: reason.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct FuturesExecutionReport {
|
||||||
|
pub order_events: Vec<OrderEvent>,
|
||||||
|
pub fill_events: Vec<FillEvent>,
|
||||||
|
pub position_events: Vec<PositionEvent>,
|
||||||
|
pub account_events: Vec<AccountEvent>,
|
||||||
|
pub diagnostics: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl FuturesContractSpec {
|
impl FuturesContractSpec {
|
||||||
pub fn new(contract_multiplier: f64, long_margin_rate: f64, short_margin_rate: f64) -> Self {
|
pub fn new(contract_multiplier: f64, long_margin_rate: f64, short_margin_rate: f64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -146,6 +249,24 @@ impl FuturesPosition {
|
|||||||
price: f64,
|
price: f64,
|
||||||
transaction_cost: f64,
|
transaction_cost: f64,
|
||||||
) -> Result<f64, String> {
|
) -> Result<f64, String> {
|
||||||
|
self.close_with_effect(
|
||||||
|
quantity,
|
||||||
|
price,
|
||||||
|
transaction_cost,
|
||||||
|
FuturesPositionEffect::Close,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_with_effect(
|
||||||
|
&mut self,
|
||||||
|
quantity: u32,
|
||||||
|
price: f64,
|
||||||
|
transaction_cost: f64,
|
||||||
|
effect: FuturesPositionEffect,
|
||||||
|
) -> Result<f64, String> {
|
||||||
|
if effect == FuturesPositionEffect::Open {
|
||||||
|
return Err("close_with_effect does not accept open effect".to_string());
|
||||||
|
}
|
||||||
if quantity > self.quantity {
|
if quantity > self.quantity {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"close quantity {} exceeds current quantity {} for {} {}",
|
"close quantity {} exceeds current quantity {} for {} {}",
|
||||||
@@ -158,6 +279,37 @@ impl FuturesPosition {
|
|||||||
if quantity == 0 {
|
if quantity == 0 {
|
||||||
return Ok(0.0);
|
return Ok(0.0);
|
||||||
}
|
}
|
||||||
|
match effect {
|
||||||
|
FuturesPositionEffect::Open => unreachable!(),
|
||||||
|
FuturesPositionEffect::Close => {
|
||||||
|
let old_closed = quantity.min(self.old_quantity);
|
||||||
|
self.old_quantity -= old_closed;
|
||||||
|
}
|
||||||
|
FuturesPositionEffect::CloseToday => {
|
||||||
|
let today_quantity = self.today_quantity();
|
||||||
|
if quantity > today_quantity {
|
||||||
|
return Err(format!(
|
||||||
|
"close today quantity {} exceeds today quantity {} for {} {}",
|
||||||
|
quantity,
|
||||||
|
today_quantity,
|
||||||
|
self.symbol,
|
||||||
|
self.direction.as_str()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FuturesPositionEffect::CloseYesterday => {
|
||||||
|
if quantity > self.old_quantity {
|
||||||
|
return Err(format!(
|
||||||
|
"close yesterday quantity {} exceeds old quantity {} for {} {}",
|
||||||
|
quantity,
|
||||||
|
self.old_quantity,
|
||||||
|
self.symbol,
|
||||||
|
self.direction.as_str()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.old_quantity -= quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let realized = (price - self.avg_price)
|
let realized = (price - self.avg_price)
|
||||||
* quantity as f64
|
* quantity as f64
|
||||||
@@ -194,6 +346,7 @@ impl FuturesPosition {
|
|||||||
let cash_delta = self.equity();
|
let cash_delta = self.equity();
|
||||||
self.avg_price = self.last_price;
|
self.avg_price = self.last_price;
|
||||||
self.prev_close = self.last_price;
|
self.prev_close = self.last_price;
|
||||||
|
self.old_quantity = self.quantity;
|
||||||
cash_delta
|
cash_delta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,13 +459,32 @@ impl FuturesAccountState {
|
|||||||
quantity: u32,
|
quantity: u32,
|
||||||
price: f64,
|
price: f64,
|
||||||
transaction_cost: f64,
|
transaction_cost: f64,
|
||||||
|
) -> Result<f64, String> {
|
||||||
|
self.close_with_effect(
|
||||||
|
symbol,
|
||||||
|
direction,
|
||||||
|
quantity,
|
||||||
|
price,
|
||||||
|
transaction_cost,
|
||||||
|
FuturesPositionEffect::Close,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_with_effect(
|
||||||
|
&mut self,
|
||||||
|
symbol: &str,
|
||||||
|
direction: FuturesDirection,
|
||||||
|
quantity: u32,
|
||||||
|
price: f64,
|
||||||
|
transaction_cost: f64,
|
||||||
|
effect: FuturesPositionEffect,
|
||||||
) -> Result<f64, String> {
|
) -> Result<f64, String> {
|
||||||
let key = (symbol.to_string(), direction);
|
let key = (symbol.to_string(), direction);
|
||||||
let position = self
|
let position = self
|
||||||
.positions
|
.positions
|
||||||
.get_mut(&key)
|
.get_mut(&key)
|
||||||
.ok_or_else(|| format!("missing futures position {symbol} {}", direction.as_str()))?;
|
.ok_or_else(|| format!("missing futures position {symbol} {}", direction.as_str()))?;
|
||||||
let cash_delta = position.close(quantity, price, transaction_cost)?;
|
let cash_delta = position.close_with_effect(quantity, price, transaction_cost, effect)?;
|
||||||
self.total_cash += cash_delta;
|
self.total_cash += cash_delta;
|
||||||
if position.quantity == 0 {
|
if position.quantity == 0 {
|
||||||
self.positions.remove(&key);
|
self.positions.remove(&key);
|
||||||
@@ -320,6 +492,183 @@ impl FuturesAccountState {
|
|||||||
Ok(cash_delta)
|
Ok(cash_delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn execute_order(
|
||||||
|
&mut self,
|
||||||
|
date: NaiveDate,
|
||||||
|
order_id: Option<u64>,
|
||||||
|
intent: FuturesOrderIntent,
|
||||||
|
) -> FuturesExecutionReport {
|
||||||
|
let mut report = FuturesExecutionReport::default();
|
||||||
|
let side = if intent.effect == FuturesPositionEffect::Open {
|
||||||
|
intent.direction.open_side()
|
||||||
|
} else {
|
||||||
|
intent.direction.close_side()
|
||||||
|
};
|
||||||
|
|
||||||
|
if intent.quantity == 0 || !intent.price.is_finite() || intent.price <= 0.0 {
|
||||||
|
report.order_events.push(OrderEvent {
|
||||||
|
date,
|
||||||
|
order_id,
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side,
|
||||||
|
requested_quantity: intent.quantity,
|
||||||
|
filled_quantity: 0,
|
||||||
|
status: OrderStatus::Rejected,
|
||||||
|
reason: format!(
|
||||||
|
"{}: invalid futures order effect={} price={} quantity={}",
|
||||||
|
intent.reason,
|
||||||
|
intent.effect.as_str(),
|
||||||
|
intent.price,
|
||||||
|
intent.quantity
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cash_before = self.total_cash();
|
||||||
|
let position_before = self
|
||||||
|
.position(&intent.symbol, intent.direction)
|
||||||
|
.map(|position| position.quantity)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let result = match intent.effect {
|
||||||
|
FuturesPositionEffect::Open => {
|
||||||
|
let mut projected = self.clone();
|
||||||
|
projected.open(
|
||||||
|
intent.symbol.clone(),
|
||||||
|
intent.direction,
|
||||||
|
intent.spec,
|
||||||
|
intent.quantity,
|
||||||
|
intent.price,
|
||||||
|
intent.transaction_cost,
|
||||||
|
);
|
||||||
|
if projected.cash() < -1e-8 {
|
||||||
|
Err(format!(
|
||||||
|
"insufficient futures margin available_cash={:.2} required_margin_after={:.2}",
|
||||||
|
self.cash(),
|
||||||
|
projected.margin()
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
self.open(
|
||||||
|
intent.symbol.clone(),
|
||||||
|
intent.direction,
|
||||||
|
intent.spec,
|
||||||
|
intent.quantity,
|
||||||
|
intent.price,
|
||||||
|
intent.transaction_cost,
|
||||||
|
);
|
||||||
|
Ok(-intent.transaction_cost.max(0.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FuturesPositionEffect::Close
|
||||||
|
| FuturesPositionEffect::CloseToday
|
||||||
|
| FuturesPositionEffect::CloseYesterday => self.close_with_effect(
|
||||||
|
&intent.symbol,
|
||||||
|
intent.direction,
|
||||||
|
intent.quantity,
|
||||||
|
intent.price,
|
||||||
|
intent.transaction_cost,
|
||||||
|
intent.effect,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(cash_delta) => {
|
||||||
|
let position_after = self
|
||||||
|
.position(&intent.symbol, intent.direction)
|
||||||
|
.map(|position| position.quantity)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let avg_price_after = self
|
||||||
|
.position(&intent.symbol, intent.direction)
|
||||||
|
.map(|position| position.avg_price)
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let notional =
|
||||||
|
intent.price * intent.quantity as f64 * intent.spec.contract_multiplier;
|
||||||
|
report.fill_events.push(FillEvent {
|
||||||
|
date,
|
||||||
|
order_id,
|
||||||
|
symbol: intent.symbol.clone(),
|
||||||
|
side,
|
||||||
|
quantity: intent.quantity,
|
||||||
|
price: intent.price,
|
||||||
|
gross_amount: notional,
|
||||||
|
commission: intent.transaction_cost.max(0.0),
|
||||||
|
stamp_tax: 0.0,
|
||||||
|
net_cash_flow: cash_delta,
|
||||||
|
reason: format!(
|
||||||
|
"{} direction={} effect={}",
|
||||||
|
intent.reason,
|
||||||
|
intent.direction.as_str(),
|
||||||
|
intent.effect.as_str()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
report.position_events.push(PositionEvent {
|
||||||
|
date,
|
||||||
|
symbol: intent.symbol.clone(),
|
||||||
|
delta_quantity: position_after as i32 - position_before as i32,
|
||||||
|
quantity_after: position_after,
|
||||||
|
average_cost: avg_price_after,
|
||||||
|
realized_pnl_delta: if intent.effect == FuturesPositionEffect::Open {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
cash_delta
|
||||||
|
},
|
||||||
|
reason: format!(
|
||||||
|
"{} direction={} effect={}",
|
||||||
|
intent.reason,
|
||||||
|
intent.direction.as_str(),
|
||||||
|
intent.effect.as_str()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
report.account_events.push(AccountEvent {
|
||||||
|
date,
|
||||||
|
cash_before,
|
||||||
|
cash_after: self.total_cash(),
|
||||||
|
total_equity: self.total_value(),
|
||||||
|
note: format!(
|
||||||
|
"futures {} {} {}",
|
||||||
|
intent.symbol,
|
||||||
|
intent.direction.as_str(),
|
||||||
|
intent.effect.as_str()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
report.order_events.push(OrderEvent {
|
||||||
|
date,
|
||||||
|
order_id,
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side,
|
||||||
|
requested_quantity: intent.quantity,
|
||||||
|
filled_quantity: intent.quantity,
|
||||||
|
status: OrderStatus::Filled,
|
||||||
|
reason: format!(
|
||||||
|
"{} direction={} effect={}",
|
||||||
|
intent.reason,
|
||||||
|
intent.direction.as_str(),
|
||||||
|
intent.effect.as_str()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(reason) => {
|
||||||
|
report.order_events.push(OrderEvent {
|
||||||
|
date,
|
||||||
|
order_id,
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side,
|
||||||
|
requested_quantity: intent.quantity,
|
||||||
|
filled_quantity: 0,
|
||||||
|
status: OrderStatus::Rejected,
|
||||||
|
reason: format!(
|
||||||
|
"{}: {} direction={} effect={}",
|
||||||
|
intent.reason,
|
||||||
|
reason,
|
||||||
|
intent.direction.as_str(),
|
||||||
|
intent.effect.as_str()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mark_price(&mut self, symbol: &str, direction: FuturesDirection, price: f64) {
|
pub fn mark_price(&mut self, symbol: &str, direction: FuturesDirection, price: f64) {
|
||||||
if let Some(position) = self.positions.get_mut(&(symbol.to_string(), direction)) {
|
if let Some(position) = self.positions.get_mut(&(symbol.to_string(), direction)) {
|
||||||
position.mark_price(price);
|
position.mark_price(price);
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ pub use events::{
|
|||||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||||
ProcessEventKind,
|
ProcessEventKind,
|
||||||
};
|
};
|
||||||
pub use futures::{FuturesAccountState, FuturesContractSpec, FuturesDirection, FuturesPosition};
|
pub use futures::{
|
||||||
|
FuturesAccountState, FuturesContractSpec, FuturesDirection, FuturesExecutionReport,
|
||||||
|
FuturesOrderIntent, FuturesPosition, FuturesPositionEffect,
|
||||||
|
};
|
||||||
pub use instrument::Instrument;
|
pub use instrument::Instrument;
|
||||||
pub use metrics::{BacktestMetrics, compute_backtest_metrics};
|
pub use metrics::{BacktestMetrics, compute_backtest_metrics};
|
||||||
pub use platform_expr_strategy::{
|
pub use platform_expr_strategy::{
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use fidc_core::{FuturesAccountState, FuturesContractSpec, FuturesDirection};
|
use chrono::NaiveDate;
|
||||||
|
use fidc_core::{
|
||||||
|
FuturesAccountState, FuturesContractSpec, FuturesDirection, FuturesOrderIntent,
|
||||||
|
FuturesPositionEffect, OrderStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
||||||
|
NaiveDate::from_ymd_opt(year, month, day).expect("valid date")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn futures_account_tracks_long_margin_pnl_and_settlement() {
|
fn futures_account_tracks_long_margin_pnl_and_settlement() {
|
||||||
@@ -50,3 +58,106 @@ fn futures_account_tracks_short_close_cash_delta() {
|
|||||||
assert_eq!(position.quantity, 3);
|
assert_eq!(position.quantity, 3);
|
||||||
assert!((position.equity() - 900.0).abs() < 1e-6);
|
assert!((position.equity() - 900.0).abs() < 1e-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn futures_order_execution_splits_close_between_old_and_today_quantity() {
|
||||||
|
let spec = FuturesContractSpec::new(300.0, 0.12, 0.12);
|
||||||
|
let mut account = FuturesAccountState::new(1_000_000.0);
|
||||||
|
|
||||||
|
account.open("IF2501", FuturesDirection::Long, spec, 3, 4000.0, 0.0);
|
||||||
|
account.begin_trading_day();
|
||||||
|
account.open("IF2501", FuturesDirection::Long, spec, 2, 4010.0, 0.0);
|
||||||
|
|
||||||
|
let report = account.execute_order(
|
||||||
|
d(2025, 1, 2),
|
||||||
|
Some(10),
|
||||||
|
FuturesOrderIntent::close(
|
||||||
|
"IF2501",
|
||||||
|
FuturesDirection::Long,
|
||||||
|
FuturesPositionEffect::Close,
|
||||||
|
spec,
|
||||||
|
4,
|
||||||
|
4020.0,
|
||||||
|
4.0,
|
||||||
|
"auto close",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(report.order_events.len(), 1);
|
||||||
|
assert_eq!(report.order_events[0].status, OrderStatus::Filled);
|
||||||
|
assert_eq!(report.fill_events[0].quantity, 4);
|
||||||
|
assert!((report.fill_events[0].net_cash_flow - 19_196.0).abs() < 1e-6);
|
||||||
|
let position = account
|
||||||
|
.position("IF2501", FuturesDirection::Long)
|
||||||
|
.expect("remaining long position");
|
||||||
|
assert_eq!(position.quantity, 1);
|
||||||
|
assert_eq!(position.old_quantity, 0);
|
||||||
|
assert_eq!(position.today_quantity(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn futures_close_today_rejects_when_today_quantity_is_insufficient() {
|
||||||
|
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
|
||||||
|
let mut account = FuturesAccountState::new(100_000.0);
|
||||||
|
|
||||||
|
account.open("RB2501", FuturesDirection::Short, spec, 2, 3500.0, 0.0);
|
||||||
|
account.begin_trading_day();
|
||||||
|
|
||||||
|
let report = account.execute_order(
|
||||||
|
d(2025, 1, 3),
|
||||||
|
Some(11),
|
||||||
|
FuturesOrderIntent::close(
|
||||||
|
"RB2501",
|
||||||
|
FuturesDirection::Short,
|
||||||
|
FuturesPositionEffect::CloseToday,
|
||||||
|
spec,
|
||||||
|
1,
|
||||||
|
3490.0,
|
||||||
|
1.0,
|
||||||
|
"close today without today position",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(report.order_events.len(), 1);
|
||||||
|
assert_eq!(report.order_events[0].status, OrderStatus::Rejected);
|
||||||
|
assert!(
|
||||||
|
report.order_events[0]
|
||||||
|
.reason
|
||||||
|
.contains("close today quantity")
|
||||||
|
);
|
||||||
|
let position = account
|
||||||
|
.position("RB2501", FuturesDirection::Short)
|
||||||
|
.expect("short position unchanged");
|
||||||
|
assert_eq!(position.quantity, 2);
|
||||||
|
assert_eq!(position.old_quantity, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn futures_open_order_rejects_when_margin_is_insufficient() {
|
||||||
|
let spec = FuturesContractSpec::new(300.0, 0.2, 0.2);
|
||||||
|
let mut account = FuturesAccountState::new(10_000.0);
|
||||||
|
|
||||||
|
let report = account.execute_order(
|
||||||
|
d(2025, 1, 6),
|
||||||
|
Some(12),
|
||||||
|
FuturesOrderIntent::open(
|
||||||
|
"IF2501",
|
||||||
|
FuturesDirection::Long,
|
||||||
|
spec,
|
||||||
|
1,
|
||||||
|
4000.0,
|
||||||
|
2.0,
|
||||||
|
"oversized open",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(report.order_events.len(), 1);
|
||||||
|
assert_eq!(report.order_events[0].status, OrderStatus::Rejected);
|
||||||
|
assert!(
|
||||||
|
report.order_events[0]
|
||||||
|
.reason
|
||||||
|
.contains("insufficient futures margin")
|
||||||
|
);
|
||||||
|
assert!(account.position("IF2501", FuturesDirection::Long).is_none());
|
||||||
|
assert!((account.total_cash() - 10_000.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|||||||
@@ -87,9 +87,10 @@ current alignment pass.
|
|||||||
`account_by_type("STOCK")`)
|
`account_by_type("STOCK")`)
|
||||||
- [x] standalone futures account model with contract multiplier, long/short
|
- [x] standalone futures account model with contract multiplier, long/short
|
||||||
margin, daily mark-to-market settlement, and short close cashflow
|
margin, daily mark-to-market settlement, and short close cashflow
|
||||||
|
- [x] standalone futures order execution model with open, close, close-today,
|
||||||
|
close-yesterday, margin rejection, order/fill/position/account events
|
||||||
- [ ] wire futures account into the generic backtest engine runtime
|
- [ ] wire futures account into the generic backtest engine runtime
|
||||||
- [ ] futures order intents, matching, close-today semantics, and expiration
|
- [ ] futures intraday matching integration and expiration settlement
|
||||||
settlement
|
|
||||||
|
|
||||||
## Execution Order
|
## Execution Order
|
||||||
|
|
||||||
@@ -108,5 +109,6 @@ current alignment pass.
|
|||||||
Active implementation target: continue account parity after exposing the stock
|
Active implementation target: continue account parity after exposing the stock
|
||||||
account runtime view, core Portfolio fields, deposit/withdraw, financing
|
account runtime view, core Portfolio fields, deposit/withdraw, financing
|
||||||
liability APIs, management-fee callbacks, stock account accessors, and the
|
liability APIs, management-fee callbacks, stock account accessors, and the
|
||||||
standalone futures account model; next gap is wiring futures into the generic
|
standalone futures account/order execution model; next gap is wiring futures
|
||||||
engine runtime and adding futures-specific order matching semantics.
|
into the generic engine runtime and adding futures intraday/expiration
|
||||||
|
semantics.
|
||||||
|
|||||||
Reference in New Issue
Block a user