实现FIDC配置化风控与交易成本
This commit is contained in:
@@ -71,11 +71,11 @@ fn aiquant_cost_model_matches_alv_run_options() {
|
||||
let model = ChinaAShareCostModel::aiquant_default();
|
||||
|
||||
let buy = model.calculate(d(2026, 5, 19), OrderSide::Buy, 49_978.84);
|
||||
assert!((buy.commission - 12.49471).abs() < 1e-9);
|
||||
assert!((buy.commission - 14.993652).abs() < 1e-9);
|
||||
assert_eq!(buy.stamp_tax, 0.0);
|
||||
|
||||
let sell = model.calculate(d(2026, 5, 19), OrderSide::Sell, 100_724.72);
|
||||
assert!((sell.commission - 25.18118).abs() < 1e-9);
|
||||
assert!((sell.commission - 30.217416).abs() < 1e-9);
|
||||
assert!((sell.stamp_tax - 50.36236).abs() < 1e-9);
|
||||
|
||||
let small_buy = model.calculate(d(2026, 5, 19), OrderSide::Buy, 1_000.0);
|
||||
|
||||
@@ -88,6 +88,7 @@ impl Strategy for BuyAndHoldStrategy {
|
||||
},
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ impl Strategy for BuyThenHoldStrategy {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
});
|
||||
}
|
||||
Ok(StrategyDecision::default())
|
||||
|
||||
@@ -294,6 +294,7 @@ impl Strategy for HookProbeStrategy {
|
||||
order_intents: Vec::new(),
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -336,6 +337,7 @@ impl Strategy for AuctionOrderStrategy {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -384,6 +386,7 @@ impl Strategy for FuturesOrderStrategy {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -716,6 +719,7 @@ impl Strategy for LimitCarryStrategy {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -793,6 +797,7 @@ impl Strategy for UniverseDirectiveStrategy {
|
||||
order_intents,
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -816,6 +821,7 @@ impl Strategy for MinuteProbeStrategy {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -857,6 +863,7 @@ impl Strategy for MinuteProbeStrategy {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -958,6 +965,7 @@ impl Strategy for OrderInspectionStrategy {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1015,6 +1023,7 @@ impl Strategy for AccountFlowStrategy {
|
||||
],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3765,6 +3774,7 @@ impl Strategy for BuyMissingRowThenHoldStrategy {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
});
|
||||
}
|
||||
Ok(StrategyDecision::default())
|
||||
|
||||
@@ -2,8 +2,9 @@ use chrono::{NaiveDate, NaiveTime};
|
||||
use fidc_core::{
|
||||
AlgoOrderStyle, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, ChinaAShareCostModel,
|
||||
ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, DynamicSlippageConfig,
|
||||
Instrument, IntradayExecutionQuote, MatchingType, OrderIntent, OrderStatus, PortfolioState,
|
||||
PriceField, ProcessEventKind, SlippageModel, StrategyDecision, TargetPortfolioOrderPricing,
|
||||
FidcRiskControlConfig, Instrument, IntradayExecutionQuote, MatchingType, OrderIntent,
|
||||
OrderStatus, PortfolioState, PriceField, ProcessEventKind, SlippageModel, StrategyDecision,
|
||||
TargetPortfolioOrderPricing,
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
@@ -104,12 +105,87 @@ fn execute_single_value_order(
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
(portfolio, report)
|
||||
}
|
||||
|
||||
fn single_symbol_limit_price_data(
|
||||
date: NaiveDate,
|
||||
symbol: &str,
|
||||
price: f64,
|
||||
upper_limit: f64,
|
||||
lower_limit: f64,
|
||||
) -> DataSet {
|
||||
DataSet::from_components(
|
||||
vec![Instrument {
|
||||
symbol: symbol.to_string(),
|
||||
name: "Test".to_string(),
|
||||
board: "SZ".to_string(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()),
|
||||
delisted_at: None,
|
||||
status: "active".to_string(),
|
||||
}],
|
||||
vec![DailyMarketSnapshot {
|
||||
date,
|
||||
symbol: symbol.to_string(),
|
||||
timestamp: Some(format!("{date} 10:18:00")),
|
||||
day_open: price,
|
||||
open: price,
|
||||
high: price,
|
||||
low: price,
|
||||
close: price,
|
||||
last_price: price,
|
||||
bid1: price,
|
||||
ask1: price,
|
||||
prev_close: 10.0,
|
||||
volume: 1_000_000,
|
||||
minute_volume: 1_000_000,
|
||||
bid1_volume: 500_000,
|
||||
ask1_volume: 500_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit,
|
||||
lower_limit,
|
||||
price_tick: 0.01,
|
||||
}],
|
||||
vec![DailyFactorSnapshot {
|
||||
date,
|
||||
symbol: symbol.to_string(),
|
||||
market_cap_bn: 50.0,
|
||||
free_float_cap_bn: 45.0,
|
||||
pe_ttm: 15.0,
|
||||
turnover_ratio: Some(2.0),
|
||||
effective_turnover_ratio: Some(1.8),
|
||||
extra_factors: BTreeMap::new(),
|
||||
}],
|
||||
vec![CandidateEligibility {
|
||||
date,
|
||||
symbol: symbol.to_string(),
|
||||
is_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
}],
|
||||
vec![BenchmarkSnapshot {
|
||||
date,
|
||||
benchmark: "000300.SH".to_string(),
|
||||
open: 100.0,
|
||||
close: 100.0,
|
||||
prev_close: 99.0,
|
||||
volume: 1_000_000,
|
||||
}],
|
||||
)
|
||||
.expect("dataset")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broker_executes_explicit_order_value_buy() {
|
||||
let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||
@@ -215,6 +291,7 @@ fn broker_executes_explicit_order_value_buy() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -370,6 +447,7 @@ fn broker_delayed_limit_open_sell_uses_minute_price() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -504,6 +582,7 @@ fn broker_executes_order_shares_and_order_lots() {
|
||||
],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -613,6 +692,7 @@ fn broker_executes_target_shares_like_order_to() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -793,6 +873,7 @@ fn broker_executes_target_portfolio_smart_with_custom_prices() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -943,6 +1024,7 @@ fn broker_executes_target_portfolio_smart_with_algo_order_style() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -1050,6 +1132,7 @@ fn broker_executes_order_percent_and_target_percent() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("percent execution");
|
||||
@@ -1073,6 +1156,7 @@ fn broker_executes_order_percent_and_target_percent() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("target percent execution");
|
||||
@@ -1172,6 +1256,7 @@ fn broker_uses_day_open_price_for_open_auction_matching() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -1276,6 +1361,7 @@ fn broker_open_auction_uses_auction_volume_without_quote_liquidity() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -1376,6 +1462,7 @@ fn broker_cancels_buy_when_open_hits_upper_limit() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -1490,6 +1577,7 @@ fn broker_applies_price_ratio_slippage_on_snapshot_fills() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -1592,6 +1680,7 @@ fn broker_applies_dynamic_slippage_on_snapshot_fills() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -1710,6 +1799,7 @@ fn broker_applies_tick_size_slippage_on_intraday_last_fills() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -1813,6 +1903,7 @@ fn broker_rejects_intraday_last_order_without_execution_quotes() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -1934,6 +2025,7 @@ fn broker_executes_intraday_last_on_start_quote_without_trade_delta() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -2050,6 +2142,7 @@ fn broker_cancels_market_order_remainder_when_intraday_quote_liquidity_exhausted
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -2163,6 +2256,7 @@ fn broker_cancels_market_buy_when_minute_has_no_volume() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -2296,6 +2390,7 @@ fn broker_splits_intraday_quote_fills_and_tracks_commission_by_order() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -2458,6 +2553,7 @@ fn broker_aggregates_intraday_quote_fills_into_vwap_leg() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -2631,6 +2727,7 @@ fn broker_executes_algo_vwap_value_with_time_window() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -2776,6 +2873,7 @@ fn broker_executes_algo_twap_percent_across_window_quotes() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -2906,6 +3004,7 @@ fn broker_uses_best_own_price_for_intraday_matching() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -3021,6 +3120,7 @@ fn broker_uses_best_counterparty_price_for_intraday_matching() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -3183,6 +3283,7 @@ fn rebalance_optimizer_skips_unfunded_buy_when_existing_position_cannot_sell() {
|
||||
order_intents: Vec::new(),
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -3376,6 +3477,7 @@ fn rebalance_uses_prev_close_for_open_auction_valuation() {
|
||||
order_intents: Vec::new(),
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -3555,6 +3657,7 @@ fn rebalance_optimizer_prioritizes_higher_target_weight_when_cash_is_tight() {
|
||||
order_intents: Vec::new(),
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -3681,6 +3784,7 @@ fn broker_uses_board_specific_min_quantity_and_step_size_for_buy_sizing() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -3780,6 +3884,7 @@ fn broker_allows_bjse_quantities_above_minimum_without_round_lot_step() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -3883,6 +3988,7 @@ fn broker_allows_full_odd_lot_sell_when_liquidating_position() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -3894,7 +4000,7 @@ fn broker_allows_full_odd_lot_sell_when_liquidating_position() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_day_sell_then_rebuy_reinserts_position_at_end() {
|
||||
fn same_day_sell_then_rebuy_is_rejected_by_default() {
|
||||
let prev_date = NaiveDate::from_ymd_opt(2024, 1, 9).unwrap();
|
||||
let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||
let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"];
|
||||
@@ -4020,6 +4126,149 @@ fn same_day_sell_then_rebuy_reinserts_position_at_end() {
|
||||
],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
|
||||
let symbols = portfolio.positions().keys().cloned().collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
symbols,
|
||||
vec!["000001.SZ".to_string(), "000003.SZ".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_day_sell_then_rebuy_can_be_allowed_by_policy() {
|
||||
let prev_date = NaiveDate::from_ymd_opt(2024, 1, 9).unwrap();
|
||||
let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||
let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"];
|
||||
let instruments = symbols
|
||||
.iter()
|
||||
.map(|symbol| Instrument {
|
||||
symbol: (*symbol).to_string(),
|
||||
name: (*symbol).to_string(),
|
||||
board: "SZ".to_string(),
|
||||
round_lot: 100,
|
||||
listed_at: None,
|
||||
delisted_at: None,
|
||||
status: "active".to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let market = symbols
|
||||
.iter()
|
||||
.map(|symbol| DailyMarketSnapshot {
|
||||
date,
|
||||
symbol: (*symbol).to_string(),
|
||||
timestamp: Some("2024-01-10 10:18:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
high: 10.1,
|
||||
low: 9.9,
|
||||
close: 10.0,
|
||||
last_price: 10.0,
|
||||
bid1: 9.99,
|
||||
ask1: 10.01,
|
||||
prev_close: 10.0,
|
||||
volume: 100_000,
|
||||
minute_volume: 100_000,
|
||||
bid1_volume: 80_000,
|
||||
ask1_volume: 80_000,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
paused: false,
|
||||
upper_limit: 11.0,
|
||||
lower_limit: 9.0,
|
||||
price_tick: 0.01,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let factors = symbols
|
||||
.iter()
|
||||
.map(|symbol| DailyFactorSnapshot {
|
||||
date,
|
||||
symbol: (*symbol).to_string(),
|
||||
market_cap_bn: 50.0,
|
||||
free_float_cap_bn: 45.0,
|
||||
pe_ttm: 15.0,
|
||||
turnover_ratio: Some(2.0),
|
||||
effective_turnover_ratio: Some(1.8),
|
||||
extra_factors: BTreeMap::new(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let candidates = symbols
|
||||
.iter()
|
||||
.map(|symbol| CandidateEligibility {
|
||||
date,
|
||||
symbol: (*symbol).to_string(),
|
||||
is_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let data = DataSet::from_components(
|
||||
instruments,
|
||||
market,
|
||||
factors,
|
||||
candidates,
|
||||
vec![BenchmarkSnapshot {
|
||||
date,
|
||||
benchmark: "000300.SH".to_string(),
|
||||
open: 100.0,
|
||||
close: 100.0,
|
||||
prev_close: 99.0,
|
||||
volume: 1_000_000,
|
||||
}],
|
||||
)
|
||||
.expect("dataset");
|
||||
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
portfolio
|
||||
.position_mut("000001.SZ")
|
||||
.buy(prev_date, 100, 10.0);
|
||||
portfolio
|
||||
.position_mut("000002.SZ")
|
||||
.buy(prev_date, 100, 10.0);
|
||||
portfolio
|
||||
.position_mut("000003.SZ")
|
||||
.buy(prev_date, 100, 10.0);
|
||||
|
||||
let mut risk_config = FidcRiskControlConfig::default();
|
||||
risk_config.static_rules.forbid_same_day_rebuy_after_sell = false;
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
.with_risk_config(risk_config);
|
||||
|
||||
broker
|
||||
.execute(
|
||||
date,
|
||||
&mut portfolio,
|
||||
&data,
|
||||
&StrategyDecision {
|
||||
rebalance: false,
|
||||
target_weights: BTreeMap::new(),
|
||||
exit_symbols: BTreeSet::new(),
|
||||
order_intents: vec![
|
||||
OrderIntent::TargetValue {
|
||||
symbol: "000002.SZ".to_string(),
|
||||
target_value: 0.0,
|
||||
reason: "sell_then_rebuy".to_string(),
|
||||
},
|
||||
OrderIntent::Value {
|
||||
symbol: "000002.SZ".to_string(),
|
||||
value: 10_000.0,
|
||||
reason: "sell_then_rebuy".to_string(),
|
||||
},
|
||||
],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -4035,6 +4284,94 @@ fn same_day_sell_then_rebuy_reinserts_position_at_end() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broker_configured_policy_can_allow_upper_limit_buy() {
|
||||
let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||
let data = single_symbol_limit_price_data(date, "000002.SZ", 11.0, 11.0, 9.0);
|
||||
let mut risk_config = FidcRiskControlConfig::default();
|
||||
risk_config.static_rules.reject_upper_limit_buy = false;
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
.with_risk_config(risk_config);
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
|
||||
let report = broker
|
||||
.execute(
|
||||
date,
|
||||
&mut portfolio,
|
||||
&data,
|
||||
&StrategyDecision {
|
||||
rebalance: false,
|
||||
target_weights: BTreeMap::new(),
|
||||
exit_symbols: BTreeSet::new(),
|
||||
order_intents: vec![OrderIntent::Value {
|
||||
symbol: "000002.SZ".to_string(),
|
||||
value: 10_000.0,
|
||||
reason: "configured_upper_limit_buy".to_string(),
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
|
||||
assert_eq!(report.fill_events.len(), 1);
|
||||
assert_eq!(
|
||||
report.order_events.last().map(|event| event.status),
|
||||
Some(OrderStatus::Filled)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broker_configured_policy_can_allow_lower_limit_sell() {
|
||||
let prev_date = NaiveDate::from_ymd_opt(2024, 1, 9).unwrap();
|
||||
let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||
let data = single_symbol_limit_price_data(date, "000002.SZ", 9.0, 11.0, 9.0);
|
||||
let mut risk_config = FidcRiskControlConfig::default();
|
||||
risk_config.static_rules.reject_lower_limit_sell = false;
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
)
|
||||
.with_risk_config(risk_config);
|
||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||
portfolio
|
||||
.position_mut("000002.SZ")
|
||||
.buy(prev_date, 1_000, 10.0);
|
||||
|
||||
let report = broker
|
||||
.execute(
|
||||
date,
|
||||
&mut portfolio,
|
||||
&data,
|
||||
&StrategyDecision {
|
||||
rebalance: false,
|
||||
target_weights: BTreeMap::new(),
|
||||
exit_symbols: BTreeSet::new(),
|
||||
order_intents: vec![OrderIntent::TargetValue {
|
||||
symbol: "000002.SZ".to_string(),
|
||||
target_value: 0.0,
|
||||
reason: "configured_lower_limit_sell".to_string(),
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
|
||||
assert_eq!(report.fill_events.len(), 1);
|
||||
assert_eq!(
|
||||
report.order_events.last().map(|event| event.status),
|
||||
Some(OrderStatus::Filled)
|
||||
);
|
||||
}
|
||||
|
||||
fn two_day_limit_order_data(day1_open: f64, day2_open: f64) -> DataSet {
|
||||
let day1 = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
|
||||
let day2 = NaiveDate::from_ymd_opt(2024, 1, 11).unwrap();
|
||||
@@ -4195,6 +4532,7 @@ fn broker_rejects_open_limit_buy_at_market_close() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("day1 execution");
|
||||
@@ -4229,6 +4567,7 @@ fn broker_rejects_open_limit_buy_at_market_close() {
|
||||
order_intents: Vec::new(),
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("day2 execution");
|
||||
@@ -4266,6 +4605,7 @@ fn broker_uses_limit_price_slippage_for_limit_orders() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -4303,6 +4643,7 @@ fn broker_rejects_limit_buy_when_final_execution_price_reaches_upper_limit() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -4346,6 +4687,7 @@ fn broker_executes_limit_value_and_limit_percent_intents() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -4370,6 +4712,7 @@ fn broker_executes_limit_value_and_limit_percent_intents() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
@@ -4405,6 +4748,7 @@ fn broker_cancels_open_order_by_order_id() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("day1 execution");
|
||||
@@ -4425,6 +4769,7 @@ fn broker_cancels_open_order_by_order_id() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("day2 execution");
|
||||
@@ -4471,6 +4816,7 @@ fn broker_emits_cancellation_reject_for_unknown_order() {
|
||||
}],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("cancel reject execution");
|
||||
@@ -4585,6 +4931,7 @@ fn broker_reserves_sellable_quantity_for_open_limit_sells() {
|
||||
],
|
||||
notes: Vec::new(),
|
||||
diagnostics: Vec::new(),
|
||||
risk_decisions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("broker execution");
|
||||
|
||||
Reference in New Issue
Block a user