修正目标市值盘中估值口径

This commit is contained in:
boris
2026-06-13 21:09:38 +08:00
parent a030554ab6
commit 0813ce3ffb
2 changed files with 155 additions and 28 deletions
+151 -27
View File
@@ -340,6 +340,59 @@ where
data: &DataSet, data: &DataSet,
symbol: &str, symbol: &str,
snapshot: &crate::data::DailyMarketSnapshot, snapshot: &crate::data::DailyMarketSnapshot,
) -> f64 {
self.value_order_sizing_price(date, data, symbol, snapshot, OrderSide::Buy)
}
fn value_sell_sizing_price(
&self,
date: NaiveDate,
data: &DataSet,
symbol: &str,
snapshot: &crate::data::DailyMarketSnapshot,
) -> f64 {
self.value_order_sizing_price(date, data, symbol, snapshot, OrderSide::Sell)
}
fn target_value_valuation_price(
&self,
date: NaiveDate,
data: &DataSet,
symbol: &str,
snapshot: &crate::data::DailyMarketSnapshot,
) -> f64 {
if self.execution_price_field == PriceField::Last {
let start_cursor = self
.runtime_intraday_start_time
.get()
.or(self.intraday_execution_start_time)
.map(|start_time| date.and_time(start_time));
if let Some(price) = data
.execution_quotes_on(date, symbol)
.iter()
.filter(|quote| {
start_cursor
.map(|cursor| quote.timestamp >= cursor)
.unwrap_or(true)
})
.find_map(|quote| {
(quote.last_price.is_finite() && quote.last_price > 0.0)
.then_some(quote.last_price)
})
{
return price;
}
}
self.sizing_price(snapshot)
}
fn value_order_sizing_price(
&self,
date: NaiveDate,
data: &DataSet,
symbol: &str,
snapshot: &crate::data::DailyMarketSnapshot,
side: OrderSide,
) -> f64 { ) -> f64 {
let start_cursor = self let start_cursor = self
.runtime_intraday_start_time .runtime_intraday_start_time
@@ -353,11 +406,13 @@ where
.map(|cursor| quote.timestamp >= cursor) .map(|cursor| quote.timestamp >= cursor)
.unwrap_or(true) .unwrap_or(true)
}) })
.next() .find_map(|quote| {
.and_then(|quote| match self.execution_price_field { self.select_quote_reference_price(
PriceField::Last => (quote.last_price.is_finite() && quote.last_price > 0.0) snapshot,
.then_some(quote.last_price), quote,
_ => quote.buy_price(), side,
self.matching_type_for_algo_request(None),
)
}) })
.unwrap_or_else(|| self.sizing_price(snapshot)) .unwrap_or_else(|| self.sizing_price(snapshot))
} }
@@ -2677,9 +2732,8 @@ where
commission_state: &mut BTreeMap<u64, f64>, commission_state: &mut BTreeMap<u64, f64>,
report: &mut BrokerExecutionReport, report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> { ) -> Result<(), BacktestError> {
let price = data let snapshot = data
.market(date, symbol) .market(date, symbol)
.map(|snapshot| self.sizing_price(snapshot))
.ok_or_else(|| BacktestError::MissingPrice { .ok_or_else(|| BacktestError::MissingPrice {
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
@@ -2689,20 +2743,27 @@ where
.position(symbol) .position(symbol)
.map(|pos| pos.quantity) .map(|pos| pos.quantity)
.unwrap_or(0); .unwrap_or(0);
let current_value = price * current_qty as f64;
let target_qty = self.round_buy_quantity(
((target_value.max(0.0)) / price).floor() as u32,
self.minimum_order_quantity(data, symbol),
self.order_step_size(data, symbol),
);
if current_qty > target_qty { 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( self.process_sell(
date, date,
portfolio, portfolio,
data, data,
symbol, symbol,
current_qty - target_qty, current_qty,
self.reserve_order_id(), self.reserve_order_id(),
reason, reason,
intraday_turnover, intraday_turnover,
@@ -2715,27 +2776,28 @@ where
None, None,
report, report,
)?; )?;
} else if target_qty > current_qty { return Ok(());
self.process_buy( }
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_value(
date, date,
portfolio, portfolio,
data, data,
symbol, symbol,
target_qty - current_qty, cash_delta,
self.reserve_order_id(),
reason, reason,
intraday_turnover, intraday_turnover,
execution_cursors, execution_cursors,
global_execution_cursor, global_execution_cursor,
commission_state, commission_state,
None,
None,
false,
true,
None,
report, report,
)?; )?;
} else if (current_value - target_value).abs() <= f64::EPSILON { } else {
report.order_events.push(OrderEvent { report.order_events.push(OrderEvent {
date, date,
order_id: None, order_id: None,
@@ -3142,7 +3204,7 @@ where
report, report,
) )
} else { } else {
let price = self.sizing_price(snapshot); let price = self.value_sell_sizing_price(date, data, symbol, snapshot);
let requested_qty = self.round_buy_quantity( let requested_qty = self.round_buy_quantity(
((value.abs()) / price).floor() as u32, ((value.abs()) / price).floor() as u32,
self.minimum_order_quantity(data, symbol), self.minimum_order_quantity(data, symbol),
@@ -4887,9 +4949,11 @@ mod tests {
use super::{BrokerSimulator, MatchingType}; use super::{BrokerSimulator, MatchingType};
use crate::cost::ChinaAShareCostModel; use crate::cost::ChinaAShareCostModel;
use crate::data::{ use crate::data::{
CandidateEligibility, DailyMarketSnapshot, IntradayExecutionQuote, PriceField, BenchmarkSnapshot, CandidateEligibility, DailyMarketSnapshot, DataSet,
IntradayExecutionQuote, PriceField,
}; };
use crate::events::OrderSide; use crate::events::OrderSide;
use crate::instrument::Instrument;
use crate::rules::ChinaEquityRuleHooks; use crate::rules::ChinaEquityRuleHooks;
fn limit_test_snapshot() -> DailyMarketSnapshot { fn limit_test_snapshot() -> DailyMarketSnapshot {
@@ -4951,6 +5015,30 @@ mod tests {
} }
} }
fn limit_test_instrument() -> Instrument {
Instrument {
symbol: "000001.SZ".to_string(),
name: "PingAn".to_string(),
board: "SZ".to_string(),
round_lot: 100,
listed_at: None,
delisted_at: None,
status: "active".to_string(),
}
}
fn limit_test_benchmark() -> BenchmarkSnapshot {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
BenchmarkSnapshot {
date,
benchmark: "000852.SH".to_string(),
open: 1000.0,
close: 1000.0,
prev_close: 1000.0,
volume: 1_000_000,
}
}
#[test] #[test]
fn next_tick_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() { fn next_tick_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks) let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
@@ -4976,6 +5064,42 @@ mod tests {
assert!(liquidity_limited.quote_quantity_limited(MatchingType::NextTickLast)); assert!(liquidity_limited.quote_quantity_limited(MatchingType::NextTickLast));
} }
#[test]
fn target_value_sizing_uses_intraday_tick_instead_of_daily_snapshot() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Last,
)
.with_intraday_execution_start_time(date.and_hms_opt(9, 33, 0).unwrap().time());
let mut snapshot = limit_test_snapshot();
snapshot.last_price = 10.0;
snapshot.close = 10.0;
let mut quote = limit_test_quote(11.0, 10.99, 11.01);
quote.timestamp = date.and_hms_opt(9, 34, 0).expect("valid timestamp");
let data = DataSet::from_components_with_actions_and_quotes(
vec![limit_test_instrument()],
vec![snapshot.clone()],
Vec::new(),
vec![limit_test_candidate(true, true)],
vec![limit_test_benchmark()],
Vec::new(),
vec![quote],
)
.expect("valid dataset");
let snapshot = data.market(date, "000001.SZ").expect("market snapshot");
assert_eq!(
broker.target_value_valuation_price(date, &data, "000001.SZ", snapshot),
11.0
);
assert_eq!(
broker.value_sell_sizing_price(date, &data, "000001.SZ", snapshot),
11.0
);
}
#[test] #[test]
fn instantaneous_twap_without_limits_does_not_cap_quote_quantity() { fn instantaneous_twap_without_limits_does_not_cap_quote_quantity() {
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks) let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
+4 -1
View File
@@ -503,7 +503,10 @@ impl SymbolPriceSeries {
let closes = sorted.iter().map(|row| row.close).collect::<Vec<_>>(); let closes = sorted.iter().map(|row| row.close).collect::<Vec<_>>();
let prev_closes = sorted.iter().map(|row| row.prev_close).collect::<Vec<_>>(); let prev_closes = sorted.iter().map(|row| row.prev_close).collect::<Vec<_>>();
let last_prices = sorted.iter().map(|row| row.last_price).collect::<Vec<_>>(); let last_prices = sorted.iter().map(|row| row.last_price).collect::<Vec<_>>();
let volumes = sorted.iter().map(|row| row.volume as f64).collect::<Vec<_>>(); let volumes = sorted
.iter()
.map(|row| row.volume as f64)
.collect::<Vec<_>>();
let open_prefix = prefix_sums(&opens); let open_prefix = prefix_sums(&opens);
let close_prefix = prefix_sums(&closes); let close_prefix = prefix_sums(&closes);
let prev_close_prefix = prefix_sums(&prev_closes); let prev_close_prefix = prefix_sums(&prev_closes);