just a checkpoint

This commit is contained in:
zastian-dev
2026-02-13 16:28:42 +00:00
parent 798c3eafd5
commit 73cc7a3a66
9 changed files with 958 additions and 317 deletions

View File

@@ -26,7 +26,13 @@ impl Strategy {
}
}
/// Volatility-adjusted position sizing using ATR.
/// Volatility-adjusted position sizing using ATR (Kelly-inspired).
///
/// Position size = (Risk per trade / ATR stop distance) * confidence.
/// The confidence scaling now has a much wider range (0.4 to 1.0) so that
/// weak Buy signals (confidence ~0.4) get 40% size while StrongBuy signals
/// (confidence ~1.0) get full size. This is a fractional Kelly approach:
/// bet more when conviction is higher, less when marginal.
pub fn calculate_position_size(
&self,
price: f64,
@@ -42,8 +48,9 @@ impl Strategy {
let atr_stop_pct = signal.atr_pct * ATR_STOP_MULTIPLIER;
let risk_amount = portfolio_value * RISK_PER_TRADE;
let vol_adjusted = risk_amount / atr_stop_pct;
// Scale by confidence
let confidence_scale = 0.7 + 0.3 * signal.confidence;
// Wide confidence scaling: 0.4x for weak signals, 1.0x for strongest.
// Old code used 0.7 + 0.3*conf which barely differentiated.
let confidence_scale = 0.4 + 0.6 * signal.confidence;
let sized = vol_adjusted * confidence_scale;
sized.min(portfolio_value * MAX_POSITION_SIZE)
} else {
@@ -51,12 +58,26 @@ impl Strategy {
};
let position_value = position_value.min(available_cash);
// Use fractional shares Alpaca supports them for paper trading.
// Use fractional shares -- Alpaca supports them for paper trading.
// Truncate to 4 decimal places to avoid floating point dust.
((position_value / price) * 10000.0).floor() / 10000.0
}
/// Check if stop-loss, trailing stop, or time exit should trigger.
///
/// Exit priority (checked in order):
/// 1. Hard max-loss cap (MAX_LOSS_PCT) -- absolute worst-case, gap protection
/// 2. ATR-based stop-loss (ATR_STOP_MULTIPLIER * ATR) -- primary risk control
/// 3. Fixed % stop-loss (STOP_LOSS_PCT) -- fallback when ATR unavailable
/// 4. ATR trailing stop (ATR_TRAIL_MULTIPLIER * ATR from HWM) -- profit protection
/// 5. Time-based exit (TIME_EXIT_BARS) -- only if position is LOSING
///
/// Key design decisions:
/// - Trailing stop activates early (1.5x ATR) but has wide distance (2.5x ATR)
/// so winners have room to breathe but profits are protected.
/// - Time exit ONLY sells losers. Winners at the time limit are doing fine;
/// the trailing stop handles profit-taking on them.
/// - Max loss is wide enough to avoid being hit by normal ATR-level moves.
pub fn check_stop_loss_take_profit(
&mut self,
symbol: &str,
@@ -78,34 +99,25 @@ impl Strategy {
}
}
// Hard max-loss cap
// 1. Hard max-loss cap (catastrophic gap protection)
if pnl_pct <= -MAX_LOSS_PCT {
return Some(Signal::StrongSell);
}
// ATR-based stop loss
// 2. ATR-based initial stop-loss (primary risk control)
if entry_atr > 0.0 {
let atr_stop_price = entry_price - ATR_STOP_MULTIPLIER * entry_atr;
if current_price <= atr_stop_price {
return Some(Signal::StrongSell);
}
} else if pnl_pct <= -STOP_LOSS_PCT {
// 3. Fixed percentage fallback
return Some(Signal::StrongSell);
}
// Time-based exit
if bars_held >= TIME_EXIT_BARS {
let activation = if entry_atr > 0.0 {
(ATR_TRAIL_ACTIVATION_MULTIPLIER * entry_atr) / entry_price
} else {
TRAILING_STOP_ACTIVATION
};
if pnl_pct < activation {
return Some(Signal::Sell);
}
}
// ATR-based trailing stop
// 4. ATR-based trailing stop (profit protection)
// Activates earlier than before (1.5x ATR gain) so profits are locked in.
// Distance is wider (2.5x ATR from HWM) so normal retracements don't trigger it.
let activation_gain = if entry_atr > 0.0 {
(ATR_TRAIL_ACTIVATION_MULTIPLIER * entry_atr) / entry_price
} else {
@@ -126,6 +138,14 @@ impl Strategy {
}
}
// 5. Time-based exit: only for LOSING positions (capital efficiency)
// Winners at the time limit are managed by the trailing stop.
// This prevents the old behavior of dumping winners just because they
// haven't hit an arbitrary activation threshold in N bars.
if bars_held >= TIME_EXIT_BARS && pnl_pct < 0.0 {
return Some(Signal::Sell);
}
None
}