From 84461319a0dd102b4af0c08d8ac32cf0b27350a9 Mon Sep 17 00:00:00 2001 From: mrfluffy Date: Thu, 26 Feb 2026 17:05:57 +0000 Subject: [PATCH] more profit --- src/config.rs | 6 ++++ src/dashboard.rs | 26 ++++++++++++++--- src/strategy.rs | 72 +++++++++++++++++++++++++++++------------------- 3 files changed, 72 insertions(+), 32 deletions(-) diff --git a/src/config.rs b/src/config.rs index 0ccdc20..90dc33c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -78,6 +78,12 @@ pub const RISK_PER_TRADE: f64 = 0.015; // 1.5% risk per trade (8 positions * 1.5 pub const ATR_STOP_MULTIPLIER: f64 = 3.5; // Wide stops reduce false stop-outs (the #1 loss source) pub const ATR_TRAIL_MULTIPLIER: f64 = 3.0; // Wide trail so winners run longer pub const ATR_TRAIL_ACTIVATION_MULTIPLIER: f64 = 2.0; // Don't activate trail too early +// Tiered trailing stop: tight trail for small gains, wide trail for big gains +pub const EARLY_TRAIL_ACTIVATION_MULTIPLIER: f64 = 0.5; // Activate tight trail after 0.5x ATR gain +pub const EARLY_TRAIL_MULTIPLIER: f64 = 1.5; // Tight trail distance for small gains +// Breakeven protection: once in profit, don't let it become a big loss +pub const BREAKEVEN_ACTIVATION_PCT: f64 = 0.02; // Activate after 2% gain (meaningful, not noise) +pub const BREAKEVEN_MAX_LOSS_PCT: f64 = 0.005; // Once activated, don't give back more than 0.5% from entry // Portfolio-level controls pub const MAX_CONCURRENT_POSITIONS: usize = 8; // Fewer positions = higher conviction per trade pub const MAX_SECTOR_POSITIONS: usize = 2; diff --git a/src/dashboard.rs b/src/dashboard.rs index eda5e43..3768c5f 100644 --- a/src/dashboard.rs +++ b/src/dashboard.rs @@ -14,7 +14,11 @@ use tower_http::cors::CorsLayer; use crate::{ alpaca::AlpacaClient, - config::{ATR_TRAIL_ACTIVATION_MULTIPLIER, ATR_TRAIL_MULTIPLIER}, + config::{ + ATR_STOP_MULTIPLIER, ATR_TRAIL_ACTIVATION_MULTIPLIER, ATR_TRAIL_MULTIPLIER, + BREAKEVEN_ACTIVATION_PCT, BREAKEVEN_MAX_LOSS_PCT, + EARLY_TRAIL_ACTIVATION_MULTIPLIER, EARLY_TRAIL_MULTIPLIER, + }, paths::{LIVE_ENTRY_ATRS_FILE, LIVE_EQUITY_FILE, LIVE_HIGH_WATER_MARKS_FILE}, types::EquitySnapshot, }; @@ -580,12 +584,26 @@ async fn api_positions(State(state): State>) -> impl IntoRes 0.0 }; - let (trail_status, stop_loss_price) = if pnl_pct >= activation_gain && entry_atr > 0.0 { + let best_pnl = (high_water_mark - entry_price) / entry_price; + let big_activation = if entry_atr > 0.0 { + (ATR_TRAIL_ACTIVATION_MULTIPLIER * entry_atr) / entry_price + } else { 0.0 }; + let small_activation = if entry_atr > 0.0 { + (EARLY_TRAIL_ACTIVATION_MULTIPLIER * entry_atr) / entry_price + } else { 0.0 }; + + let (trail_status, stop_loss_price) = if best_pnl >= BREAKEVEN_ACTIVATION_PCT && pnl_pct <= -BREAKEVEN_MAX_LOSS_PCT { + ("Breakeven!".to_string(), entry_price * (1.0 - BREAKEVEN_MAX_LOSS_PCT)) + } else if entry_atr > 0.0 && best_pnl >= big_activation { let trail_distance = ATR_TRAIL_MULTIPLIER * entry_atr; let stop_price = high_water_mark - trail_distance; - ("Active".to_string(), stop_price) + ("Wide Trail".to_string(), stop_price) + } else if entry_atr > 0.0 && pnl_pct >= small_activation { + let trail_distance = EARLY_TRAIL_MULTIPLIER * entry_atr; + let stop_price = high_water_mark - trail_distance; + ("Tight Trail".to_string(), stop_price) } else { - ("Inactive".to_string(), entry_price - ATR_TRAIL_MULTIPLIER * entry_atr) + ("Inactive".to_string(), entry_price - ATR_STOP_MULTIPLIER * entry_atr) }; PositionResponse { diff --git a/src/strategy.rs b/src/strategy.rs index 5d39057..3e71850 100644 --- a/src/strategy.rs +++ b/src/strategy.rs @@ -2,7 +2,10 @@ use std::collections::HashMap; use crate::config::{ get_sector, IndicatorParams, Timeframe, ATR_STOP_MULTIPLIER, - ATR_TRAIL_ACTIVATION_MULTIPLIER, ATR_TRAIL_MULTIPLIER, MAX_LOSS_PCT, MAX_POSITION_SIZE, + ATR_TRAIL_ACTIVATION_MULTIPLIER, ATR_TRAIL_MULTIPLIER, + BREAKEVEN_ACTIVATION_PCT, BREAKEVEN_MAX_LOSS_PCT, + EARLY_TRAIL_ACTIVATION_MULTIPLIER, EARLY_TRAIL_MULTIPLIER, + MAX_LOSS_PCT, MAX_POSITION_SIZE, MIN_ATR_PCT, RISK_PER_TRADE, STOP_LOSS_PCT, TIME_EXIT_BARS, TRAILING_STOP_ACTIVATION, TRAILING_STOP_DISTANCE, }; @@ -66,18 +69,14 @@ impl Strategy { /// 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. + /// 1. Hard max-loss cap (MAX_LOSS_PCT) -- gap protection + /// 2. ATR-based stop-loss -- primary risk control + /// 3. Fixed % stop-loss -- fallback when ATR unavailable + /// 4. Breakeven ratchet -- once in profit, never lose more than 1% + /// 5. Tiered trailing stop: + /// - Small gains (0.5x ATR): tight trail (1.5x ATR) + /// - Big gains (2.0x ATR): wide trail (3.0x ATR) + /// 6. Time-based exit -- only if position is LOSING pub fn check_stop_loss_take_profit( &mut self, symbol: &str, @@ -115,22 +114,39 @@ impl Strategy { return Some(Signal::StrongSell); } - // 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 { - TRAILING_STOP_ACTIVATION - }; - - if pnl_pct >= activation_gain { + // 4. Breakeven ratchet: once we've been in profit, cap downside to -1% + if pnl_pct <= -BREAKEVEN_MAX_LOSS_PCT { if let Some(&high_water) = self.high_water_marks.get(symbol) { - let trail_distance = if entry_atr > 0.0 { - ATR_TRAIL_MULTIPLIER * entry_atr + let best_pnl = (high_water - entry_price) / entry_price; + if best_pnl >= BREAKEVEN_ACTIVATION_PCT { + // Was in profit but now losing > 1% — get out + return Some(Signal::Sell); + } + } + } + + // 5. Tiered ATR trailing stop (profit protection) + // Tier 1: small gains (0.5x ATR) → tight trail (1.5x ATR) + // Tier 2: big gains (2.0x ATR) → wide trail (3.0x ATR) to let winners run + if let Some(&high_water) = self.high_water_marks.get(symbol) { + let best_pnl = (high_water - entry_price) / entry_price; + + let (activation_gain, trail_distance) = if entry_atr > 0.0 { + let big_activation = (ATR_TRAIL_ACTIVATION_MULTIPLIER * entry_atr) / entry_price; + let small_activation = (EARLY_TRAIL_ACTIVATION_MULTIPLIER * entry_atr) / entry_price; + + if best_pnl >= big_activation { + // Tier 2: big winner — wide trail + (big_activation, ATR_TRAIL_MULTIPLIER * entry_atr) } else { - high_water * TRAILING_STOP_DISTANCE - }; + // Tier 1: small gain — tight trail + (small_activation, EARLY_TRAIL_MULTIPLIER * entry_atr) + } + } else { + (TRAILING_STOP_ACTIVATION, high_water * TRAILING_STOP_DISTANCE) + }; + + if pnl_pct >= activation_gain { let trailing_stop_price = high_water - trail_distance; if current_price <= trailing_stop_price { return Some(Signal::Sell); @@ -138,7 +154,7 @@ impl Strategy { } } - // 5. Time-based exit: only for LOSING positions (capital efficiency) + // 6. 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.