Refine jq microcap execution alignment
This commit is contained in:
@@ -594,12 +594,24 @@ where
|
|||||||
let price = self.sizing_price(snapshot);
|
let price = self.sizing_price(snapshot);
|
||||||
let snapshot_requested_qty =
|
let snapshot_requested_qty =
|
||||||
self.round_buy_quantity(((value.abs()) / price).floor() as u32, round_lot);
|
self.round_buy_quantity(((value.abs()) / price).floor() as u32, round_lot);
|
||||||
self.process_buy(
|
let requested_qty = self.maybe_expand_periodic_value_buy_quantity(
|
||||||
date,
|
date,
|
||||||
portfolio,
|
portfolio,
|
||||||
data,
|
data,
|
||||||
symbol,
|
symbol,
|
||||||
snapshot_requested_qty,
|
snapshot_requested_qty,
|
||||||
|
round_lot,
|
||||||
|
value.abs(),
|
||||||
|
reason,
|
||||||
|
execution_cursors,
|
||||||
|
*global_execution_cursor,
|
||||||
|
);
|
||||||
|
self.process_buy(
|
||||||
|
date,
|
||||||
|
portfolio,
|
||||||
|
data,
|
||||||
|
symbol,
|
||||||
|
requested_qty,
|
||||||
reason,
|
reason,
|
||||||
intraday_turnover,
|
intraday_turnover,
|
||||||
execution_cursors,
|
execution_cursors,
|
||||||
@@ -654,83 +666,50 @@ where
|
|||||||
)
|
)
|
||||||
.ok()?;
|
.ok()?;
|
||||||
let max_requested_qty = market_limited_qty;
|
let max_requested_qty = market_limited_qty;
|
||||||
let start_cursor = execution_cursors
|
let start_cursor = self
|
||||||
.get(symbol)
|
.intraday_execution_start_time
|
||||||
.copied()
|
.map(|start_time| date.and_time(start_time));
|
||||||
.into_iter()
|
|
||||||
.chain(global_execution_cursor)
|
|
||||||
.chain(
|
|
||||||
self.intraday_execution_start_time
|
|
||||||
.map(|start_time| date.and_time(start_time)),
|
|
||||||
)
|
|
||||||
.max();
|
|
||||||
let quotes = data.execution_quotes_on(date, symbol);
|
let quotes = data.execution_quotes_on(date, symbol);
|
||||||
let estimated = self.select_buy_sizing_fill(
|
if let Some(estimated) = self.select_buy_sizing_fill(
|
||||||
quotes,
|
quotes,
|
||||||
start_cursor,
|
start_cursor,
|
||||||
max_requested_qty,
|
max_requested_qty,
|
||||||
round_lot,
|
round_lot,
|
||||||
Some(portfolio.cash()),
|
Some(portfolio.cash()),
|
||||||
Some(value_budget),
|
Some(value_budget),
|
||||||
)?;
|
) {
|
||||||
Some(estimated.quantity)
|
return Some(estimated.quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
let execution_price = self.snapshot_execution_price(snapshot, OrderSide::Buy);
|
||||||
|
let fallback_qty = self.affordable_buy_quantity(
|
||||||
|
portfolio.cash(),
|
||||||
|
Some(value_budget),
|
||||||
|
execution_price,
|
||||||
|
max_requested_qty,
|
||||||
|
round_lot,
|
||||||
|
);
|
||||||
|
if fallback_qty > 0 {
|
||||||
|
Some(fallback_qty)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maybe_expand_periodic_value_buy_quantity(
|
fn maybe_expand_periodic_value_buy_quantity(
|
||||||
&self,
|
&self,
|
||||||
date: NaiveDate,
|
_date: NaiveDate,
|
||||||
portfolio: &PortfolioState,
|
_portfolio: &PortfolioState,
|
||||||
data: &DataSet,
|
_data: &DataSet,
|
||||||
symbol: &str,
|
_symbol: &str,
|
||||||
requested_qty: u32,
|
requested_qty: u32,
|
||||||
round_lot: u32,
|
_round_lot: u32,
|
||||||
value_budget: f64,
|
_value_budget: f64,
|
||||||
reason: &str,
|
_reason: &str,
|
||||||
execution_cursors: &BTreeMap<String, NaiveDateTime>,
|
_execution_cursors: &BTreeMap<String, NaiveDateTime>,
|
||||||
global_execution_cursor: Option<NaiveDateTime>,
|
_global_execution_cursor: Option<NaiveDateTime>,
|
||||||
) -> u32 {
|
) -> u32 {
|
||||||
const PERIODIC_BUY_OVERSHOOT_TOLERANCE: f64 = 400.0;
|
requested_qty
|
||||||
|
|
||||||
if requested_qty == 0 || reason != "periodic_rebalance_buy" {
|
|
||||||
return requested_qty;
|
|
||||||
}
|
|
||||||
|
|
||||||
let candidate_qty = requested_qty.saturating_add(round_lot.max(1));
|
|
||||||
let start_cursor = execution_cursors
|
|
||||||
.get(symbol)
|
|
||||||
.copied()
|
|
||||||
.into_iter()
|
|
||||||
.chain(global_execution_cursor)
|
|
||||||
.chain(
|
|
||||||
self.intraday_execution_start_time
|
|
||||||
.map(|start_time| date.and_time(start_time)),
|
|
||||||
)
|
|
||||||
.max();
|
|
||||||
let quotes = data.execution_quotes_on(date, symbol);
|
|
||||||
let Some(fill) = self.select_execution_fill(
|
|
||||||
quotes,
|
|
||||||
OrderSide::Buy,
|
|
||||||
start_cursor,
|
|
||||||
candidate_qty,
|
|
||||||
round_lot,
|
|
||||||
Some(portfolio.cash()),
|
|
||||||
None,
|
|
||||||
) else {
|
|
||||||
return requested_qty;
|
|
||||||
};
|
|
||||||
if fill.quantity < candidate_qty {
|
|
||||||
return requested_qty;
|
|
||||||
}
|
|
||||||
let candidate_gross = fill.price * fill.quantity as f64;
|
|
||||||
let candidate_cost = self.cost_model.calculate(OrderSide::Buy, candidate_gross);
|
|
||||||
let candidate_cash_out = candidate_gross + candidate_cost.total();
|
|
||||||
if candidate_cash_out <= value_budget + PERIODIC_BUY_OVERSHOOT_TOLERANCE
|
|
||||||
&& candidate_cash_out <= portfolio.cash() + 1e-6
|
|
||||||
{
|
|
||||||
candidate_qty
|
|
||||||
} else {
|
|
||||||
requested_qty
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_buy_sizing_fill(
|
fn select_buy_sizing_fill(
|
||||||
@@ -925,7 +904,7 @@ where
|
|||||||
execution_cursors,
|
execution_cursors,
|
||||||
None,
|
None,
|
||||||
Some(portfolio.cash()),
|
Some(portfolio.cash()),
|
||||||
None,
|
value_budget.map(|budget| budget + 400.0),
|
||||||
);
|
);
|
||||||
let (filled_qty, execution_price) = if let Some(fill) = fill {
|
let (filled_qty, execution_price) = if let Some(fill) = fill {
|
||||||
execution_cursors.insert(symbol.to_string(), fill.next_cursor);
|
execution_cursors.insert(symbol.to_string(), fill.next_cursor);
|
||||||
@@ -937,7 +916,7 @@ where
|
|||||||
let execution_price = self.snapshot_execution_price(snapshot, OrderSide::Buy);
|
let execution_price = self.snapshot_execution_price(snapshot, OrderSide::Buy);
|
||||||
let filled_qty = self.affordable_buy_quantity(
|
let filled_qty = self.affordable_buy_quantity(
|
||||||
portfolio.cash(),
|
portfolio.cash(),
|
||||||
value_budget,
|
value_budget.map(|budget| budget + 400.0),
|
||||||
execution_price,
|
execution_price,
|
||||||
constrained_qty,
|
constrained_qty,
|
||||||
self.round_lot(data, symbol),
|
self.round_lot(data, symbol),
|
||||||
@@ -1148,6 +1127,23 @@ where
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let start_cursor = self
|
||||||
|
.intraday_execution_start_time
|
||||||
|
.map(|start_time| date.and_time(start_time));
|
||||||
|
let quotes = data.execution_quotes_on(date, symbol);
|
||||||
|
|
||||||
|
if let Some(fill) = self.select_execution_fill(
|
||||||
|
quotes,
|
||||||
|
side,
|
||||||
|
start_cursor,
|
||||||
|
requested_qty,
|
||||||
|
round_lot,
|
||||||
|
cash_limit,
|
||||||
|
gross_limit,
|
||||||
|
) {
|
||||||
|
return Some(fill);
|
||||||
|
}
|
||||||
|
|
||||||
if self.intraday_execution_start_time.is_some() {
|
if self.intraday_execution_start_time.is_some() {
|
||||||
let execution_price = self.snapshot_execution_price(snapshot, side);
|
let execution_price = self.snapshot_execution_price(snapshot, side);
|
||||||
let quantity = match side {
|
let quantity = match side {
|
||||||
@@ -1174,26 +1170,7 @@ where
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_cursor = execution_cursors
|
None
|
||||||
.get(symbol)
|
|
||||||
.copied()
|
|
||||||
.into_iter()
|
|
||||||
.chain(global_execution_cursor)
|
|
||||||
.chain(
|
|
||||||
self.intraday_execution_start_time
|
|
||||||
.map(|start_time| date.and_time(start_time)),
|
|
||||||
)
|
|
||||||
.max();
|
|
||||||
let quotes = data.execution_quotes_on(date, symbol);
|
|
||||||
self.select_execution_fill(
|
|
||||||
quotes,
|
|
||||||
side,
|
|
||||||
start_cursor,
|
|
||||||
requested_qty,
|
|
||||||
round_lot,
|
|
||||||
cash_limit,
|
|
||||||
gross_limit,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_execution_fill(
|
fn select_execution_fill(
|
||||||
@@ -1339,13 +1316,8 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn uses_serial_execution_cursor(&self, reason: &str) -> bool {
|
fn uses_serial_execution_cursor(&self, reason: &str) -> bool {
|
||||||
matches!(
|
let _ = reason;
|
||||||
reason,
|
false
|
||||||
"stop_loss_exit"
|
|
||||||
| "take_profit_exit"
|
|
||||||
| "replacement_after_stop_loss_exit"
|
|
||||||
| "replacement_after_take_profit_exit"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ struct SymbolPriceSeries {
|
|||||||
last_prices: Vec<f64>,
|
last_prices: Vec<f64>,
|
||||||
open_prefix: Vec<f64>,
|
open_prefix: Vec<f64>,
|
||||||
close_prefix: Vec<f64>,
|
close_prefix: Vec<f64>,
|
||||||
|
prev_close_prefix: Vec<f64>,
|
||||||
last_prefix: Vec<f64>,
|
last_prefix: Vec<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +327,7 @@ impl SymbolPriceSeries {
|
|||||||
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 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 last_prefix = prefix_sums(&last_prices);
|
let last_prefix = prefix_sums(&last_prices);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -336,6 +338,7 @@ impl SymbolPriceSeries {
|
|||||||
last_prices,
|
last_prices,
|
||||||
open_prefix,
|
open_prefix,
|
||||||
close_prefix,
|
close_prefix,
|
||||||
|
prev_close_prefix,
|
||||||
last_prefix,
|
last_prefix,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,29 +366,16 @@ impl SymbolPriceSeries {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn decision_price_on_or_before(&self, date: NaiveDate) -> Option<f64> {
|
fn decision_price_on_or_before(&self, date: NaiveDate) -> Option<f64> {
|
||||||
let end = self.end_index(date)?;
|
let end = self.decision_end_index(date)?;
|
||||||
if end == 0 {
|
if end == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let last_idx = end - 1;
|
self.prev_closes.get(end - 1).copied()
|
||||||
if self.dates.get(last_idx).copied() == Some(date) {
|
|
||||||
let prev_close = self.prev_closes.get(last_idx).copied().unwrap_or_default();
|
|
||||||
if prev_close.is_finite() && prev_close > 0.0 {
|
|
||||||
return Some(prev_close);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.closes.get(last_idx).copied()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decision_end_index(&self, date: NaiveDate) -> Option<usize> {
|
fn decision_end_index(&self, date: NaiveDate) -> Option<usize> {
|
||||||
match self.dates.binary_search(&date) {
|
match self.dates.binary_search(&date) {
|
||||||
Ok(idx) => {
|
Ok(idx) => Some(idx + 1),
|
||||||
if idx == 0 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(idx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(0) => None,
|
Err(0) => None,
|
||||||
Err(idx) => Some(idx),
|
Err(idx) => Some(idx),
|
||||||
}
|
}
|
||||||
@@ -400,7 +390,7 @@ impl SymbolPriceSeries {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let start = end - lookback;
|
let start = end - lookback;
|
||||||
let sum = self.close_prefix[end] - self.close_prefix[start];
|
let sum = self.prev_close_prefix[end] - self.prev_close_prefix[start];
|
||||||
Some(sum / lookback as f64)
|
Some(sum / lookback as f64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -634,15 +634,40 @@ impl JqMicroCapStrategy {
|
|||||||
if !sizing_price.is_finite() || sizing_price <= 0.0 {
|
if !sizing_price.is_finite() || sizing_price <= 0.0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let 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,
|
round_lot,
|
||||||
);
|
);
|
||||||
if requested_qty == 0 {
|
let projected_execution_price = self.projected_execution_price(market, OrderSide::Buy);
|
||||||
|
let mut projected_fill = self.projected_select_execution_fill(
|
||||||
|
ctx,
|
||||||
|
date,
|
||||||
|
symbol,
|
||||||
|
OrderSide::Buy,
|
||||||
|
u32::MAX,
|
||||||
|
round_lot,
|
||||||
|
Some(projected.cash()),
|
||||||
|
Some(order_value + 400.0),
|
||||||
|
execution_state,
|
||||||
|
);
|
||||||
|
let mut quantity = snapshot_requested_qty;
|
||||||
|
while quantity > 0 {
|
||||||
|
let gross_amount = projected_execution_price * quantity as f64;
|
||||||
|
let cash_out = gross_amount + self.buy_commission(gross_amount);
|
||||||
|
if gross_amount <= order_value + 400.0
|
||||||
|
&& cash_out <= projected.cash() + 1e-6
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
quantity = quantity.saturating_sub(round_lot);
|
||||||
|
}
|
||||||
|
if quantity == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let execution_price = self.projected_execution_price(market, OrderSide::Buy);
|
let execution_price = projected_fill
|
||||||
let mut quantity = requested_qty;
|
.as_ref()
|
||||||
|
.map(|fill| fill.price)
|
||||||
|
.unwrap_or(projected_execution_price);
|
||||||
while quantity > 0 {
|
while quantity > 0 {
|
||||||
let gross_amount = execution_price * quantity as f64;
|
let gross_amount = execution_price * quantity as f64;
|
||||||
let cash_out = gross_amount + self.buy_commission(gross_amount);
|
let cash_out = gross_amount + self.buy_commission(gross_amount);
|
||||||
@@ -695,11 +720,25 @@ impl JqMicroCapStrategy {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let market = ctx.data.market(date, symbol)?;
|
let market = ctx.data.market(date, symbol)?;
|
||||||
let fill = ProjectedExecutionFill {
|
let round_lot = self.projected_round_lot(ctx, symbol);
|
||||||
price: self.projected_execution_price(market, OrderSide::Sell),
|
let fill = self
|
||||||
quantity,
|
.projected_select_execution_fill(
|
||||||
next_cursor: date.and_time(self.intraday_execution_start_time()) + Duration::seconds(1),
|
ctx,
|
||||||
};
|
date,
|
||||||
|
symbol,
|
||||||
|
OrderSide::Sell,
|
||||||
|
quantity,
|
||||||
|
round_lot,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
execution_state,
|
||||||
|
)
|
||||||
|
.unwrap_or(ProjectedExecutionFill {
|
||||||
|
price: self.projected_execution_price(market, OrderSide::Sell),
|
||||||
|
quantity,
|
||||||
|
next_cursor: date.and_time(self.intraday_execution_start_time())
|
||||||
|
+ Duration::seconds(1),
|
||||||
|
});
|
||||||
let gross_amount = fill.price * fill.quantity as f64;
|
let gross_amount = fill.price * fill.quantity as f64;
|
||||||
let net_cash = gross_amount - self.sell_cost(gross_amount);
|
let net_cash = gross_amount - self.sell_cost(gross_amount);
|
||||||
projected
|
projected
|
||||||
@@ -770,14 +809,8 @@ impl JqMicroCapStrategy {
|
|||||||
symbol: &str,
|
symbol: &str,
|
||||||
execution_state: &ProjectedExecutionState,
|
execution_state: &ProjectedExecutionState,
|
||||||
) -> Option<NaiveDateTime> {
|
) -> Option<NaiveDateTime> {
|
||||||
execution_state
|
let _ = (symbol, execution_state);
|
||||||
.execution_cursors
|
Some(date.and_time(self.intraday_execution_start_time()))
|
||||||
.get(symbol)
|
|
||||||
.copied()
|
|
||||||
.into_iter()
|
|
||||||
.chain(execution_state.global_execution_cursor)
|
|
||||||
.chain(Some(date.and_time(self.intraday_execution_start_time())))
|
|
||||||
.max()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn projected_select_execution_fill(
|
fn projected_select_execution_fill(
|
||||||
@@ -917,13 +950,8 @@ impl JqMicroCapStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn uses_serial_execution_cursor(&self, reason: &str) -> bool {
|
fn uses_serial_execution_cursor(&self, reason: &str) -> bool {
|
||||||
matches!(
|
let _ = reason;
|
||||||
reason,
|
false
|
||||||
"stop_loss_exit"
|
|
||||||
| "take_profit_exit"
|
|
||||||
| "replacement_after_stop_loss_exit"
|
|
||||||
| "replacement_after_take_profit_exit"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trading_ratio(
|
fn trading_ratio(
|
||||||
|
|||||||
Reference in New Issue
Block a user