修正目标市值盘中估值口径
This commit is contained in:
+151
-27
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user