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

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,
symbol: &str,
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 {
let start_cursor = self
.runtime_intraday_start_time
@@ -353,11 +406,13 @@ where
.map(|cursor| quote.timestamp >= cursor)
.unwrap_or(true)
})
.next()
.and_then(|quote| match self.execution_price_field {
PriceField::Last => (quote.last_price.is_finite() && quote.last_price > 0.0)
.then_some(quote.last_price),
_ => quote.buy_price(),
.find_map(|quote| {
self.select_quote_reference_price(
snapshot,
quote,
side,
self.matching_type_for_algo_request(None),
)
})
.unwrap_or_else(|| self.sizing_price(snapshot))
}
@@ -2677,9 +2732,8 @@ where
commission_state: &mut BTreeMap<u64, f64>,
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
let price = data
let snapshot = data
.market(date, symbol)
.map(|snapshot| self.sizing_price(snapshot))
.ok_or_else(|| BacktestError::MissingPrice {
date,
symbol: symbol.to_string(),
@@ -2689,20 +2743,27 @@ where
.position(symbol)
.map(|pos| pos.quantity)
.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(
date,
portfolio,
data,
symbol,
current_qty - target_qty,
current_qty,
self.reserve_order_id(),
reason,
intraday_turnover,
@@ -2715,27 +2776,28 @@ where
None,
report,
)?;
} else if target_qty > current_qty {
self.process_buy(
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_value(
date,
portfolio,
data,
symbol,
target_qty - current_qty,
self.reserve_order_id(),
cash_delta,
reason,
intraday_turnover,
execution_cursors,
global_execution_cursor,
commission_state,
None,
None,
false,
true,
None,
report,
)?;
} else if (current_value - target_value).abs() <= f64::EPSILON {
} else {
report.order_events.push(OrderEvent {
date,
order_id: None,
@@ -3142,7 +3204,7 @@ where
report,
)
} 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(
((value.abs()) / price).floor() as u32,
self.minimum_order_quantity(data, symbol),
@@ -4887,9 +4949,11 @@ mod tests {
use super::{BrokerSimulator, MatchingType};
use crate::cost::ChinaAShareCostModel;
use crate::data::{
CandidateEligibility, DailyMarketSnapshot, IntradayExecutionQuote, PriceField,
BenchmarkSnapshot, CandidateEligibility, DailyMarketSnapshot, DataSet,
IntradayExecutionQuote, PriceField,
};
use crate::events::OrderSide;
use crate::instrument::Instrument;
use crate::rules::ChinaEquityRuleHooks;
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]
fn next_tick_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
@@ -4976,6 +5064,42 @@ mod tests {
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]
fn instantaneous_twap_without_limits_does_not_cap_quote_quantity() {
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 prev_closes = sorted.iter().map(|row| row.prev_close).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 close_prefix = prefix_sums(&closes);
let prev_close_prefix = prefix_sums(&prev_closes);