Align projected stock order sizing semantics
This commit is contained in:
@@ -214,9 +214,16 @@ impl PlatformExprStrategy {
|
|||||||
engine.register_fn("pow", |lhs: f64, rhs: f64| lhs.powf(rhs));
|
engine.register_fn("pow", |lhs: f64, rhs: f64| lhs.powf(rhs));
|
||||||
engine.register_fn("log", |value: f64| value.ln());
|
engine.register_fn("log", |value: f64| value.ln());
|
||||||
engine.register_fn("exp", |value: f64| value.exp());
|
engine.register_fn("exp", |value: f64| value.exp());
|
||||||
engine.register_fn("clamp", |value: f64, low: f64, high: f64| value.clamp(low, high));
|
engine.register_fn("clamp", |value: f64, low: f64, high: f64| {
|
||||||
engine.register_fn("between", |value: f64, low: f64, high: f64| value >= low && value <= high);
|
value.clamp(low, high)
|
||||||
engine.register_fn("nz", |value: f64, fallback: f64| if value.is_finite() { value } else { fallback });
|
});
|
||||||
|
engine.register_fn("between", |value: f64, low: f64, high: f64| {
|
||||||
|
value >= low && value <= high
|
||||||
|
});
|
||||||
|
engine.register_fn(
|
||||||
|
"nz",
|
||||||
|
|value: f64, fallback: f64| if value.is_finite() { value } else { fallback },
|
||||||
|
);
|
||||||
engine.register_fn("safe_div", |lhs: f64, rhs: f64, fallback: f64| {
|
engine.register_fn("safe_div", |lhs: f64, rhs: f64, fallback: f64| {
|
||||||
if rhs.abs() <= f64::EPSILON {
|
if rhs.abs() <= f64::EPSILON {
|
||||||
fallback
|
fallback
|
||||||
@@ -224,12 +231,25 @@ impl PlatformExprStrategy {
|
|||||||
lhs / rhs
|
lhs / rhs
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
engine.register_fn("iff", |condition: bool, when_true: Dynamic, when_false: Dynamic| {
|
engine.register_fn(
|
||||||
if condition { when_true } else { when_false }
|
"iff",
|
||||||
|
|condition: bool, when_true: Dynamic, when_false: Dynamic| {
|
||||||
|
if condition {
|
||||||
|
when_true
|
||||||
|
} else {
|
||||||
|
when_false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
engine.register_fn("contains", |value: &str, needle: &str| {
|
||||||
|
value.contains(needle)
|
||||||
|
});
|
||||||
|
engine.register_fn("starts_with", |value: &str, prefix: &str| {
|
||||||
|
value.starts_with(prefix)
|
||||||
|
});
|
||||||
|
engine.register_fn("ends_with", |value: &str, suffix: &str| {
|
||||||
|
value.ends_with(suffix)
|
||||||
});
|
});
|
||||||
engine.register_fn("contains", |value: &str, needle: &str| value.contains(needle));
|
|
||||||
engine.register_fn("starts_with", |value: &str, prefix: &str| value.starts_with(prefix));
|
|
||||||
engine.register_fn("ends_with", |value: &str, suffix: &str| value.ends_with(suffix));
|
|
||||||
engine.register_fn("lower", |value: &str| value.to_lowercase());
|
engine.register_fn("lower", |value: &str| value.to_lowercase());
|
||||||
engine.register_fn("upper", |value: &str| value.to_uppercase());
|
engine.register_fn("upper", |value: &str| value.to_uppercase());
|
||||||
engine.register_fn("trim", |value: &str| value.trim().to_string());
|
engine.register_fn("trim", |value: &str| value.trim().to_string());
|
||||||
@@ -352,12 +372,42 @@ impl PlatformExprStrategy {
|
|||||||
|
|
||||||
fn sell_cost(&self, date: NaiveDate, gross_amount: f64) -> f64 {
|
fn sell_cost(&self, date: NaiveDate, gross_amount: f64) -> f64 {
|
||||||
let model = ChinaAShareCostModel::default();
|
let model = ChinaAShareCostModel::default();
|
||||||
model.commission_for(gross_amount) + model.stamp_tax_for(date, OrderSide::Sell, gross_amount)
|
model.commission_for(gross_amount)
|
||||||
|
+ model.stamp_tax_for(date, OrderSide::Sell, gross_amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn round_lot_quantity(&self, quantity: u32, round_lot: u32) -> u32 {
|
fn round_lot_quantity(
|
||||||
let lot = round_lot.max(1);
|
&self,
|
||||||
(quantity / lot) * lot
|
quantity: u32,
|
||||||
|
minimum_order_quantity: u32,
|
||||||
|
order_step_size: u32,
|
||||||
|
) -> u32 {
|
||||||
|
let step = order_step_size.max(1);
|
||||||
|
let normalized = (quantity / step) * step;
|
||||||
|
if normalized < minimum_order_quantity.max(1) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_order_quantity(
|
||||||
|
&self,
|
||||||
|
quantity: u32,
|
||||||
|
minimum_order_quantity: u32,
|
||||||
|
order_step_size: u32,
|
||||||
|
) -> u32 {
|
||||||
|
let minimum = minimum_order_quantity.max(1);
|
||||||
|
if quantity <= minimum {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
let next = quantity.saturating_sub(order_step_size.max(1));
|
||||||
|
if next < minimum {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
next
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn projected_round_lot(&self, ctx: &StrategyContext<'_>, symbol: &str) -> u32 {
|
fn projected_round_lot(&self, ctx: &StrategyContext<'_>, symbol: &str) -> u32 {
|
||||||
@@ -368,11 +418,23 @@ impl PlatformExprStrategy {
|
|||||||
.max(1)
|
.max(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn projected_execution_price(
|
fn projected_minimum_order_quantity(&self, ctx: &StrategyContext<'_>, symbol: &str) -> u32 {
|
||||||
&self,
|
ctx.data
|
||||||
market: &DailyMarketSnapshot,
|
.instrument(symbol)
|
||||||
side: OrderSide,
|
.map(|instrument| instrument.minimum_order_quantity())
|
||||||
) -> f64 {
|
.unwrap_or(100)
|
||||||
|
.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn projected_order_step_size(&self, ctx: &StrategyContext<'_>, symbol: &str) -> u32 {
|
||||||
|
ctx.data
|
||||||
|
.instrument(symbol)
|
||||||
|
.map(|instrument| instrument.order_step_size())
|
||||||
|
.unwrap_or(100)
|
||||||
|
.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn projected_execution_price(&self, market: &DailyMarketSnapshot, side: OrderSide) -> f64 {
|
||||||
match side {
|
match side {
|
||||||
OrderSide::Buy => market.buy_price(PriceField::Last),
|
OrderSide::Buy => market.buy_price(PriceField::Last),
|
||||||
OrderSide::Sell => market.sell_price(PriceField::Last),
|
OrderSide::Sell => market.sell_price(PriceField::Last),
|
||||||
@@ -396,6 +458,9 @@ impl PlatformExprStrategy {
|
|||||||
side: OrderSide,
|
side: OrderSide,
|
||||||
requested_qty: u32,
|
requested_qty: u32,
|
||||||
round_lot: u32,
|
round_lot: u32,
|
||||||
|
minimum_order_quantity: u32,
|
||||||
|
order_step_size: u32,
|
||||||
|
allow_odd_lot_sell: bool,
|
||||||
cash_limit: Option<f64>,
|
cash_limit: Option<f64>,
|
||||||
gross_limit: Option<f64>,
|
gross_limit: Option<f64>,
|
||||||
execution_state: &ProjectedExecutionState,
|
execution_state: &ProjectedExecutionState,
|
||||||
@@ -410,19 +475,31 @@ impl PlatformExprStrategy {
|
|||||||
let quantity = match side {
|
let quantity = match side {
|
||||||
OrderSide::Buy => {
|
OrderSide::Buy => {
|
||||||
let cash = cash_limit.unwrap_or(f64::INFINITY);
|
let cash = cash_limit.unwrap_or(f64::INFINITY);
|
||||||
let lot = round_lot.max(1);
|
let mut take_qty = self.round_lot_quantity(
|
||||||
let mut take_qty = self.round_lot_quantity(requested_qty, lot);
|
requested_qty,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
);
|
||||||
while take_qty > 0 {
|
while take_qty > 0 {
|
||||||
let candidate_gross = execution_price * take_qty as f64;
|
let candidate_gross = execution_price * take_qty as f64;
|
||||||
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
|
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
|
||||||
take_qty = take_qty.saturating_sub(lot);
|
take_qty = self.decrement_order_quantity(
|
||||||
|
take_qty,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let candidate_cash = candidate_gross + self.buy_commission(candidate_gross);
|
let candidate_cash =
|
||||||
|
candidate_gross + self.buy_commission(candidate_gross);
|
||||||
if candidate_cash <= cash + 1e-6 {
|
if candidate_cash <= cash + 1e-6 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
take_qty = take_qty.saturating_sub(lot);
|
take_qty = self.decrement_order_quantity(
|
||||||
|
take_qty,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
take_qty
|
take_qty
|
||||||
}
|
}
|
||||||
@@ -439,7 +516,6 @@ impl PlatformExprStrategy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let lot = round_lot.max(1);
|
|
||||||
let start_cursor = self.projected_execution_start_cursor(date, symbol, execution_state);
|
let start_cursor = self.projected_execution_start_cursor(date, symbol, execution_state);
|
||||||
let quotes = ctx.data.execution_quotes_on(date, symbol);
|
let quotes = ctx.data.execution_quotes_on(date, symbol);
|
||||||
let mut filled_qty = 0_u32;
|
let mut filled_qty = 0_u32;
|
||||||
@@ -461,7 +537,7 @@ impl PlatformExprStrategy {
|
|||||||
OrderSide::Buy => quote.ask1_volume,
|
OrderSide::Buy => quote.ask1_volume,
|
||||||
OrderSide::Sell => quote.bid1_volume,
|
OrderSide::Sell => quote.bid1_volume,
|
||||||
}
|
}
|
||||||
.saturating_mul(lot as u64)
|
.saturating_mul(round_lot.max(1) as u64)
|
||||||
.min(u32::MAX as u64) as u32;
|
.min(u32::MAX as u64) as u32;
|
||||||
if available_qty == 0 {
|
if available_qty == 0 {
|
||||||
continue;
|
continue;
|
||||||
@@ -471,7 +547,11 @@ impl PlatformExprStrategy {
|
|||||||
if remaining_qty == 0 {
|
if remaining_qty == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let mut take_qty = self.round_lot_quantity(remaining_qty.min(available_qty), lot);
|
let mut take_qty = remaining_qty.min(available_qty);
|
||||||
|
if !(side == OrderSide::Sell && allow_odd_lot_sell && take_qty == remaining_qty) {
|
||||||
|
take_qty =
|
||||||
|
self.round_lot_quantity(take_qty, minimum_order_quantity, order_step_size);
|
||||||
|
}
|
||||||
if take_qty == 0 {
|
if take_qty == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -480,13 +560,21 @@ impl PlatformExprStrategy {
|
|||||||
while take_qty > 0 {
|
while take_qty > 0 {
|
||||||
let candidate_gross = gross_amount + quote_price * take_qty as f64;
|
let candidate_gross = gross_amount + quote_price * take_qty as f64;
|
||||||
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
|
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
|
||||||
take_qty = take_qty.saturating_sub(lot);
|
take_qty = self.decrement_order_quantity(
|
||||||
|
take_qty,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if candidate_gross + self.buy_commission(candidate_gross) <= cash + 1e-6 {
|
if candidate_gross + self.buy_commission(candidate_gross) <= cash + 1e-6 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
take_qty = take_qty.saturating_sub(lot);
|
take_qty = self.decrement_order_quantity(
|
||||||
|
take_qty,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if take_qty == 0 {
|
if take_qty == 0 {
|
||||||
break;
|
break;
|
||||||
@@ -525,6 +613,8 @@ impl PlatformExprStrategy {
|
|||||||
}
|
}
|
||||||
let market = ctx.data.market(date, symbol)?;
|
let market = ctx.data.market(date, symbol)?;
|
||||||
let round_lot = self.projected_round_lot(ctx, symbol);
|
let round_lot = self.projected_round_lot(ctx, symbol);
|
||||||
|
let minimum_order_quantity = self.projected_minimum_order_quantity(ctx, symbol);
|
||||||
|
let order_step_size = self.projected_order_step_size(ctx, symbol);
|
||||||
let fill = self
|
let fill = self
|
||||||
.projected_select_execution_fill(
|
.projected_select_execution_fill(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -533,6 +623,9 @@ impl PlatformExprStrategy {
|
|||||||
OrderSide::Sell,
|
OrderSide::Sell,
|
||||||
quantity,
|
quantity,
|
||||||
round_lot,
|
round_lot,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
true,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
execution_state,
|
execution_state,
|
||||||
@@ -574,6 +667,8 @@ impl PlatformExprStrategy {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let round_lot = self.projected_round_lot(ctx, symbol);
|
let round_lot = self.projected_round_lot(ctx, symbol);
|
||||||
|
let minimum_order_quantity = self.projected_minimum_order_quantity(ctx, symbol);
|
||||||
|
let order_step_size = self.projected_order_step_size(ctx, symbol);
|
||||||
let market = match ctx.data.market(date, symbol) {
|
let market = match ctx.data.market(date, symbol) {
|
||||||
Some(market) => market,
|
Some(market) => market,
|
||||||
None => return 0,
|
None => return 0,
|
||||||
@@ -584,16 +679,20 @@ impl PlatformExprStrategy {
|
|||||||
}
|
}
|
||||||
let snapshot_requested_qty = self.round_lot_quantity(
|
let snapshot_requested_qty = self.round_lot_quantity(
|
||||||
((projected.cash().min(order_value)) / sizing_price).floor() as u32,
|
((projected.cash().min(order_value)) / sizing_price).floor() as u32,
|
||||||
round_lot,
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
);
|
);
|
||||||
let execution_price = self.projected_execution_price(market, OrderSide::Buy);
|
let execution_price = self.projected_execution_price(market, OrderSide::Buy);
|
||||||
let mut quantity = snapshot_requested_qty;
|
let mut quantity = snapshot_requested_qty;
|
||||||
while quantity > 0 {
|
while quantity > 0 {
|
||||||
let gross_amount = execution_price * quantity as f64;
|
let gross_amount = execution_price * quantity as f64;
|
||||||
if gross_amount <= order_value + 400.0 && gross_amount + self.buy_commission(gross_amount) <= projected.cash() + 1e-6 {
|
if gross_amount <= order_value + 400.0
|
||||||
|
&& gross_amount + self.buy_commission(gross_amount) <= projected.cash() + 1e-6
|
||||||
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
quantity = quantity.saturating_sub(round_lot);
|
quantity =
|
||||||
|
self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size);
|
||||||
}
|
}
|
||||||
if quantity == 0 {
|
if quantity == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -606,6 +705,9 @@ impl PlatformExprStrategy {
|
|||||||
OrderSide::Buy,
|
OrderSide::Buy,
|
||||||
quantity,
|
quantity,
|
||||||
round_lot,
|
round_lot,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
false,
|
||||||
Some(projected.cash()),
|
Some(projected.cash()),
|
||||||
Some(order_value + 400.0),
|
Some(order_value + 400.0),
|
||||||
execution_state,
|
execution_state,
|
||||||
@@ -622,7 +724,9 @@ impl PlatformExprStrategy {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
projected.apply_cash_delta(-cash_out);
|
projected.apply_cash_delta(-cash_out);
|
||||||
projected.position_mut(symbol).buy(date, fill.quantity, fill.price);
|
projected
|
||||||
|
.position_mut(symbol)
|
||||||
|
.buy(date, fill.quantity, fill.price);
|
||||||
*execution_state
|
*execution_state
|
||||||
.intraday_turnover
|
.intraday_turnover
|
||||||
.entry(symbol.to_string())
|
.entry(symbol.to_string())
|
||||||
@@ -716,9 +820,18 @@ impl PlatformExprStrategy {
|
|||||||
.market_decision_close_moving_average(date, &self.config.signal_symbol, 30)
|
.market_decision_close_moving_average(date, &self.config.signal_symbol, 30)
|
||||||
.unwrap_or(benchmark_ma20);
|
.unwrap_or(benchmark_ma20);
|
||||||
let cash = ctx.portfolio.cash();
|
let cash = ctx.portfolio.cash();
|
||||||
let market_value = ctx.portfolio.positions().values().map(|position| position.market_value()).sum::<f64>();
|
let market_value = ctx
|
||||||
|
.portfolio
|
||||||
|
.positions()
|
||||||
|
.values()
|
||||||
|
.map(|position| position.market_value())
|
||||||
|
.sum::<f64>();
|
||||||
let total_equity = cash + market_value;
|
let total_equity = cash + market_value;
|
||||||
let current_exposure = if total_equity > 0.0 { market_value / total_equity } else { 0.0 };
|
let current_exposure = if total_equity > 0.0 {
|
||||||
|
market_value / total_equity
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
let next_day = date + Duration::days(1);
|
let next_day = date + Duration::days(1);
|
||||||
let is_month_end = next_day.month() != date.month();
|
let is_month_end = next_day.month() != date.month();
|
||||||
|
|
||||||
@@ -850,7 +963,9 @@ impl PlatformExprStrategy {
|
|||||||
upper_limit: market.upper_limit,
|
upper_limit: market.upper_limit,
|
||||||
lower_limit: market.lower_limit,
|
lower_limit: market.lower_limit,
|
||||||
price_tick: market.price_tick,
|
price_tick: market.price_tick,
|
||||||
round_lot: instrument.map(|item| item.effective_round_lot()).unwrap_or(100) as i64,
|
round_lot: instrument
|
||||||
|
.map(|item| item.effective_round_lot())
|
||||||
|
.unwrap_or(100) as i64,
|
||||||
paused: market.paused || candidate.is_paused,
|
paused: market.paused || candidate.is_paused,
|
||||||
is_st: candidate.is_st || self.special_name(ctx, symbol),
|
is_st: candidate.is_st || self.special_name(ctx, symbol),
|
||||||
is_kcb: candidate.is_kcb,
|
is_kcb: candidate.is_kcb,
|
||||||
@@ -927,12 +1042,21 @@ impl PlatformExprStrategy {
|
|||||||
day_factors.insert("benchmark_ma10".into(), Dynamic::from(day.benchmark_ma10));
|
day_factors.insert("benchmark_ma10".into(), Dynamic::from(day.benchmark_ma10));
|
||||||
day_factors.insert("benchmark_ma20".into(), Dynamic::from(day.benchmark_ma20));
|
day_factors.insert("benchmark_ma20".into(), Dynamic::from(day.benchmark_ma20));
|
||||||
day_factors.insert("benchmark_ma30".into(), Dynamic::from(day.benchmark_ma30));
|
day_factors.insert("benchmark_ma30".into(), Dynamic::from(day.benchmark_ma30));
|
||||||
day_factors.insert("benchmark_ma_short".into(), Dynamic::from(day.benchmark_ma_short));
|
day_factors.insert(
|
||||||
day_factors.insert("benchmark_ma_long".into(), Dynamic::from(day.benchmark_ma_long));
|
"benchmark_ma_short".into(),
|
||||||
|
Dynamic::from(day.benchmark_ma_short),
|
||||||
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"benchmark_ma_long".into(),
|
||||||
|
Dynamic::from(day.benchmark_ma_long),
|
||||||
|
);
|
||||||
day_factors.insert("cash".into(), Dynamic::from(day.cash));
|
day_factors.insert("cash".into(), Dynamic::from(day.cash));
|
||||||
day_factors.insert("market_value".into(), Dynamic::from(day.market_value));
|
day_factors.insert("market_value".into(), Dynamic::from(day.market_value));
|
||||||
day_factors.insert("total_equity".into(), Dynamic::from(day.total_equity));
|
day_factors.insert("total_equity".into(), Dynamic::from(day.total_equity));
|
||||||
day_factors.insert("current_exposure".into(), Dynamic::from(day.current_exposure));
|
day_factors.insert(
|
||||||
|
"current_exposure".into(),
|
||||||
|
Dynamic::from(day.current_exposure),
|
||||||
|
);
|
||||||
day_factors.insert("position_count".into(), Dynamic::from(day.position_count));
|
day_factors.insert("position_count".into(), Dynamic::from(day.position_count));
|
||||||
day_factors.insert("max_positions".into(), Dynamic::from(day.max_positions));
|
day_factors.insert("max_positions".into(), Dynamic::from(day.max_positions));
|
||||||
day_factors.insert("refresh_rate".into(), Dynamic::from(day.refresh_rate));
|
day_factors.insert("refresh_rate".into(), Dynamic::from(day.refresh_rate));
|
||||||
@@ -947,8 +1071,10 @@ impl PlatformExprStrategy {
|
|||||||
day_factors.insert("is_month_end".into(), Dynamic::from(day.is_month_end));
|
day_factors.insert("is_month_end".into(), Dynamic::from(day.is_month_end));
|
||||||
scope.push("day_factors", day_factors);
|
scope.push("day_factors", day_factors);
|
||||||
if let Some(stock) = stock {
|
if let Some(stock) = stock {
|
||||||
let at_upper_limit = Self::price_is_at_limit(stock.last, stock.upper_limit, stock.price_tick);
|
let at_upper_limit =
|
||||||
let at_lower_limit = Self::price_is_at_limit(stock.last, stock.lower_limit, stock.price_tick);
|
Self::price_is_at_limit(stock.last, stock.upper_limit, stock.price_tick);
|
||||||
|
let at_lower_limit =
|
||||||
|
Self::price_is_at_limit(stock.last, stock.lower_limit, stock.price_tick);
|
||||||
scope.push("symbol", stock.symbol.clone());
|
scope.push("symbol", stock.symbol.clone());
|
||||||
scope.push("market_cap", stock.market_cap);
|
scope.push("market_cap", stock.market_cap);
|
||||||
scope.push("free_float_cap", stock.free_float_cap);
|
scope.push("free_float_cap", stock.free_float_cap);
|
||||||
@@ -1038,8 +1164,14 @@ impl PlatformExprStrategy {
|
|||||||
"touched_lower_limit".into(),
|
"touched_lower_limit".into(),
|
||||||
Dynamic::from(stock.touched_lower_limit),
|
Dynamic::from(stock.touched_lower_limit),
|
||||||
);
|
);
|
||||||
factors.insert("hit_upper_limit".into(), Dynamic::from(stock.touched_upper_limit));
|
factors.insert(
|
||||||
factors.insert("hit_lower_limit".into(), Dynamic::from(stock.touched_lower_limit));
|
"hit_upper_limit".into(),
|
||||||
|
Dynamic::from(stock.touched_upper_limit),
|
||||||
|
);
|
||||||
|
factors.insert(
|
||||||
|
"hit_lower_limit".into(),
|
||||||
|
Dynamic::from(stock.touched_lower_limit),
|
||||||
|
);
|
||||||
factors.insert("listed_days".into(), Dynamic::from(stock.listed_days));
|
factors.insert("listed_days".into(), Dynamic::from(stock.listed_days));
|
||||||
factors.insert("at_upper_limit".into(), Dynamic::from(at_upper_limit));
|
factors.insert("at_upper_limit".into(), Dynamic::from(at_upper_limit));
|
||||||
factors.insert("at_lower_limit".into(), Dynamic::from(at_lower_limit));
|
factors.insert("at_lower_limit".into(), Dynamic::from(at_lower_limit));
|
||||||
@@ -1051,10 +1183,22 @@ impl PlatformExprStrategy {
|
|||||||
factors.insert("ma10".into(), Dynamic::from(stock.stock_ma10));
|
factors.insert("ma10".into(), Dynamic::from(stock.stock_ma10));
|
||||||
factors.insert("ma20".into(), Dynamic::from(stock.stock_ma20));
|
factors.insert("ma20".into(), Dynamic::from(stock.stock_ma20));
|
||||||
factors.insert("ma30".into(), Dynamic::from(stock.stock_ma30));
|
factors.insert("ma30".into(), Dynamic::from(stock.stock_ma30));
|
||||||
factors.insert("stock_volume_ma5".into(), Dynamic::from(stock.stock_volume_ma5));
|
factors.insert(
|
||||||
factors.insert("stock_volume_ma10".into(), Dynamic::from(stock.stock_volume_ma10));
|
"stock_volume_ma5".into(),
|
||||||
factors.insert("stock_volume_ma20".into(), Dynamic::from(stock.stock_volume_ma20));
|
Dynamic::from(stock.stock_volume_ma5),
|
||||||
factors.insert("stock_volume_ma60".into(), Dynamic::from(stock.stock_volume_ma60));
|
);
|
||||||
|
factors.insert(
|
||||||
|
"stock_volume_ma10".into(),
|
||||||
|
Dynamic::from(stock.stock_volume_ma10),
|
||||||
|
);
|
||||||
|
factors.insert(
|
||||||
|
"stock_volume_ma20".into(),
|
||||||
|
Dynamic::from(stock.stock_volume_ma20),
|
||||||
|
);
|
||||||
|
factors.insert(
|
||||||
|
"stock_volume_ma60".into(),
|
||||||
|
Dynamic::from(stock.stock_volume_ma60),
|
||||||
|
);
|
||||||
factors.insert("volume_ma5".into(), Dynamic::from(stock.stock_volume_ma5));
|
factors.insert("volume_ma5".into(), Dynamic::from(stock.stock_volume_ma5));
|
||||||
factors.insert("volume_ma10".into(), Dynamic::from(stock.stock_volume_ma10));
|
factors.insert("volume_ma10".into(), Dynamic::from(stock.stock_volume_ma10));
|
||||||
factors.insert("volume_ma20".into(), Dynamic::from(stock.stock_volume_ma20));
|
factors.insert("volume_ma20".into(), Dynamic::from(stock.stock_volume_ma20));
|
||||||
@@ -1120,7 +1264,8 @@ impl PlatformExprStrategy {
|
|||||||
item.extra_factors
|
item.extra_factors
|
||||||
.keys()
|
.keys()
|
||||||
.filter(|key| {
|
.filter(|key| {
|
||||||
Self::is_expression_identifier(key) && !reserved_names.contains(key.as_str())
|
Self::is_expression_identifier(key)
|
||||||
|
&& !reserved_names.contains(key.as_str())
|
||||||
})
|
})
|
||||||
.map(|key| format!("let {key} = factors[\"{key}\"];"))
|
.map(|key| format!("let {key} = factors[\"{key}\"];"))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
@@ -1138,7 +1283,9 @@ impl PlatformExprStrategy {
|
|||||||
let script = script_parts.join("\n");
|
let script = script_parts.join("\n");
|
||||||
self.engine
|
self.engine
|
||||||
.eval_with_scope::<Dynamic>(&mut scope, &script)
|
.eval_with_scope::<Dynamic>(&mut scope, &script)
|
||||||
.map_err(|error| BacktestError::Execution(format!("platform expr eval failed: {}", error)))
|
.map_err(|error| {
|
||||||
|
BacktestError::Execution(format!("platform expr eval failed: {}", error))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_expr(expr: &str) -> String {
|
fn normalize_expr(expr: &str) -> String {
|
||||||
@@ -1265,23 +1412,32 @@ impl PlatformExprStrategy {
|
|||||||
let value = match field {
|
let value = match field {
|
||||||
"benchmark_open" => ctx.data.benchmark_open_moving_average(day.date, lookback),
|
"benchmark_open" => ctx.data.benchmark_open_moving_average(day.date, lookback),
|
||||||
"benchmark_close" => ctx.data.benchmark_moving_average(day.date, lookback),
|
"benchmark_close" => ctx.data.benchmark_moving_average(day.date, lookback),
|
||||||
"signal_open" => ctx
|
"signal_open" => {
|
||||||
.data
|
ctx.data
|
||||||
.market_open_moving_average(day.date, &self.config.signal_symbol, lookback),
|
.market_open_moving_average(day.date, &self.config.signal_symbol, lookback)
|
||||||
"signal_close" => ctx
|
}
|
||||||
.data
|
"signal_close" => ctx.data.market_decision_close_moving_average(
|
||||||
.market_decision_close_moving_average(day.date, &self.config.signal_symbol, lookback),
|
day.date,
|
||||||
"signal_volume" => ctx
|
&self.config.signal_symbol,
|
||||||
.data
|
lookback,
|
||||||
.market_decision_volume_moving_average(day.date, &self.config.signal_symbol, lookback),
|
),
|
||||||
|
"signal_volume" => ctx.data.market_decision_volume_moving_average(
|
||||||
|
day.date,
|
||||||
|
&self.config.signal_symbol,
|
||||||
|
lookback,
|
||||||
|
),
|
||||||
other => {
|
other => {
|
||||||
let stock = stock.ok_or_else(|| {
|
let stock = stock.ok_or_else(|| {
|
||||||
BacktestError::Execution(format!(
|
BacktestError::Execution(format!(
|
||||||
"rolling_mean(\"{other}\", {lookback}) requires stock context"
|
"rolling_mean(\"{other}\", {lookback}) requires stock context"
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
ctx.data
|
ctx.data.market_decision_numeric_moving_average(
|
||||||
.market_decision_numeric_moving_average(day.date, &stock.symbol, other, lookback)
|
day.date,
|
||||||
|
&stock.symbol,
|
||||||
|
other,
|
||||||
|
lookback,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
value.ok_or_else(|| {
|
value.ok_or_else(|| {
|
||||||
@@ -1375,9 +1531,7 @@ impl PlatformExprStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn quote_rhai_string(value: &str) -> String {
|
fn quote_rhai_string(value: &str) -> String {
|
||||||
let escaped = value
|
let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
|
||||||
.replace('\\', "\\\\")
|
|
||||||
.replace('"', "\\\"");
|
|
||||||
format!("\"{escaped}\"")
|
format!("\"{escaped}\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1486,7 +1640,10 @@ impl PlatformExprStrategy {
|
|||||||
let condition = Self::rewrite_ternary(expr[..question_idx].trim());
|
let condition = Self::rewrite_ternary(expr[..question_idx].trim());
|
||||||
let when_true = Self::rewrite_ternary(expr[question_idx + 1..colon_idx].trim());
|
let when_true = Self::rewrite_ternary(expr[question_idx + 1..colon_idx].trim());
|
||||||
let when_false = Self::rewrite_ternary(expr[colon_idx + 1..].trim());
|
let when_false = Self::rewrite_ternary(expr[colon_idx + 1..].trim());
|
||||||
format!("if {} {{ {} }} else {{ {} }}", condition, when_true, when_false)
|
format!(
|
||||||
|
"if {} {{ {} }} else {{ {} }}",
|
||||||
|
condition, when_true, when_false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_top_level_ternary(expr: &str) -> Option<(usize, usize)> {
|
fn find_top_level_ternary(expr: &str) -> Option<(usize, usize)> {
|
||||||
@@ -1861,7 +2018,9 @@ impl PlatformExprStrategy {
|
|||||||
let Some(position) = ctx.portfolio.position(symbol) else {
|
let Some(position) = ctx.portfolio.position(symbol) else {
|
||||||
return Ok((false, false));
|
return Ok((false, false));
|
||||||
};
|
};
|
||||||
if self.config.stop_loss_expr.trim().is_empty() && self.config.take_profit_expr.trim().is_empty() {
|
if self.config.stop_loss_expr.trim().is_empty()
|
||||||
|
&& self.config.take_profit_expr.trim().is_empty()
|
||||||
|
{
|
||||||
return Ok((false, false));
|
return Ok((false, false));
|
||||||
}
|
}
|
||||||
if position.quantity == 0 || position.average_cost <= 0.0 {
|
if position.quantity == 0 || position.average_cost <= 0.0 {
|
||||||
@@ -1884,7 +2043,13 @@ impl PlatformExprStrategy {
|
|||||||
let stop_hit = if self.config.stop_loss_expr.trim().is_empty() {
|
let stop_hit = if self.config.stop_loss_expr.trim().is_empty() {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
let stop_result = self.eval_dynamic(ctx, &self.config.stop_loss_expr, day, Some(&stock), Some(&position_state))?;
|
let stop_result = self.eval_dynamic(
|
||||||
|
ctx,
|
||||||
|
&self.config.stop_loss_expr,
|
||||||
|
day,
|
||||||
|
Some(&stock),
|
||||||
|
Some(&position_state),
|
||||||
|
)?;
|
||||||
if let Some(boolean) = stop_result.clone().try_cast::<bool>() {
|
if let Some(boolean) = stop_result.clone().try_cast::<bool>() {
|
||||||
boolean
|
boolean
|
||||||
} else if let Some(multiplier) = stop_result.clone().try_cast::<f64>() {
|
} else if let Some(multiplier) = stop_result.clone().try_cast::<f64>() {
|
||||||
@@ -1898,14 +2063,24 @@ impl PlatformExprStrategy {
|
|||||||
let profit_hit = if self.config.take_profit_expr.trim().is_empty() {
|
let profit_hit = if self.config.take_profit_expr.trim().is_empty() {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
let take_result = self.eval_dynamic(ctx, &self.config.take_profit_expr, day, Some(&stock), Some(&position_state))?;
|
let take_result = self.eval_dynamic(
|
||||||
|
ctx,
|
||||||
|
&self.config.take_profit_expr,
|
||||||
|
day,
|
||||||
|
Some(&stock),
|
||||||
|
Some(&position_state),
|
||||||
|
)?;
|
||||||
if let Some(boolean) = take_result.clone().try_cast::<bool>() {
|
if let Some(boolean) = take_result.clone().try_cast::<bool>() {
|
||||||
boolean
|
boolean
|
||||||
} else if let Some(multiplier) = take_result.clone().try_cast::<f64>() {
|
} else if let Some(multiplier) = take_result.clone().try_cast::<f64>() {
|
||||||
!ctx.data.require_market(date, symbol)?.is_at_upper_limit_price(current_price)
|
!ctx.data
|
||||||
|
.require_market(date, symbol)?
|
||||||
|
.is_at_upper_limit_price(current_price)
|
||||||
&& current_price / position.average_cost > multiplier
|
&& current_price / position.average_cost > multiplier
|
||||||
} else if let Some(multiplier) = take_result.try_cast::<i64>() {
|
} else if let Some(multiplier) = take_result.try_cast::<i64>() {
|
||||||
!ctx.data.require_market(date, symbol)?.is_at_upper_limit_price(current_price)
|
!ctx.data
|
||||||
|
.require_market(date, symbol)?
|
||||||
|
.is_at_upper_limit_price(current_price)
|
||||||
&& current_price / position.average_cost > multiplier as f64
|
&& current_price / position.average_cost > multiplier as f64
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@@ -1998,7 +2173,10 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let stock = self.stock_state(ctx, date, symbol)?;
|
let stock = self.stock_state(ctx, date, symbol)?;
|
||||||
if self.buy_rejection_reason(ctx, date, symbol, &stock)?.is_some() {
|
if self
|
||||||
|
.buy_rejection_reason(ctx, date, symbol, &stock)?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !self.stock_passes_expr(ctx, &day, &stock)? {
|
if !self.stock_passes_expr(ctx, &day, &stock)? {
|
||||||
@@ -2067,7 +2245,10 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let stock = self.stock_state(ctx, date, symbol)?;
|
let stock = self.stock_state(ctx, date, symbol)?;
|
||||||
if self.buy_rejection_reason(ctx, date, symbol, &stock)?.is_some() {
|
if self
|
||||||
|
.buy_rejection_reason(ctx, date, symbol, &stock)?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !self.stock_passes_expr(ctx, &day, &stock)? {
|
if !self.stock_passes_expr(ctx, &day, &stock)? {
|
||||||
|
|||||||
@@ -175,7 +175,11 @@ impl CnSmallCapRotationStrategy {
|
|||||||
let (sum, count) = window.fold((0.0, 0usize), |(sum, count), value| {
|
let (sum, count) = window.fold((0.0, 0usize), |(sum, count), value| {
|
||||||
(sum + value, count + 1)
|
(sum + value, count + 1)
|
||||||
});
|
});
|
||||||
if count == 0 { 0.0 } else { sum / count as f64 }
|
if count == 0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
sum / count as f64
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gross_exposure(&self, closes: &[f64]) -> f64 {
|
fn gross_exposure(&self, closes: &[f64]) -> f64 {
|
||||||
@@ -325,7 +329,7 @@ impl Strategy for CnSmallCapRotationStrategy {
|
|||||||
order_intents: Vec::new(),
|
order_intents: Vec::new(),
|
||||||
notes: vec![format!("warmup: {}", message)],
|
notes: vec![format!("warmup: {}", message)],
|
||||||
diagnostics: vec![
|
diagnostics: vec![
|
||||||
"insufficient history; skip trading on warmup dates".to_string(),
|
"insufficient history; skip trading on warmup dates".to_string()
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -591,12 +595,42 @@ impl JqMicroCapStrategy {
|
|||||||
|
|
||||||
fn sell_cost(&self, date: NaiveDate, gross_amount: f64) -> f64 {
|
fn sell_cost(&self, date: NaiveDate, gross_amount: f64) -> f64 {
|
||||||
let model = ChinaAShareCostModel::default();
|
let model = ChinaAShareCostModel::default();
|
||||||
model.commission_for(gross_amount) + model.stamp_tax_for(date, OrderSide::Sell, gross_amount)
|
model.commission_for(gross_amount)
|
||||||
|
+ model.stamp_tax_for(date, OrderSide::Sell, gross_amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn round_lot_quantity(&self, quantity: u32, round_lot: u32) -> u32 {
|
fn round_lot_quantity(
|
||||||
let lot = round_lot.max(1);
|
&self,
|
||||||
(quantity / lot) * lot
|
quantity: u32,
|
||||||
|
minimum_order_quantity: u32,
|
||||||
|
order_step_size: u32,
|
||||||
|
) -> u32 {
|
||||||
|
let step = order_step_size.max(1);
|
||||||
|
let normalized = (quantity / step) * step;
|
||||||
|
if normalized < minimum_order_quantity.max(1) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_order_quantity(
|
||||||
|
&self,
|
||||||
|
quantity: u32,
|
||||||
|
minimum_order_quantity: u32,
|
||||||
|
order_step_size: u32,
|
||||||
|
) -> u32 {
|
||||||
|
let minimum = minimum_order_quantity.max(1);
|
||||||
|
if quantity <= minimum {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
let next = quantity.saturating_sub(order_step_size.max(1));
|
||||||
|
if next < minimum {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
next
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn intraday_execution_start_time(&self) -> NaiveTime {
|
fn intraday_execution_start_time(&self) -> NaiveTime {
|
||||||
@@ -611,17 +645,33 @@ impl JqMicroCapStrategy {
|
|||||||
.max(1)
|
.max(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn projected_minimum_order_quantity(&self, ctx: &StrategyContext<'_>, symbol: &str) -> u32 {
|
||||||
|
ctx.data
|
||||||
|
.instrument(symbol)
|
||||||
|
.map(|instrument| instrument.minimum_order_quantity())
|
||||||
|
.unwrap_or(100)
|
||||||
|
.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn projected_order_step_size(&self, ctx: &StrategyContext<'_>, symbol: &str) -> u32 {
|
||||||
|
ctx.data
|
||||||
|
.instrument(symbol)
|
||||||
|
.map(|instrument| instrument.order_step_size())
|
||||||
|
.unwrap_or(100)
|
||||||
|
.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
fn projected_buy_quantity(&self, cash: f64, sizing_price: f64, execution_price: f64) -> u32 {
|
fn projected_buy_quantity(&self, cash: f64, sizing_price: f64, execution_price: f64) -> u32 {
|
||||||
if cash <= 0.0 || sizing_price <= 0.0 || execution_price <= 0.0 {
|
if cash <= 0.0 || sizing_price <= 0.0 || execution_price <= 0.0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let mut quantity = self.round_lot_quantity((cash / sizing_price).floor() as u32, 100);
|
let mut quantity = self.round_lot_quantity((cash / sizing_price).floor() as u32, 100, 100);
|
||||||
while quantity > 0 {
|
while quantity > 0 {
|
||||||
let gross_amount = execution_price * quantity as f64;
|
let gross_amount = execution_price * quantity as f64;
|
||||||
if gross_amount + self.buy_commission(gross_amount) <= cash + 1e-6 {
|
if gross_amount + self.buy_commission(gross_amount) <= cash + 1e-6 {
|
||||||
return quantity;
|
return quantity;
|
||||||
}
|
}
|
||||||
quantity = quantity.saturating_sub(100);
|
quantity = self.decrement_order_quantity(quantity, 100, 100);
|
||||||
}
|
}
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
@@ -649,6 +699,8 @@ impl JqMicroCapStrategy {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let round_lot = self.projected_round_lot(ctx, symbol);
|
let round_lot = self.projected_round_lot(ctx, symbol);
|
||||||
|
let minimum_order_quantity = self.projected_minimum_order_quantity(ctx, symbol);
|
||||||
|
let order_step_size = self.projected_order_step_size(ctx, symbol);
|
||||||
let market = match ctx.data.market(date, symbol) {
|
let market = match ctx.data.market(date, symbol) {
|
||||||
Some(market) => market,
|
Some(market) => market,
|
||||||
None => return 0,
|
None => return 0,
|
||||||
@@ -659,7 +711,8 @@ impl JqMicroCapStrategy {
|
|||||||
}
|
}
|
||||||
let snapshot_requested_qty = self.round_lot_quantity(
|
let snapshot_requested_qty = self.round_lot_quantity(
|
||||||
((projected.cash().min(order_value)) / sizing_price).floor() as u32,
|
((projected.cash().min(order_value)) / sizing_price).floor() as u32,
|
||||||
round_lot,
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
);
|
);
|
||||||
let projected_execution_price = self.projected_execution_price(market, OrderSide::Buy);
|
let projected_execution_price = self.projected_execution_price(market, OrderSide::Buy);
|
||||||
let projected_fill = self.projected_select_execution_fill(
|
let projected_fill = self.projected_select_execution_fill(
|
||||||
@@ -669,6 +722,9 @@ impl JqMicroCapStrategy {
|
|||||||
OrderSide::Buy,
|
OrderSide::Buy,
|
||||||
u32::MAX,
|
u32::MAX,
|
||||||
round_lot,
|
round_lot,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
false,
|
||||||
Some(projected.cash()),
|
Some(projected.cash()),
|
||||||
Some(order_value + 400.0),
|
Some(order_value + 400.0),
|
||||||
execution_state,
|
execution_state,
|
||||||
@@ -676,12 +732,11 @@ impl JqMicroCapStrategy {
|
|||||||
let mut quantity = snapshot_requested_qty;
|
let mut quantity = snapshot_requested_qty;
|
||||||
while quantity > 0 {
|
while quantity > 0 {
|
||||||
let gross_amount = projected_execution_price * quantity as f64;
|
let gross_amount = projected_execution_price * quantity as f64;
|
||||||
if gross_amount <= order_value + 400.0
|
if gross_amount <= order_value + 400.0 && gross_amount <= projected.cash() + 1e-6 {
|
||||||
&& gross_amount <= projected.cash() + 1e-6
|
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
quantity = quantity.saturating_sub(round_lot);
|
quantity =
|
||||||
|
self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size);
|
||||||
}
|
}
|
||||||
if quantity == 0 {
|
if quantity == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -695,7 +750,8 @@ impl JqMicroCapStrategy {
|
|||||||
if gross_amount <= projected.cash() + 1e-6 {
|
if gross_amount <= projected.cash() + 1e-6 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
quantity = quantity.saturating_sub(round_lot);
|
quantity =
|
||||||
|
self.decrement_order_quantity(quantity, minimum_order_quantity, order_step_size);
|
||||||
}
|
}
|
||||||
if quantity == 0 {
|
if quantity == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -742,6 +798,8 @@ impl JqMicroCapStrategy {
|
|||||||
}
|
}
|
||||||
let market = ctx.data.market(date, symbol)?;
|
let market = ctx.data.market(date, symbol)?;
|
||||||
let round_lot = self.projected_round_lot(ctx, symbol);
|
let round_lot = self.projected_round_lot(ctx, symbol);
|
||||||
|
let minimum_order_quantity = self.projected_minimum_order_quantity(ctx, symbol);
|
||||||
|
let order_step_size = self.projected_order_step_size(ctx, symbol);
|
||||||
let fill = self
|
let fill = self
|
||||||
.projected_select_execution_fill(
|
.projected_select_execution_fill(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -750,6 +808,9 @@ impl JqMicroCapStrategy {
|
|||||||
OrderSide::Sell,
|
OrderSide::Sell,
|
||||||
quantity,
|
quantity,
|
||||||
round_lot,
|
round_lot,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
true,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
execution_state,
|
execution_state,
|
||||||
@@ -788,7 +849,10 @@ impl JqMicroCapStrategy {
|
|||||||
symbol: &str,
|
symbol: &str,
|
||||||
side: OrderSide,
|
side: OrderSide,
|
||||||
requested_qty: u32,
|
requested_qty: u32,
|
||||||
round_lot: u32,
|
_round_lot: u32,
|
||||||
|
minimum_order_quantity: u32,
|
||||||
|
order_step_size: u32,
|
||||||
|
allow_odd_lot_sell: bool,
|
||||||
execution_state: &ProjectedExecutionState,
|
execution_state: &ProjectedExecutionState,
|
||||||
) -> Option<u32> {
|
) -> Option<u32> {
|
||||||
if requested_qty == 0 {
|
if requested_qty == 0 {
|
||||||
@@ -799,7 +863,6 @@ impl JqMicroCapStrategy {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lot = round_lot.max(1);
|
|
||||||
let mut max_fill = requested_qty;
|
let mut max_fill = requested_qty;
|
||||||
let top_level_liquidity = match side {
|
let top_level_liquidity = match side {
|
||||||
OrderSide::Buy => snapshot.liquidity_for_buy(),
|
OrderSide::Buy => snapshot.liquidity_for_buy(),
|
||||||
@@ -809,7 +872,12 @@ impl JqMicroCapStrategy {
|
|||||||
if top_level_liquidity == 0 {
|
if top_level_liquidity == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
max_fill = max_fill.min(self.round_lot_quantity(top_level_liquidity, lot));
|
let liquidity_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
|
||||||
|
top_level_liquidity
|
||||||
|
} else {
|
||||||
|
self.round_lot_quantity(top_level_liquidity, minimum_order_quantity, order_step_size)
|
||||||
|
};
|
||||||
|
max_fill = max_fill.min(liquidity_limited);
|
||||||
|
|
||||||
let consumed_turnover = *execution_state.intraday_turnover.get(symbol).unwrap_or(&0);
|
let consumed_turnover = *execution_state.intraday_turnover.get(symbol).unwrap_or(&0);
|
||||||
let raw_limit =
|
let raw_limit =
|
||||||
@@ -817,7 +885,11 @@ impl JqMicroCapStrategy {
|
|||||||
if raw_limit <= 0 {
|
if raw_limit <= 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let volume_limited = self.round_lot_quantity(raw_limit as u32, lot);
|
let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
|
||||||
|
raw_limit as u32
|
||||||
|
} else {
|
||||||
|
self.round_lot_quantity(raw_limit as u32, minimum_order_quantity, order_step_size)
|
||||||
|
};
|
||||||
if volume_limited == 0 {
|
if volume_limited == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -842,6 +914,9 @@ impl JqMicroCapStrategy {
|
|||||||
side: OrderSide,
|
side: OrderSide,
|
||||||
requested_qty: u32,
|
requested_qty: u32,
|
||||||
round_lot: u32,
|
round_lot: u32,
|
||||||
|
minimum_order_quantity: u32,
|
||||||
|
order_step_size: u32,
|
||||||
|
allow_odd_lot_sell: bool,
|
||||||
cash_limit: Option<f64>,
|
cash_limit: Option<f64>,
|
||||||
gross_limit: Option<f64>,
|
gross_limit: Option<f64>,
|
||||||
execution_state: &ProjectedExecutionState,
|
execution_state: &ProjectedExecutionState,
|
||||||
@@ -856,26 +931,39 @@ impl JqMicroCapStrategy {
|
|||||||
let quantity = match side {
|
let quantity = match side {
|
||||||
OrderSide::Buy => {
|
OrderSide::Buy => {
|
||||||
let cash = cash_limit.unwrap_or(f64::INFINITY);
|
let cash = cash_limit.unwrap_or(f64::INFINITY);
|
||||||
let mut take_qty = self.round_lot_quantity(requested_qty, round_lot.max(1));
|
let mut take_qty = self.round_lot_quantity(
|
||||||
|
requested_qty,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
);
|
||||||
while take_qty > 0 {
|
while take_qty > 0 {
|
||||||
let candidate_gross = execution_price * take_qty as f64;
|
let candidate_gross = execution_price * take_qty as f64;
|
||||||
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
|
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
|
||||||
take_qty = take_qty.saturating_sub(round_lot.max(1));
|
take_qty = self.decrement_order_quantity(
|
||||||
|
take_qty,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let candidate_cash = candidate_gross + self.buy_commission(candidate_gross);
|
let candidate_cash =
|
||||||
|
candidate_gross + self.buy_commission(candidate_gross);
|
||||||
if candidate_cash <= cash + 1e-6 {
|
if candidate_cash <= cash + 1e-6 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
take_qty = take_qty.saturating_sub(round_lot.max(1));
|
take_qty = self.decrement_order_quantity(
|
||||||
|
take_qty,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
take_qty
|
take_qty
|
||||||
}
|
}
|
||||||
OrderSide::Sell => requested_qty,
|
OrderSide::Sell => requested_qty,
|
||||||
};
|
};
|
||||||
if quantity > 0 {
|
if quantity > 0 {
|
||||||
let next_cursor = date.and_time(self.intraday_execution_start_time())
|
let next_cursor =
|
||||||
+ Duration::seconds(1);
|
date.and_time(self.intraday_execution_start_time()) + Duration::seconds(1);
|
||||||
return Some(ProjectedExecutionFill {
|
return Some(ProjectedExecutionFill {
|
||||||
price: execution_price,
|
price: execution_price,
|
||||||
quantity,
|
quantity,
|
||||||
@@ -885,7 +973,6 @@ impl JqMicroCapStrategy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let lot = round_lot.max(1);
|
|
||||||
let start_cursor = self.projected_execution_start_cursor(date, symbol, execution_state);
|
let start_cursor = self.projected_execution_start_cursor(date, symbol, execution_state);
|
||||||
let quotes = ctx.data.execution_quotes_on(date, symbol);
|
let quotes = ctx.data.execution_quotes_on(date, symbol);
|
||||||
let mut filled_qty = 0_u32;
|
let mut filled_qty = 0_u32;
|
||||||
@@ -921,7 +1008,7 @@ impl JqMicroCapStrategy {
|
|||||||
OrderSide::Buy => quote.ask1_volume,
|
OrderSide::Buy => quote.ask1_volume,
|
||||||
OrderSide::Sell => quote.bid1_volume,
|
OrderSide::Sell => quote.bid1_volume,
|
||||||
}
|
}
|
||||||
.saturating_mul(lot as u64)
|
.saturating_mul(round_lot.max(1) as u64)
|
||||||
.min(u32::MAX as u64) as u32;
|
.min(u32::MAX as u64) as u32;
|
||||||
if available_qty == 0 {
|
if available_qty == 0 {
|
||||||
continue;
|
continue;
|
||||||
@@ -931,7 +1018,11 @@ impl JqMicroCapStrategy {
|
|||||||
if remaining_qty == 0 {
|
if remaining_qty == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let mut take_qty = self.round_lot_quantity(remaining_qty.min(available_qty), lot);
|
let mut take_qty = remaining_qty.min(available_qty);
|
||||||
|
if !(side == OrderSide::Sell && allow_odd_lot_sell && take_qty == remaining_qty) {
|
||||||
|
take_qty =
|
||||||
|
self.round_lot_quantity(take_qty, minimum_order_quantity, order_step_size);
|
||||||
|
}
|
||||||
if take_qty == 0 {
|
if take_qty == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -940,13 +1031,21 @@ impl JqMicroCapStrategy {
|
|||||||
while take_qty > 0 {
|
while take_qty > 0 {
|
||||||
let candidate_gross = gross_amount + quote_price * take_qty as f64;
|
let candidate_gross = gross_amount + quote_price * take_qty as f64;
|
||||||
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
|
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
|
||||||
take_qty = take_qty.saturating_sub(lot);
|
take_qty = self.decrement_order_quantity(
|
||||||
|
take_qty,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if candidate_gross + self.buy_commission(candidate_gross) <= cash + 1e-6 {
|
if candidate_gross + self.buy_commission(candidate_gross) <= cash + 1e-6 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
take_qty = take_qty.saturating_sub(lot);
|
take_qty = self.decrement_order_quantity(
|
||||||
|
take_qty,
|
||||||
|
minimum_order_quantity,
|
||||||
|
order_step_size,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if take_qty == 0 {
|
if take_qty == 0 {
|
||||||
break;
|
break;
|
||||||
@@ -1436,7 +1535,7 @@ impl Strategy for JqMicroCapStrategy {
|
|||||||
order_intents: Vec::new(),
|
order_intents: Vec::new(),
|
||||||
notes: vec![format!("warmup: {}", message)],
|
notes: vec![format!("warmup: {}", message)],
|
||||||
diagnostics: vec![
|
diagnostics: vec![
|
||||||
"insufficient history; skip trading on warmup dates".to_string(),
|
"insufficient history; skip trading on warmup dates".to_string()
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user