use std::cmp::Ordering;
use std::collections::HashMap;
use crate::backtesting::engine::BacktestEngine;
use crate::backtesting::position::{Position, PositionSide};
use crate::backtesting::result::SignalRecord;
use crate::backtesting::signal::{Signal, SignalDirection};
use crate::backtesting::strategy::Strategy;
use super::super::config::PortfolioConfig;
use super::state::{SymbolState, compute_buying_power};
#[allow(clippy::too_many_arguments)]
pub(super) fn open_pending_entries<S: Strategy>(
config: &PortfolioConfig,
helper_engine: &BacktestEngine,
states: &mut HashMap<String, SymbolState<S>>,
mut pending_entries: Vec<(String, Signal)>,
timestamp: i64,
initial_capital: f64,
n_symbols: usize,
cash: &mut f64,
) {
pending_entries.sort_by(|(sym_a, sig_a), (sym_b, sig_b)| {
sig_b
.strength
.value()
.partial_cmp(&sig_a.strength.value())
.unwrap_or(Ordering::Equal)
.then_with(|| sym_a.cmp(sym_b))
});
let open_positions_count: usize = states.values().filter(|s| s.position.is_some()).count();
let mut positions_open = open_positions_count;
for (sym, signal) in pending_entries {
let (has_position, signal_price, fill_open, fill_ts) = {
let state = states.get(&sym).unwrap();
let idx = state.ts_index[×tamp];
let signal_price = state.candles[idx].close;
let next = state.candles.get(idx + 1).map(|c| (c.open, c.timestamp));
(
state.position.is_some(),
signal_price,
next.map(|(o, _)| o),
next.map(|(_, t)| t),
)
};
if has_position {
continue;
}
let (Some(fill_open), Some(fill_ts)) = (fill_open, fill_ts) else {
states.get_mut(&sym).unwrap().signals.push(SignalRecord {
timestamp: signal.timestamp,
price: signal.price,
direction: signal.direction,
strength: signal.strength.value(),
reason: signal.reason,
executed: false,
tags: signal.tags,
});
continue;
};
if let Some(max) = config.max_total_positions
&& positions_open >= max
{
states.get_mut(&sym).unwrap().signals.push(SignalRecord {
timestamp: signal.timestamp,
price: signal.price,
direction: signal.direction,
strength: signal.strength.value(),
reason: signal.reason,
executed: false,
tags: signal.tags,
});
continue;
}
if signal.direction == SignalDirection::Short && !config.base.allow_short {
states.get_mut(&sym).unwrap().signals.push(SignalRecord {
timestamp: signal.timestamp,
price: signal.price,
direction: signal.direction,
strength: signal.strength.value(),
reason: signal.reason,
executed: false,
tags: signal.tags,
});
continue;
}
let is_long = signal.direction == SignalDirection::Long;
let entry_price_slipped = config.base.apply_entry_slippage(fill_open, is_long);
let entry_price = config.base.apply_entry_spread(entry_price_slipped, is_long);
let fraction = {
let state = states.get(&sym).unwrap();
let idx = state.ts_index[×tamp];
let ctx = helper_engine.build_sizing_context(idx, &state.sizing_series, &state.trades);
config.base.sizing_fraction(entry_price, &ctx)
};
let buying_power = compute_buying_power(*cash, states, timestamp, config.base.max_leverage);
let target_capital =
config.allocation_target(&sym, buying_power, initial_capital, n_symbols, fraction);
if target_capital <= 0.0 {
states.get_mut(&sym).unwrap().signals.push(SignalRecord {
timestamp: signal.timestamp,
price: signal.price,
direction: signal.direction,
strength: signal.strength.value(),
reason: signal.reason,
executed: false,
tags: signal.tags,
});
continue;
}
let (flat_reserve, pct_friction) = if config.base.commission_fn.is_some() {
(0.0, 0.0)
} else {
(config.base.commission, config.base.commission_pct)
};
let tax_friction = if is_long {
config.base.transaction_tax_pct
} else {
0.0
};
let effective_target = (target_capital - flat_reserve).max(0.0);
let quantity = effective_target / (entry_price * (1.0 + pct_friction + tax_friction));
let entry_comm = config.base.calculate_commission(quantity, entry_price);
let entry_tax = config
.base
.calculate_transaction_tax(entry_price * quantity, is_long);
let entry_cost = entry_price * quantity + entry_comm + entry_tax;
if entry_cost > buying_power {
states.get_mut(&sym).unwrap().signals.push(SignalRecord {
timestamp: signal.timestamp,
price: signal.price,
direction: signal.direction,
strength: signal.strength.value(),
reason: signal.reason,
executed: false,
tags: signal.tags,
});
continue;
}
if is_long {
*cash -= entry_cost;
} else {
*cash += entry_price * quantity - entry_comm;
}
let side = if is_long {
PositionSide::Long
} else {
PositionSide::Short
};
let state = states.get_mut(&sym).unwrap();
state.position = Some(Position::new_with_tax(
side,
fill_ts,
entry_price,
quantity,
entry_comm,
entry_tax,
signal.clone(),
));
state.hwm = Some(entry_price);
state.extremes = None;
state.signals.push(SignalRecord {
timestamp: signal.timestamp,
price: signal_price,
direction: signal.direction,
strength: signal.strength.value(),
reason: signal.reason,
executed: true,
tags: signal.tags,
});
positions_open += 1;
}
}