修正FiRisk强平执行时间口径

This commit is contained in:
boris
2026-06-20 22:46:43 +08:00
parent 816fc48077
commit cdca7984ed
4 changed files with 262 additions and 5 deletions
+217
View File
@@ -908,6 +908,29 @@ where
commission_state,
report,
),
OrderIntent::TimedTargetValue {
symbol,
target_value,
style,
start_time,
end_time,
reason,
} => self.process_timed_target_value(
date,
portfolio,
data,
symbol,
*target_value,
*style,
*start_time,
*end_time,
reason,
intraday_turnover,
execution_cursors,
global_execution_cursor,
commission_state,
report,
),
OrderIntent::LimitTargetValue {
symbol,
target_value,
@@ -3011,6 +3034,118 @@ where
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn process_timed_target_value(
&self,
date: NaiveDate,
portfolio: &mut PortfolioState,
data: &DataSet,
symbol: &str,
target_value: f64,
style: AlgoOrderStyle,
start_time: Option<NaiveTime>,
end_time: Option<NaiveTime>,
reason: &str,
intraday_turnover: &mut BTreeMap<String, u32>,
execution_cursors: &mut BTreeMap<String, NaiveDateTime>,
global_execution_cursor: &mut Option<NaiveDateTime>,
commission_state: &mut BTreeMap<u64, f64>,
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
let snapshot = data
.market(date, symbol)
.ok_or_else(|| BacktestError::MissingPrice {
date,
symbol: symbol.to_string(),
field: price_field_name(self.execution_price_field),
})?;
let current_qty = portfolio
.position(symbol)
.map(|pos| pos.quantity)
.unwrap_or(0);
let algo_request = AlgoExecutionRequest {
style: match style {
AlgoOrderStyle::Vwap => AlgoExecutionStyle::Vwap,
AlgoOrderStyle::Twap => AlgoExecutionStyle::Twap,
},
start_time,
end_time,
};
if target_value <= f64::EPSILON {
if current_qty == 0 {
report.order_events.push(OrderEvent {
date,
order_id: None,
symbol: symbol.to_string(),
side: OrderSide::Sell,
requested_quantity: 0,
filled_quantity: 0,
status: OrderStatus::Filled,
reason: format!("{reason}: already at target value"),
});
return Ok(());
}
self.process_sell(
date,
portfolio,
data,
symbol,
current_qty,
self.reserve_order_id(),
reason,
intraday_turnover,
execution_cursors,
global_execution_cursor,
commission_state,
None,
false,
true,
Some(&algo_request),
report,
)?;
return Ok(());
}
let valuation_price = self.target_value_valuation_price(date, data, symbol, snapshot);
let current_value = valuation_price * current_qty as f64;
let cash_delta = target_value.max(0.0) - current_value;
if cash_delta.abs() > f64::EPSILON {
self.process_algo_value(
date,
portfolio,
data,
symbol,
cash_delta,
style,
start_time,
end_time,
reason,
intraday_turnover,
execution_cursors,
global_execution_cursor,
commission_state,
report,
)?;
} else {
report.order_events.push(OrderEvent {
date,
order_id: None,
symbol: symbol.to_string(),
side: if current_qty > 0 {
OrderSide::Sell
} else {
OrderSide::Buy
},
requested_quantity: 0,
filled_quantity: 0,
status: OrderStatus::Filled,
reason: format!("{reason}: already at target value"),
});
}
Ok(())
}
fn process_target_shares(
&self,
date: NaiveDate,
@@ -5265,6 +5400,7 @@ mod tests {
use crate::instrument::Instrument;
use crate::portfolio::PortfolioState;
use crate::rules::ChinaEquityRuleHooks;
use crate::strategy::AlgoOrderStyle;
fn limit_test_snapshot() -> DailyMarketSnapshot {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
@@ -5410,6 +5546,87 @@ mod tests {
);
}
#[test]
fn timed_target_value_zero_sells_full_position_at_requested_time_quote() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
let prev_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date");
let symbol = "000001.SZ";
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Last,
)
.with_intraday_execution_start_time(date.and_hms_opt(10, 18, 0).unwrap().time())
.with_matching_type(MatchingType::NextTickLast)
.with_slippage_model(SlippageModel::PriceRatio(0.001))
.with_volume_limit(false)
.with_liquidity_limit(false);
let mut snapshot = limit_test_snapshot();
snapshot.symbol = symbol.to_string();
snapshot.last_price = 4.21;
snapshot.close = 4.21;
snapshot.bid1 = 4.20;
snapshot.ask1 = 4.22;
snapshot.prev_close = 4.15;
snapshot.upper_limit = 4.98;
snapshot.lower_limit = 3.32;
let mut quote_0931 = limit_test_quote(4.11, 4.10, 4.12);
quote_0931.symbol = symbol.to_string();
quote_0931.timestamp = date.and_hms_opt(9, 31, 0).expect("valid timestamp");
let mut quote_1018 = limit_test_quote(4.21, 4.20, 4.22);
quote_1018.symbol = symbol.to_string();
quote_1018.timestamp = date.and_hms_opt(10, 18, 0).expect("valid timestamp");
let data = DataSet::from_components_with_actions_and_quotes(
vec![limit_test_instrument()],
vec![snapshot],
Vec::new(),
vec![limit_test_candidate(true, true)],
vec![limit_test_benchmark()],
Vec::new(),
vec![quote_0931, quote_1018],
)
.expect("valid dataset");
let mut portfolio = PortfolioState::new(1_000_000.0);
portfolio.position_mut(symbol).buy(prev_date, 72_600, 4.0);
portfolio.apply_cash_delta(-290_400.0);
let mut report = BrokerExecutionReport::default();
broker
.process_timed_target_value(
date,
&mut portfolio,
&data,
symbol,
0.0,
AlgoOrderStyle::Twap,
Some(date.and_hms_opt(9, 31, 0).unwrap().time()),
Some(date.and_hms_opt(9, 31, 0).unwrap().time()),
"risk_forced_exit",
&mut BTreeMap::new(),
&mut BTreeMap::new(),
&mut None,
&mut BTreeMap::new(),
&mut report,
)
.expect("timed target value");
assert!(
!report.fill_events.is_empty(),
"order_events={:?}",
report.order_events
);
let fill = report.fill_events.last().expect("fill event");
assert_eq!(fill.quantity, 72_600);
assert!((fill.price - 4.10589).abs() < 1e-6, "{fill:?}");
assert_eq!(
portfolio
.position(symbol)
.map(|position| position.quantity)
.unwrap_or(0),
0
);
}
#[test]
fn aiquant_target_value_delta_uses_scheduled_mark_price() {
let date = chrono::NaiveDate::from_ymd_opt(2023, 5, 8).expect("valid date");