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