From 51d68b0c3146c37b176bcbc0f623a2d10433e40a Mon Sep 17 00:00:00 2001 From: Justin Oros Date: Mon, 26 Jan 2026 19:47:58 -0700 Subject: [PATCH] feat: Add multi-timeframe analysis, regime detection, ATR-based stops, limit orders, and scale-out profit targets to improve win rate --- daytrader.py | 1136 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 838 insertions(+), 298 deletions(-) diff --git a/daytrader.py b/daytrader.py index bb24345..cf52df4 100644 --- a/daytrader.py +++ b/daytrader.py @@ -28,18 +28,34 @@ ENV_PATH = SCRIPT_DIR / ".env" # Default configuration DEFAULT_CONFIG = { "SYMBOL": "SPY", - "RISK_FRACTION": 0.02, - "SHORT_WINDOW": 5, - "LONG_WINDOW": 20, + "RISK_PER_TRADE": 0.005, + "SHORT_WINDOW": 20, + "LONG_WINDOW": 50, "MIN_NOTIONAL": 1.0, - "POLL_INTERVAL": 30, - "MAX_DRAWDOWN": 0.05, + "POLL_INTERVAL": 1800, + "MAX_DRAWDOWN": 0.12, "PDT_RULE": True, "USE_TRAILING_STOP": True, - "PROFIT_TARGETS": [0.03, 0.05], + "PROFIT_TARGET_1": 1.5, + "PROFIT_TARGET_2": 3.0, "VOLATILITY_ADJUSTMENT": True, "MARKET_HOURS_FILTER": True, - "MULTI_INDICATOR": True + "ENABLE_SLIPPAGE": True, + "SLIPPAGE_PCT": 0.0005, + "COMMISSION_PCT": 0.0005, + "MIN_SIGNAL_STRENGTH": 0.75, + "BACKTEST_DAYS": 90, + "USE_LIMIT_ORDERS": True, + "LIMIT_ORDER_TIMEOUT": 60, + "ADX_THRESHOLD": 20, + "VOLUME_MULTIPLIER": 1.2, + "ATR_STOP_MULTIPLIER": 1.5, + "MAX_HOLD_TIME": 7200, + "REGIME_DETECTION": True, + "MULTIFRAME_FILTER": True, + "BB_WINDOW": 20, + "BB_STD": 2.0, + "USE_EMA": True } # Load environment variables @@ -68,7 +84,7 @@ else: # Extract configuration values SYMBOL = config["SYMBOL"] -RISK_FRACTION = float(config["RISK_FRACTION"]) +RISK_PER_TRADE = float(config["RISK_PER_TRADE"]) SHORT_WINDOW = int(config["SHORT_WINDOW"]) LONG_WINDOW = int(config["LONG_WINDOW"]) MIN_NOTIONAL = float(config["MIN_NOTIONAL"]) @@ -76,10 +92,26 @@ POLL_INTERVAL = int(config["POLL_INTERVAL"]) MAX_DRAWDOWN = float(config["MAX_DRAWDOWN"]) PDT_RULE = bool(config["PDT_RULE"]) USE_TRAILING_STOP = bool(config["USE_TRAILING_STOP"]) -PROFIT_TARGETS = config["PROFIT_TARGETS"] +PROFIT_TARGET_1 = float(config["PROFIT_TARGET_1"]) +PROFIT_TARGET_2 = float(config["PROFIT_TARGET_2"]) VOLATILITY_ADJUSTMENT = bool(config["VOLATILITY_ADJUSTMENT"]) MARKET_HOURS_FILTER = bool(config["MARKET_HOURS_FILTER"]) -MULTI_INDICATOR = bool(config["MULTI_INDICATOR"]) +ENABLE_SLIPPAGE = bool(config["ENABLE_SLIPPAGE"]) +SLIPPAGE_PCT = float(config["SLIPPAGE_PCT"]) +COMMISSION_PCT = float(config["COMMISSION_PCT"]) +MIN_SIGNAL_STRENGTH = float(config["MIN_SIGNAL_STRENGTH"]) +BACKTEST_DAYS = int(config["BACKTEST_DAYS"]) +USE_LIMIT_ORDERS = bool(config["USE_LIMIT_ORDERS"]) +LIMIT_ORDER_TIMEOUT = int(config["LIMIT_ORDER_TIMEOUT"]) +ADX_THRESHOLD = float(config["ADX_THRESHOLD"]) +VOLUME_MULTIPLIER = float(config["VOLUME_MULTIPLIER"]) +ATR_STOP_MULTIPLIER = float(config["ATR_STOP_MULTIPLIER"]) +MAX_HOLD_TIME = int(config["MAX_HOLD_TIME"]) +REGIME_DETECTION = bool(config["REGIME_DETECTION"]) +MULTIFRAME_FILTER = bool(config["MULTIFRAME_FILTER"]) +BB_WINDOW = int(config["BB_WINDOW"]) +BB_STD = float(config["BB_STD"]) +USE_EMA = bool(config["USE_EMA"]) # Initialize Alpaca API api = tradeapi.REST( @@ -90,19 +122,19 @@ api = tradeapi.REST( ) # ----------------------------------------------------------------------------- -# Technical Analysis Functions (Pure Python) +# Technical Analysis Functions # ----------------------------------------------------------------------------- def calculate_sma(data, window): - """Calculate Simple Moving Average""" + # Calculate Simple Moving Average return data.rolling(window=window).mean() def calculate_ema(data, window): - """Calculate Exponential Moving Average""" + # Calculate Exponential Moving Average return data.ewm(span=window, adjust=False).mean() def calculate_rsi(data, window=14): - """Calculate Relative Strength Index""" + # Calculate Relative Strength Index delta = data.diff() gain = (delta.where(delta > 0, 0)).rolling(window=window).mean() loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean() @@ -110,24 +142,8 @@ def calculate_rsi(data, window=14): rsi = 100 - (100 / (1 + rs)) return rsi -def calculate_macd(data, fast=12, slow=26, signal=9): - """Calculate MACD""" - ema_fast = calculate_ema(data, fast) - ema_slow = calculate_ema(data, slow) - macd_line = ema_fast - ema_slow - signal_line = calculate_ema(macd_line, signal) - return macd_line, signal_line - -def calculate_bollinger_bands(data, window=20, num_std=2): - """Calculate Bollinger Bands""" - sma = calculate_sma(data, window) - std = data.rolling(window=window).std() - upper_band = sma + (std * num_std) - lower_band = sma - (std * num_std) - return upper_band, sma, lower_band - def calculate_atr(high, low, close, window=14): - """Calculate Average True Range""" + # Calculate Average True Range high_low = high - low high_close_prev = abs(high - close.shift()) low_close_prev = abs(low - close.shift()) @@ -135,11 +151,127 @@ def calculate_atr(high, low, close, window=14): atr = true_range.rolling(window=window).mean() return atr +def calculate_adx(high, low, close, window=14): + # Calculate Average Directional Index (ADX) for trend strength + # Calculate True Range + tr1 = high - low + tr2 = abs(high - close.shift()) + tr3 = abs(low - close.shift()) + tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) + atr = tr.rolling(window=window).mean() + + # Calculate Directional Movement + up_move = high - high.shift() + down_move = low.shift() - low + + plus_dm = pd.Series(0.0, index=close.index) + minus_dm = pd.Series(0.0, index=close.index) + + plus_dm[(up_move > down_move) & (up_move > 0)] = up_move + minus_dm[(down_move > up_move) & (down_move > 0)] = down_move + + # Smooth the directional indicators + plus_di = 100 * (plus_dm.rolling(window=window).mean() / atr) + minus_di = 100 * (minus_dm.rolling(window=window).mean() / atr) + + # Calculate DX and ADX + dx = 100 * abs(plus_di - minus_di) / (plus_di + minus_di) + adx = dx.rolling(window=window).mean() + + return adx, plus_di, minus_di + +def calculate_bollinger_bands(close, window=20, num_std=2): + # Calculate Bollinger Bands for mean reversion + if USE_EMA: + middle = calculate_ema(close, window) + else: + middle = calculate_sma(close, window) + + std = close.rolling(window=window).std() + upper = middle + (std * num_std) + lower = middle - (std * num_std) + + return upper, middle, lower + +def check_volume_confirmation(bars): + # Check if current volume exceeds threshold + if 'volume' not in bars.columns or len(bars) < 20: + return True # Default to True if no volume data + + avg_volume = bars['volume'].rolling(window=20).mean().iloc[-1] + current_volume = bars['volume'].iloc[-1] + + return current_volume >= (avg_volume * VOLUME_MULTIPLIER) + +def detect_market_regime(bars): + # Detect market regime: trending, ranging, high_vol, low_vol + if len(bars) < 50: + return 'unknown' + + closes = bars['close'] + highs = bars['high'] + lows = bars['low'] + + # Calculate ADX for trend strength + adx, plus_di, minus_di = calculate_adx(highs, lows, closes, 14) + current_adx = adx.iloc[-1] + + # Calculate volatility percentile + atr = calculate_atr(highs, lows, closes, 14) + current_atr = atr.iloc[-1] + atr_percentile = (atr <= current_atr).sum() / len(atr) * 100 + + # Determine regime + if atr_percentile > 70: + return 'high_vol' + elif atr_percentile < 30: + return 'low_vol' + elif current_adx > ADX_THRESHOLD: + return 'trend' + else: + return 'range' + +def check_multiframe_confluence(symbol): + # Check hourly timeframe for trend alignment + if not MULTIFRAME_FILTER: + return 'neutral' + + try: + # Get hourly data + hourly_bars = api.get_bars(symbol, "1Hour", limit=50).df + if len(hourly_bars) < 50: + return 'neutral' + + closes = hourly_bars['close'] + + # Calculate hourly EMAs + if USE_EMA: + ema_short = calculate_ema(closes, 20) + ema_long = calculate_ema(closes, 50) + else: + ema_short = calculate_sma(closes, 20) + ema_long = calculate_sma(closes, 50) + + current_short = ema_short.iloc[-1] + current_long = ema_long.iloc[-1] + current_price = closes.iloc[-1] + + # Determine hourly trend + if current_short > current_long and current_price > current_short: + return 'bullish' + elif current_short < current_long and current_price < current_short: + return 'bearish' + else: + return 'neutral' + + except Exception as e: + logger.warning(f"âš ī¸ Could not check multiframe confluence: {e}") + return 'neutral' + # ----------------------------------------------------------------------------- # Logging Configuration # ----------------------------------------------------------------------------- -# Set up logging to daytrader.log in script directory LOG_PATH = SCRIPT_DIR / "daytrader.log" logging.basicConfig( @@ -158,7 +290,7 @@ logger = logging.getLogger(__name__) # ----------------------------------------------------------------------------- def seconds_to_human_readable(seconds): - """Convert seconds to human-readable format (hours, minutes, seconds).""" + # Convert seconds to human-readable format (hours, minutes, seconds). if seconds < 0: return "0 seconds" @@ -171,21 +303,313 @@ def seconds_to_human_readable(seconds): time_parts.append(f"{hours} hour{'s' if hours != 1 else ''}") if minutes > 0: time_parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}") - if secs > 0 and hours == 0: # Only show seconds if less than an hour + if secs > 0 and hours == 0: time_parts.append(f"{secs} second{'s' if secs != 1 else ''}") return " ".join(time_parts) if time_parts else "0 seconds" def format_market_time(dt_obj): - """Format datetime object to readable string.""" + # Format datetime object to readable string. return dt_obj.strftime("%Y-%m-%d %I:%M:%S %p %Z") +def apply_slippage(price, is_buy=True): + # Apply slippage and commission to price + if not ENABLE_SLIPPAGE: + return price + + slippage_adjustment = price * SLIPPAGE_PCT + commission_adjustment = price * COMMISSION_PCT + + if is_buy: + adjusted_price = price + slippage_adjustment + commission_adjustment + else: + adjusted_price = price - slippage_adjustment - commission_adjustment + + return adjusted_price + # ----------------------------------------------------------------------------- -# Trading Functions +# Enhanced Trading Functions # ----------------------------------------------------------------------------- +def enhanced_backtest_strategy(): + # Comprehensive backtest with improved strategy + logger.info("📊 Running enhanced backtest with improved strategy...") + + try: + end_date = datetime.now() + start_date = end_date - timedelta(days=BACKTEST_DAYS) + + bars = api.get_bars(SYMBOL, "15Min", start=start_date.isoformat(), + end=end_date.isoformat()).df + if len(bars) < 100: + logger.warning("âš ī¸ Insufficient data for backtest") + return True + + # Enhanced backtest with new strategy + closes = bars['close'] + highs = bars['high'] + lows = bars['low'] + + # Calculate indicators + if USE_EMA: + short_ma = calculate_ema(closes, SHORT_WINDOW) + long_ma = calculate_ema(closes, LONG_WINDOW) + else: + short_ma = calculate_sma(closes, SHORT_WINDOW) + long_ma = calculate_sma(closes, LONG_WINDOW) + + rsi = calculate_rsi(closes, 14) + adx, plus_di, minus_di = calculate_adx(highs, lows, closes, 14) + atr = calculate_atr(highs, lows, closes, 14) + upper_bb, middle_bb, lower_bb = calculate_bollinger_bands(closes, BB_WINDOW, BB_STD) + + # Track performance + initial_balance = 10000 + balance = initial_balance + position = 0 + entry_price = 0 + entry_time = None + stop_loss = 0 + trades = [] + winning_trades = 0 + + for i in range(max(SHORT_WINDOW, LONG_WINDOW, BB_WINDOW, 20), len(bars)): + current_price = closes.iloc[i] + current_time = bars.index[i] + current_adx = adx.iloc[i] + current_rsi = rsi.iloc[i] + current_atr = atr.iloc[i] + + # Determine regime + regime = 'trend' if current_adx > ADX_THRESHOLD else 'range' + + # Generate signals based on regime + if regime == 'trend': + # Trend-following logic + ma_signal = 1 if short_ma.iloc[i] > long_ma.iloc[i] else -1 + rsi_signal = 1 if current_rsi < 65 else (-1 if current_rsi > 35 else 0) + combined_signal = ma_signal + (rsi_signal * 0.3) + else: + # Mean reversion logic (Bollinger Bands) + if current_price <= lower_bb.iloc[i] and current_rsi < 30: + combined_signal = 1.5 # Strong buy + elif current_price >= upper_bb.iloc[i] and current_rsi > 70: + combined_signal = -1.5 # Strong sell + else: + combined_signal = 0 + + # Check volume (simplified for backtest) + volume_ok = True + + # Enter position + if position == 0 and abs(combined_signal) >= 1.2 and volume_ok: + position = 1 if combined_signal > 0 else -1 + entry_price = apply_slippage(current_price, combined_signal > 0) + entry_time = current_time + + # Set ATR-based stop loss + stop_distance = current_atr * ATR_STOP_MULTIPLIER + if position > 0: + stop_loss = entry_price - stop_distance + else: + stop_loss = entry_price + stop_distance + + trades.append({ + 'entry_price': entry_price, + 'position': position, + 'entry_time': entry_time, + 'stop_loss': stop_loss, + 'regime': regime + }) + + # Exit position + elif position != 0: + exit_triggered = False + exit_price = None + exit_reason = None + + # Stop loss check + if position > 0 and current_price <= stop_loss: + exit_triggered = True + exit_price = apply_slippage(stop_loss, False) + exit_reason = 'stop_loss' + elif position < 0 and current_price >= stop_loss: + exit_triggered = True + exit_price = apply_slippage(stop_loss, False) + exit_reason = 'stop_loss' + + # Time-based exit + time_in_trade = (current_time - entry_time).total_seconds() + if time_in_trade > MAX_HOLD_TIME: + exit_triggered = True + exit_price = apply_slippage(current_price, False) + exit_reason = 'time_limit' + + # Profit target exits + pnl_pct = (current_price - entry_price) / entry_price * position + risk_amount = abs(entry_price - stop_loss) / entry_price + + if pnl_pct >= (risk_amount * PROFIT_TARGET_1): + exit_triggered = True + exit_price = apply_slippage(current_price, False) + exit_reason = 'target_1' + + # Signal reversal + exit_signal = -1 if position > 0 else 1 + if (combined_signal * exit_signal) > 0.8: + exit_triggered = True + exit_price = apply_slippage(current_price, False) + exit_reason = 'signal_reversal' + + if exit_triggered: + pnl = (exit_price - entry_price) * position + balance += pnl + + if pnl > 0: + winning_trades += 1 + + position = 0 + trades[-1]['exit_price'] = exit_price + trades[-1]['pnl'] = pnl + trades[-1]['exit_reason'] = exit_reason + + # Calculate statistics + total_trades = len([t for t in trades if 'exit_price' in t]) + win_rate = winning_trades / total_trades if total_trades > 0 else 0 + total_return = (balance - initial_balance) / initial_balance + + # Calculate additional metrics + winning_pnl = sum([t['pnl'] for t in trades if 'pnl' in t and t['pnl'] > 0]) + losing_pnl = sum([abs(t['pnl']) for t in trades if 'pnl' in t and t['pnl'] < 0]) + profit_factor = winning_pnl / losing_pnl if losing_pnl > 0 else 0 + + logger.info(f"📈 Enhanced Backtest Results:") + logger.info(f" Total trades: {total_trades}") + logger.info(f" Win rate: {win_rate:.1%}") + logger.info(f" Total return: {total_return:.1%}") + logger.info(f" Profit factor: {profit_factor:.2f}") + logger.info(f" Final balance: ${balance:.2f}") + + if total_trades < 5: + logger.warning("âš ī¸ Very few trades generated - consider adjusting parameters") + return True + + if win_rate < 0.35: + logger.warning("âš ī¸ Low win rate in backtest - strategy may need optimization") + return True + + if profit_factor < 1.0: + logger.warning("âš ī¸ Profit factor < 1.0 - losing more than winning") + return True + + return True + + except Exception as e: + logger.warning(f"âš ī¸ Backtest failed: {e}") + return True + +def enhanced_signal_generator(symbol): + bars = get_recent_bars(symbol, 100) + if bars is None or len(bars) < 50: + return None, 0, 0 + + closes = bars['close'] + highs = bars['high'] + lows = bars['low'] + current_price = closes.iloc[-1] + + # Calculate indicators + if USE_EMA: + short_ma = calculate_ema(closes, SHORT_WINDOW).iloc[-1] + long_ma = calculate_ema(closes, LONG_WINDOW).iloc[-1] + else: + short_ma = calculate_sma(closes, SHORT_WINDOW).iloc[-1] + long_ma = calculate_sma(closes, LONG_WINDOW).iloc[-1] + + rsi = calculate_rsi(closes, 14).iloc[-1] + adx, plus_di, minus_di = calculate_adx(highs, lows, closes, 14) + current_adx = adx.iloc[-1] + atr = calculate_atr(highs, lows, closes, 14).iloc[-1] + upper_bb, middle_bb, lower_bb = calculate_bollinger_bands(closes, BB_WINDOW, BB_STD) + + # Volume confirmation + volume_ok = check_volume_confirmation(bars) + if not volume_ok: + return None, 0, 0 + + # Multi-timeframe filter + hourly_trend = check_multiframe_confluence(symbol) + + # Detect regime + regime = detect_market_regime(bars) + + # Avoid low volatility regimes + if regime == 'low_vol': + logger.info("📉 Low volatility regime detected - avoiding trade") + return None, 0, 0 + + # Initialize signal + signal = None + signal_strength = 0 + stop_loss = 0 + + # TREND REGIME: Trend-following with pullbacks + if regime == 'trend': + if current_adx > ADX_THRESHOLD: + # Bullish trend with pullback + if short_ma > long_ma and current_price < short_ma and rsi < 50: + if hourly_trend in ['bullish', 'neutral']: + signal = 'buy' + signal_strength = min(1.0, (current_adx / 40) * 0.7 + 0.3) + stop_loss = current_price - (atr * ATR_STOP_MULTIPLIER) + + # Bearish trend with pullback + elif short_ma < long_ma and current_price > short_ma and rsi > 50: + if hourly_trend in ['bearish', 'neutral']: + signal = 'sell' + signal_strength = min(1.0, (current_adx / 40) * 0.7 + 0.3) + stop_loss = current_price + (atr * ATR_STOP_MULTIPLIER) + + # RANGE REGIME: Mean reversion (Bollinger Bands) + elif regime == 'range': + bb_width = (upper_bb.iloc[-1] - lower_bb.iloc[-1]) / middle_bb.iloc[-1] + + # Oversold at lower band + if current_price <= lower_bb.iloc[- 1] and rsi < 30: + if hourly_trend != 'bearish': + signal = 'buy' + signal_strength = 0.8 + stop_loss = current_price - (atr * ATR_STOP_MULTIPLIER) + + # Overbought at upper band + elif current_price >= upper_bb.iloc[-1] and rsi > 70: + if hourly_trend != 'bullish': + signal = 'sell' + signal_strength = 0.8 + stop_loss = current_price + (atr * ATR_STOP_MULTIPLIER) + + # HIGH VOL REGIME: Reduce position sizing (handled elsewhere) + elif regime == 'high_vol': + # Still generate signals but will reduce position size + if short_ma > long_ma and rsi < 40: + if hourly_trend in ['bullish', 'neutral']: + signal = 'buy' + signal_strength = 0.6 + stop_loss = current_price - (atr * ATR_STOP_MULTIPLIER * 1.5) + elif short_ma < long_ma and rsi > 60: + if hourly_trend in ['bearish', 'neutral']: + signal = 'sell' + signal_strength = 0.6 + stop_loss = current_price + (atr * ATR_STOP_MULTIPLIER * 1.5) + + # Check minimum signal strength + if signal_strength < MIN_SIGNAL_STRENGTH: + return None, signal_strength, 0 + + return signal, signal_strength, stop_loss + def wait_until_market_open(): - """Wait until the market opens.""" + # Wait until the market opens. clock = api.get_clock() now = clock.timestamp next_open = clock.next_open @@ -197,13 +621,11 @@ def wait_until_market_open(): logger.info(f"🕒 Market opens at {format_market_time(next_open)}") logger.info(f"âąī¸ Waiting {readable_time}...") - # Sleep in smaller chunks to allow for graceful interruption while seconds_until_open > 0: - sleep_time = min(60, seconds_until_open) # Check every minute max + sleep_time = min(60, seconds_until_open) time.sleep(sleep_time) seconds_until_open -= sleep_time - # Update remaining time display periodically if sleep_time >= 60: remaining_readable = seconds_to_human_readable(seconds_until_open) logger.info(f"âąī¸ {remaining_readable} remaining...") @@ -213,7 +635,7 @@ def wait_until_market_open(): logger.info("✅ Market is open!") def fetch_equity(): - """Fetch the current account equity.""" + # Fetch the current account equity. try: account = api.get_account() return float(account.equity) @@ -222,7 +644,7 @@ def fetch_equity(): return 0.0 def fetch_buying_power(): - """Fetch the current buying power.""" + # Fetch the current buying power. try: account = api.get_account() return float(account.buying_power) @@ -231,7 +653,7 @@ def fetch_buying_power(): return 0.0 def get_day_trade_count(): - """Get the current day trade count.""" + # Get the current day trade count. try: account = api.get_account() return int(account.day_trade_count) @@ -239,29 +661,123 @@ def get_day_trade_count(): logger.error(f"❌ Failed to fetch day trade count: {e}") return 0 -def submit_buy(symbol, notional): - """Submit a buy order.""" +def submit_limit_buy(symbol, notional, limit_price): + # Submit a limit buy order if notional < MIN_NOTIONAL: logger.warning(f"âš ī¸ Notional ${notional:.2f} < minimum ${MIN_NOTIONAL} - skipping.") return False try: + shares = int(notional / limit_price) + + if shares == 0: + logger.warning(f"âš ī¸ Cannot buy fractional shares with ${notional:.2f}") + return False + + order = api.submit_order( + symbol=symbol, + qty=shares, + side="buy", + type="limit", + limit_price=round(limit_price, 2), + time_in_force="gtc" + ) + + logger.info(f"đŸŸĸ LIMIT BUY order submitted: {shares} shares of {symbol} @ ${limit_price:.2f}") + + # Wait for fill or timeout + start_time = time.time() + while (time.time() - start_time) < LIMIT_ORDER_TIMEOUT: + order_status = api.get_order(order.id) + if order_status.status == 'filled': + filled_price = float(order_status.filled_avg_price) + logger.info(f"✅ Limit buy FILLED @ ${filled_price:.2f}") + return filled_price + elif order_status.status in ['cancelled', 'expired', 'rejected']: + logger.warning(f"âš ī¸ Limit order {order_status.status}") + return False + time.sleep(2) + + # Timeout - cancel and use market order + logger.warning("âąī¸ Limit order timeout - switching to market order") + api.cancel_order(order.id) + return submit_market_buy(symbol, notional) + + except Exception as e: + logger.error(f"❌ Failed to submit limit buy: {e}") + return False + +def submit_market_buy(symbol, notional): + # Submit a market buy order (fallback) + try: + current_price = get_current_price(symbol) + if current_price == 0: + return False + + execution_price = apply_slippage(current_price, True) + shares = int(notional / execution_price) + + if shares == 0: + return False + api.submit_order( symbol=symbol, - notional=round(notional, 2), + qty=shares, side="buy", type="market", time_in_force="day" ) - logger.info(f"đŸŸĸ BUY ${notional:.2f} of {symbol}") - return True + logger.info(f"đŸŸĸ MARKET BUY {shares} shares of {symbol} at ~${execution_price:.2f}") + return execution_price except Exception as e: logger.error(f"❌ Failed to buy {symbol}: {e}") return False -def submit_sell(symbol, qty): - """Submit a sell order.""" +def submit_limit_sell(symbol, qty, limit_price): + # Submit a limit sell order try: + order = api.submit_order( + symbol=symbol, + qty=qty, + side="sell", + type="limit", + limit_price=round(limit_price, 2), + time_in_force="gtc" + ) + + logger.info(f"🔴 LIMIT SELL order submitted: {qty} shares of {symbol} @ ${limit_price:.2f}") + + # Wait for fill or timeout + start_time = time.time() + while (time.time() - start_time) < LIMIT_ORDER_TIMEOUT: + order_status = api.get_order(order.id) + if order_status.status == 'filled': + filled_price = float(order_status.filled_avg_price) + logger.info(f"✅ Limit sell FILLED @ ${filled_price:.2f}") + return filled_price + elif order_status.status in ['cancelled', 'expired', 'rejected']: + logger.warning(f"âš ī¸ Limit order {order_status.status}") + return False + time.sleep(2) + + # Timeout - cancel and use market order + logger.warning("âąī¸ Limit order timeout - switching to market order") + api.cancel_order(order.id) + return submit_market_sell(symbol, qty) + + except Exception as e: + logger.error(f"❌ Failed to submit limit sell: {e}") + return False + +def submit_market_sell(symbol, qty): + # Submit a market sell order (fallback) + try: + current_price = get_current_price(symbol) + if current_price == 0: + return False + + execution_price = apply_slippage(current_price, False) + api.submit_order( symbol=symbol, qty=qty, @@ -269,14 +785,14 @@ def submit_sell(symbol, qty): type="market", time_in_force="day" ) - logger.info(f"🔴 SELL {qty} shares of {symbol}") - return True + logger.info(f"🔴 MARKET SELL {qty} shares of {symbol} at ~${execution_price:.2f}") + return execution_price except Exception as e: logger.error(f"❌ Failed to sell {symbol}: {e}") return False def close_all_positions(): - """Close all open positions.""" + # Close all open positions. try: positions = api.list_positions() if not positions: @@ -285,15 +801,15 @@ def close_all_positions(): logger.warning("âš ī¸ Closing all open positions...") for pos in positions: - submit_sell(pos.symbol, int(float(pos.qty))) + submit_market_sell(pos.symbol, int(float(pos.qty))) logger.info("✅ All positions closed.") except Exception as e: logger.error(f"❌ Failed to close positions: {e}") def get_recent_bars(symbol, limit=100): - """Get recent bar data for a symbol.""" + # Get recent bar data for a symbol. try: - timeframe = "minute" if limit <= 200 else "15Min" # Use 15Min for larger requests + timeframe = "15Min" bars = api.get_bars( symbol, timeframe, @@ -304,91 +820,8 @@ def get_recent_bars(symbol, limit=100): logger.error(f"❌ Failed to fetch bars for {symbol}: {e}") return None -def enhanced_signal_generator(symbol): - """Multiple technical indicators for better signal confidence""" - if not MULTI_INDICATOR: - return simple_ma_cross_signal(symbol) - - bars = get_recent_bars(symbol, 100) - if bars is None or len(bars) < 50: - return None - - closes = bars['close'] - highs = bars['high'] - lows = bars['low'] - volumes = bars['volume'] - - # Multiple indicators - short_ma = calculate_sma(closes, SHORT_WINDOW).iloc[-1] - long_ma = calculate_sma(closes, LONG_WINDOW).iloc[-1] - rsi = calculate_rsi(closes, 14).iloc[-1] - macd_line, signal_line = calculate_macd(closes) - macd_current = macd_line.iloc[-1] if not pd.isna(macd_line.iloc[-1]) else 0 - macd_prev = macd_line.iloc[-2] if len(macd_line) > 1 else 0 - signal_current = signal_line.iloc[-1] if not pd.isna(signal_line.iloc[-1]) else 0 - signal_prev = signal_line.iloc[-2] if len(signal_line) > 1 else 0 - - # Volume analysis - volume_sma = calculate_sma(volumes, 20).iloc[-1] - current_volume = volumes.iloc[-1] - volume_ratio = current_volume / volume_sma if volume_sma > 0 else 1 - - # Signal scoring system - buy_score = 0 - sell_score = 0 - - # Moving average crossover - if short_ma > long_ma: - buy_score += 2 - else: - sell_score += 2 - - # RSI momentum - if rsi < 30: # Oversold - buy_score += 1 - elif rsi > 70: # Overbought - sell_score += 1 - - # MACD signal - if macd_current > signal_current and macd_prev <= signal_prev: - buy_score += 1 - elif macd_current < signal_current and macd_prev >= signal_prev: - sell_score += 1 - - # Volume confirmation - if volume_ratio > 1.2: # High volume confirmation - if buy_score > sell_score: - buy_score += 1 - elif sell_score > buy_score: - sell_score += 1 - - # Minimum threshold for action - if buy_score >= 3 and buy_score > sell_score: - return "buy" - elif sell_score >= 3 and sell_score > buy_score: - return "sell" - - return None - -def simple_ma_cross_signal(symbol): - """Simple moving average crossover signal (original logic)""" - bars = get_recent_bars(symbol, LONG_WINDOW + 5) - if bars is None or len(bars) < LONG_WINDOW: - return None - - closes = bars['close'] - short_ma = calculate_sma(closes, SHORT_WINDOW).iloc[-1] - long_ma = calculate_sma(closes, LONG_WINDOW).iloc[-1] - - if short_ma > long_ma: - return "buy" - elif short_ma < long_ma: - return "sell" - else: - return None - def current_position_qty(symbol): - """Get the current position quantity for a symbol.""" + # Get the current position quantity for a symbol. try: positions = api.list_positions() for pos in positions: @@ -400,14 +833,13 @@ def current_position_qty(symbol): return 0 def pdt_allows_new_trade(): - """Check if PDT rules allow a new trade.""" + # Check if PDT rules allow a new trade. if not PDT_RULE: return True equity = fetch_equity() day_trade_count = get_day_trade_count() - # PDT rule: If equity < $25,000, max 3 day trades per 5 rolling days if equity < 25000: if day_trade_count >= 3: logger.error(f"🛑 PDT rule triggered: {day_trade_count} day-trades in rolling 5-day window") @@ -416,7 +848,7 @@ def pdt_allows_new_trade(): return True def get_market_status(): - """Get current market status and next open/close times.""" + # Get current market status and next open/close times. clock = api.get_clock() status = "open" if clock.is_open else "closed" next_event = clock.next_open if not clock.is_open else clock.next_close @@ -429,142 +861,190 @@ def get_market_status(): "timestamp": clock.timestamp } -def dynamic_position_sizing(opening_equity): - """Adjust position size based on market volatility""" - if not VOLATILITY_ADJUSTMENT: - return max(MIN_NOTIONAL, opening_equity * RISK_FRACTION) +def calculate_position_size(equity, stop_loss, entry_price, regime='normal'): + # Calculate position size based on fixed risk per trade + risk_amount = equity * RISK_PER_TRADE - bars = get_recent_bars(SYMBOL, 50) - if bars is None or len(bars) < 20: - return max(MIN_NOTIONAL, opening_equity * RISK_FRACTION) + # Adjust for high volatility regime + if regime == 'high_vol': + risk_amount *= 0.5 + logger.info(f"📊 High volatility - reducing position size by 50%") - # Calculate recent volatility (ATR) - highs = bars['high'] - lows = bars['low'] - closes = bars['close'] - atr = calculate_atr(highs, lows, closes, 14).iloc[-1] - current_price = closes.iloc[-1] + stop_distance = abs(entry_price - stop_loss) + if stop_distance == 0: + return MIN_NOTIONAL - # Volatility adjustment - reduce position size in high volatility - if current_price > 0: - volatility_factor = max(0.5, min(2.0, 1.0 / (atr / current_price * 10))) - else: - volatility_factor = 1.0 + position_size = risk_amount / stop_distance * entry_price - adjusted_notional = opening_equity * RISK_FRACTION * volatility_factor - logger.info(f"📊 Volatility factor: {volatility_factor:.2f}, Adjusted notional: ${adjusted_notional:.2f}") - return max(MIN_NOTIONAL, adjusted_notional) - -def trailing_stop_loss(symbol, entry_price, current_price): - """Implement trailing stop loss""" - if not USE_TRAILING_STOP: - return False - - position_qty = current_position_qty(symbol) - if position_qty == 0: - return False + # Ensure minimum notional + position_size = max(MIN_NOTIONAL, position_size) - # Calculate current P&L - current_pnl = (current_price - entry_price) / entry_price + logger.info(f"💰 Position sizing: Risk=${risk_amount:.2f}, Stop=${stop_distance:.2f}, Size=${position_size:.2f}") - # Set trailing stop at 2% below highest price since entry - if hasattr(trailing_stop_loss, 'highest_price'): - trailing_stop_loss.highest_price = max(trailing_stop_loss.highest_price, current_price) - else: - trailing_stop_loss.highest_price = current_price - - stop_price = trailing_stop_loss.highest_price * 0.98 # 2% trailing stop - - if current_price <= stop_price and current_pnl > -0.01: # Only stop if not already at big loss - logger.info(f"🛑 Trailing stop triggered at ${stop_price:.2f}") - submit_sell(symbol, position_qty) - return True - - return False - -def get_market_trend(): - """Determine overall market trend using SPY""" - try: - spy_bars = api.get_bars("SPY", "30Min", limit=50).df - if len(spy_bars) < 20: - return "neutral" - - spy_closes = spy_bars['close'] - short_trend = calculate_sma(spy_closes, 10).iloc[-1] > calculate_sma(spy_closes, 20).iloc[-1] - medium_trend = calculate_sma(spy_closes, 20).iloc[-1] > calculate_sma(spy_closes, 50).iloc[-1] - - if short_trend and medium_trend: - return "bullish" - elif not short_trend and not medium_trend: - return "bearish" - else: - return "neutral" - except Exception as e: - logger.warning(f"âš ī¸ Could not determine market trend: {e}") - return "neutral" + return position_size def should_trade_based_on_market_hours(): - """Avoid trading during low-volume periods""" + # Avoid trading during low-volume periods if not MARKET_HOURS_FILTER: return True now = datetime.now().time() - # Avoid first/last 30 minutes (high volatility/uncertainty) + # Avoid first 30 minutes market_open = datetime.strptime("09:30", "%H:%M").time() + open_buffer_end = datetime.strptime("10:00", "%H:%M").time() + + # Avoid last 30 minutes market_close = datetime.strptime("16:00", "%H:%M").time() + close_buffer_start = datetime.strptime("15:30", "%H:%M").time() - open_buffer_start = datetime.strptime("10:00", "%H:%M").time() - open_buffer_end = datetime.strptime("15:30", "%H:%M").time() + if now < open_buffer_end: + logger.info("âŗ Waiting for opening volatility to settle (10:00 AM)") + return False - if now < open_buffer_start or now > open_buffer_end: - logger.info("âŗ Waiting for optimal trading hours (10AM-3:30PM)") + if now >= close_buffer_start: + logger.info("âŗ Avoiding late-day trading (after 3:30 PM)") return False return True -def take_profit_check(symbol, entry_price, current_price): - """Implement profit-taking logic""" +def atr_based_trailing_stop(symbol, entry_price, current_price, stop_loss, position_type='long'): + # Implement ATR-based trailing stop loss + if not USE_TRAILING_STOP: + # Just check fixed stop + if position_type == 'long' and current_price <= stop_loss: + return True + elif position_type == 'short' and current_price >= stop_loss: + return True + return False + position_qty = current_position_qty(symbol) if position_qty == 0: return False - profit_pct = (current_price - entry_price) / entry_price + # Get ATR for dynamic stop + bars = get_recent_bars(symbol, 20) + if bars is not None and len(bars) > 14: + atr = calculate_atr(bars['high'], bars['low'], bars['close'], 14).iloc[-1] + trail_distance = atr * ATR_STOP_MULTIPLIER + else: + trail_distance = abs(entry_price - stop_loss) - # Scale out strategy - if profit_pct >= PROFIT_TARGETS[0] and len(PROFIT_TARGETS) > 1: # First target - partial_qty = position_qty // 2 - if partial_qty > 0: - submit_sell(symbol, partial_qty) - logger.info(f"✅ Taking partial profits at {profit_pct:.2%}") + # Update trailing stop + if not hasattr(atr_based_trailing_stop, 'trailing_stop'): + atr_based_trailing_stop.trailing_stop = stop_loss + + if position_type == 'long': + # Update trailing stop as price rises + new_stop = current_price - trail_distance + if new_stop > atr_based_trailing_stop.trailing_stop: + atr_based_trailing_stop.trailing_stop = new_stop + logger.info(f"📈 Trailing stop updated to ${new_stop:.2f}") + + # Check if stop hit + if current_price <= atr_based_trailing_stop.trailing_stop: + logger.info(f"🛑 Trailing stop hit at ${current_price:.2f}") return True - if profit_pct >= PROFIT_TARGETS[-1]: # Final target - submit_sell(symbol, position_qty) - logger.info(f"đŸŽ¯ Full profit taken at {profit_pct:.2%}") - return True + elif position_type == 'short': + # Update trailing stop as price falls + new_stop = current_price + trail_distance + if new_stop < atr_based_trailing_stop.trailing_stop: + atr_based_trailing_stop.trailing_stop = new_stop + logger.info(f"📉 Trailing stop updated to ${new_stop:.2f}") + + # Check if stop hit + if current_price >= atr_based_trailing_stop.trailing_stop: + logger.info(f"🛑 Trailing stop hit at ${current_price:.2f}") + return True + + return False + +def scale_out_profit_taking(symbol, entry_price, current_price, stop_loss, position_type='long'): + # Scale out of position at profit targets + position_qty = current_position_qty(symbol) + if position_qty == 0: + return False + + # Calculate R (risk amount) + risk_distance = abs(entry_price - stop_loss) + + if position_type == 'long': + profit_pct = (current_price - entry_price) / entry_price + profit_in_r = (current_price - entry_price) / risk_distance if risk_distance > 0 else 0 + else: + profit_pct = (entry_price - current_price) / entry_price + profit_in_r = (entry_price - current_price) / risk_distance if risk_distance > 0 else 0 + + # First target: 1.5R - scale out 50% + if profit_in_r >= PROFIT_TARGET_1: + if not hasattr(scale_out_profit_taking, 'target_1_hit'): + scale_out_profit_taking.target_1_hit = True + partial_qty = position_qty // 2 + + if partial_qty > 0: + # Use limit order at current ask/bid + if USE_LIMIT_ORDERS: + limit_price = current_price if position_type == 'long' else current_price + submit_limit_sell(symbol, partial_qty, limit_price) + else: + submit_market_sell(symbol, partial_qty) + + logger.info(f"đŸŽ¯ Target 1 hit ({PROFIT_TARGET_1}R) - Scaled out 50% at ${current_price:.2f}") + + # Move stop to breakeven + atr_based_trailing_stop.trailing_stop = entry_price + logger.info(f"🔒 Stop moved to breakeven: ${entry_price:.2f}") + return True + + # Second target: 3R - close remaining position + if profit_in_r >= PROFIT_TARGET_2: + remaining_qty = current_position_qty(symbol) + if remaining_qty > 0: + if USE_LIMIT_ORDERS: + limit_price = current_price + submit_limit_sell(symbol, remaining_qty, limit_price) + else: + submit_market_sell(symbol, remaining_qty) + + logger.info(f"đŸŽ¯đŸŽ¯ Target 2 hit ({PROFIT_TARGET_2}R) - Full exit at ${current_price:.2f}") + return True return False def get_current_price(symbol): - """Get current price for a symbol""" + # Get current price for a symbol try: - bars = api.get_bars(symbol, "minute", limit=5) - if bars and len(bars) > 0: - return bars[-1].c + bars = api.get_bars(symbol, "1Min", limit=5).df + if len(bars) > 0: + return bars['close'].iloc[-1] else: return 0 except Exception as e: logger.error(f"❌ Failed to get current price for {symbol}: {e}") return 0 +def get_bid_ask(symbol): + # Get current bid/ask prices + try: + quote = api.get_latest_quote(symbol) + return float(quote.bid_price), float(quote.ask_price) + except Exception as e: + logger.warning(f"âš ī¸ Could not get bid/ask: {e}") + current_price = get_current_price(symbol) + return current_price, current_price + # ----------------------------------------------------------------------------- # Main Trading Loop # ----------------------------------------------------------------------------- def main(): - """Main trading function.""" - logger.info("đŸŽ¯ Starting enhanced daytrader.py...") + logger.info("🚀 Starting daytrader.py...") + + # Run enhanced backtest first + if not enhanced_backtest_strategy(): + logger.error("❌ Backtest failed. Exiting...") + return # Display current market status market_info = get_market_status() @@ -584,27 +1064,34 @@ def main(): logger.info(f"💰 Opening equity: ${opening_equity:.2f}") - # Compute per-trade notional - per_trade_notional = dynamic_position_sizing(opening_equity) - logger.info(f"đŸŽ¯ Per-trade notional: ${per_trade_notional:.2f}") - - # Display trading parameters - logger.info(f"âš™ī¸ Trading configuration:") + # Display enhanced trading parameters + logger.info(f"âš™ī¸ ENHANCED trading configuration:") logger.info(f" Symbol: {SYMBOL}") - logger.info(f" Risk per trade: {RISK_FRACTION:.1%}") - logger.info(f" Max drawdown: {MAX_DRAWDOWN:.1%}") - logger.info(f" MA Windows: {SHORT_WINDOW}/{LONG_WINDOW} minutes") - logger.info(f" PDT Rule enforced: {PDT_RULE}") - logger.info(f" Multi-indicator: {MULTI_INDICATOR}") - logger.info(f" Trailing stop: {USE_TRAILING_STOP}") - logger.info(f" Profit targets: {[f'{t:.1%}' for t in PROFIT_TARGETS]}") - logger.info(f" Volatility adjustment: {VOLATILITY_ADJUSTMENT}") - logger.info(f" Market hours filter: {MARKET_HOURS_FILTER}") + logger.info(f" Risk per trade: {RISK_PER_TRADE:.2%} (ATR-based stops)") + logger.info(f" MA Windows: {SHORT_WINDOW}/{LONG_WINDOW} ({'EMA' if USE_EMA else 'SMA'})") + logger.info(f" Poll interval: {POLL_INTERVAL}s ({POLL_INTERVAL//60} min)") + logger.info(f" Signal strength threshold: {MIN_SIGNAL_STRENGTH:.1%}") + logger.info(f" Profit targets: {PROFIT_TARGET_1}R / {PROFIT_TARGET_2}R") + logger.info(f" ATR stop multiplier: {ATR_STOP_MULTIPLIER}x") + logger.info(f" Max hold time: {MAX_HOLD_TIME//60} minutes") + logger.info(f" Limit orders: {USE_LIMIT_ORDERS}") + logger.info(f" Multi-timeframe filter: {MULTIFRAME_FILTER}") + logger.info(f" Regime detection: {REGIME_DETECTION}") # Main trading loop variables trade_count = 0 entry_price = 0 + entry_time = None + stop_loss = 0 position_active = False + position_type = None + total_pnl = 0 + + # Reset function attributes + if hasattr(scale_out_profit_taking, 'target_1_hit'): + delattr(scale_out_profit_taking, 'target_1_hit') + if hasattr(atr_based_trailing_stop, 'trailing_stop'): + delattr(atr_based_trailing_stop, 'trailing_stop') try: while True: @@ -622,16 +1109,9 @@ def main(): logger.error(f"💸 Maximum drawdown exceeded: {drawdown:.2%}. Stopping...") break - # Enhanced market hours filter + # Market hours filter if not should_trade_based_on_market_hours(): - time.sleep(60) - continue - - # Check market trend - market_trend = get_market_trend() - if market_trend == "bearish": - logger.info("📉 Bearish market detected - reducing activity") - time.sleep(POLL_INTERVAL * 2) # Longer wait + time.sleep(POLL_INTERVAL) continue # Check PDT rule @@ -646,54 +1126,115 @@ def main(): time.sleep(POLL_INTERVAL) continue - # Update dynamic position sizing based on current equity - per_trade_notional = dynamic_position_sizing(current_equity) - # Manage existing position if position_active: - # Check profit taking - if take_profit_check(SYMBOL, entry_price, current_price): - position_active = False - trade_count += 1 - time.sleep(POLL_INTERVAL) - continue + # Time-based exit (max hold time) + if entry_time: + time_in_trade = (datetime.now() - entry_time).total_seconds() + if time_in_trade > MAX_HOLD_TIME: + logger.info(f"⏰ Max hold time reached ({MAX_HOLD_TIME//60} min) - exiting position") + qty = current_position_qty(SYMBOL) + if qty > 0: + submit_market_sell(SYMBOL, qty) + position_active = False + trade_count += 1 + # Reset function attributes + if hasattr(scale_out_profit_taking, 'target_1_hit'): + delattr(scale_out_profit_taking, 'target_1_hit') + if hasattr(atr_based_trailing_stop, 'trailing_stop'): + delattr(atr_based_trailing_stop, 'trailing_stop') + time.sleep(POLL_INTERVAL) + continue + + # Check profit targets (scale out strategy) + if scale_out_profit_taking(SYMBOL, entry_price, current_price, stop_loss, position_type): + # Check if fully closed + remaining_qty = current_position_qty(SYMBOL) + if remaining_qty == 0: + position_active = False + trade_pnl = (current_price - entry_price) * 100 # Approximate + total_pnl += trade_pnl + logger.info(f"✅ Position fully closed (Approx PnL: ${trade_pnl:.2f})") + # Reset function attributes + if hasattr(scale_out_profit_taking, 'target_1_hit'): + delattr(scale_out_profit_taking, 'target_1_hit') + if hasattr(atr_based_trailing_stop, 'trailing_stop'): + delattr(atr_based_trailing_stop, 'trailing_stop') + time.sleep(POLL_INTERVAL) + continue # Check trailing stop loss - if trailing_stop_loss(SYMBOL, entry_price, current_price): - position_active = False - trade_count += 1 - time.sleep(POLL_INTERVAL) - continue + if atr_based_trailing_stop(SYMBOL, entry_price, current_price, stop_loss, position_type): + qty = current_position_qty(SYMBOL) + if qty > 0: + submit_market_sell(SYMBOL, qty) + position_active = False + trade_count += 1 + logger.info(f"🛑 Stop loss triggered - position closed") + # Reset function attributes + if hasattr(scale_out_profit_taking, 'target_1_hit'): + delattr(scale_out_profit_taking, 'target_1_hit') + if hasattr(atr_based_trailing_stop, 'trailing_stop'): + delattr(atr_based_trailing_stop, 'trailing_stop') + time.sleep(POLL_INTERVAL) + continue - # Generate trading signal - signal = enhanced_signal_generator(SYMBOL) + # Generate trading signal (ENHANCED) + signal, strength, signal_stop_loss = enhanced_signal_generator(SYMBOL) + + # Get market regime for logging + bars = get_recent_bars(SYMBOL, 50) + if bars is not None: + regime = detect_market_regime(bars) + else: + regime = 'unknown' # Execute trades based on signal - if signal == "buy" and not position_active: + if signal in ['buy', 'sell'] and not position_active: buying_power = fetch_buying_power() - if buying_power >= per_trade_notional: - if submit_buy(SYMBOL, per_trade_notional): + + # Calculate position size based on risk and stop loss + position_size = calculate_position_size(current_equity, signal_stop_loss, current_price, regime) + + if buying_power >= position_size: + # Use limit orders for better execution + if USE_LIMIT_ORDERS and signal == 'buy': + bid, ask = get_bid_ask(SYMBOL) + limit_price = bid # Buy at bid for better fill + execution_price = submit_limit_buy(SYMBOL, position_size, limit_price) + else: + execution_price = submit_market_buy(SYMBOL, position_size) + + if execution_price: trade_count += 1 - entry_price = current_price + entry_price = execution_price + entry_time = datetime.now() + stop_loss = signal_stop_loss position_active = True - logger.info(f"✅ Buy order executed for {SYMBOL} at ${current_price:.2f} (Trade #{trade_count})") + position_type = 'long' if signal == 'buy' else 'short' + + risk_amount = abs(entry_price - stop_loss) / entry_price + logger.info(f"✅ {signal.upper()} order executed") + logger.info(f" Entry: ${entry_price:.2f}, Stop: ${stop_loss:.2f}, Risk: {risk_amount:.2%}") + logger.info(f" Regime: {regime}, Strength: {strength:.2f}, Trade #{trade_count}") + + # Initialize trailing stop + atr_based_trailing_stop.trailing_stop = stop_loss else: - logger.warning(f"âš ī¸ Insufficient buying power: ${buying_power:.2f}") - - elif signal == "sell" and position_active: - qty = current_position_qty(SYMBOL) - if qty > 0: - if submit_sell(SYMBOL, qty): - trade_count += 1 - position_active = False - logger.info(f"✅ Sell order executed for {SYMBOL} at ${current_price:.2f} (Trade #{trade_count})") - else: - logger.info("â„šī¸ No position to sell") + logger.warning(f"âš ī¸ Insufficient buying power: ${buying_power:.2f} < ${position_size:.2f}") # Display current status - position_status = "LONG" if position_active else "FLAT" + position_status = f"{position_type.upper()}" if position_active else "FLAT" current_time = clock.timestamp.strftime("%I:%M:%S %p") - logger.info(f"âąī¸ {current_time} - {position_status} - Waiting {POLL_INTERVAL} seconds...") + hourly_trend = check_multiframe_confluence(SYMBOL) + + status_msg = f"âąī¸ {current_time} | {position_status} | Regime: {regime.upper()}" + if position_active: + pnl_pct = ((current_price - entry_price) / entry_price) * 100 if position_type == 'long' else ((entry_price - current_price) / entry_price) * 100 + status_msg += f" | PnL: {pnl_pct:+.2f}%" + status_msg += f" | H-Trend: {hourly_trend} | Next poll: {POLL_INTERVAL//60}m" + + logger.info(status_msg) time.sleep(POLL_INTERVAL) except KeyboardInterrupt: @@ -706,12 +1247,11 @@ def main(): logger.info("🔚 Script ending. Closing any remaining positions...") close_all_positions() final_equity = fetch_equity() - pnl = final_equity - opening_equity - pnl_pct = (pnl / opening_equity) * 100 if opening_equity > 0 else 0 + session_pnl = final_equity - opening_equity + session_pnl_pct = (session_pnl / opening_equity) * 100 if opening_equity > 0 else 0 logger.info(f"📊 Session summary: {trade_count} trades executed") - logger.info(f"💰 Final equity: ${final_equity:.2f} (PNL: ${pnl:.2f}, {pnl_pct:.2f}%)") - logger.info("✅ daytrader.py finished.") + logger.info(f"💰 Final equity: ${final_equity:.2f} (PNL: ${session_pnl:+.2f}, {session_pnl_pct:+.2f}%)") + logger.info("✅ ENHANCED daytrader.py finished.") if __name__ == "__main__": main() -