Files
Python/daytrader.py
T

1638 lines
62 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# Description: Day-Trading Script (Alpaca API)
# Usage: python3 daytrader.py
# Author: Justin Oros
# Source: https://github.com/JustinOros
import os
import sys
import time
import logging
import json
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from pathlib import Path
from dotenv import load_dotenv
import alpaca_trade_api as tradeapi
# -----------------------------------------------------------------------------
# Configuration
# -----------------------------------------------------------------------------
# Path configuration
SCRIPT_DIR = Path(__file__).parent
CONFIG_PATH = SCRIPT_DIR / "daytrader.json"
ENV_PATH = SCRIPT_DIR / ".env"
# Advanced configuration
DEFAULT_CONFIG = {
"SYMBOL": "SPY",
"RISK_PER_TRADE": 0.005,
"SHORT_WINDOW": 20,
"LONG_WINDOW": 50,
"MIN_NOTIONAL": 1.0,
"POLL_INTERVAL": 1800,
"MAX_DRAWDOWN": 0.12,
"PDT_RULE": True,
"USE_TRAILING_STOP": True,
"PROFIT_TARGET_1": 1.5,
"PROFIT_TARGET_2": 3.0,
"VOLATILITY_ADJUSTMENT": True,
"MARKET_HOURS_FILTER": True,
"ENABLE_SLIPPAGE": True,
"SLIPPAGE_PCT": 0.0005,
"COMMISSION_PCT": 0.0005,
"MIN_SIGNAL_STRENGTH": 0.85,
"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,
"REQUIRE_CANDLE_PATTERN": True,
"USE_PIVOT_POINTS": True,
"VIX_THRESHOLD": 20,
"USE_VIX_FILTER": True,
"USE_FIBONACCI": True,
"MAX_TRADES_PER_DAY": 2,
"SKIP_MONDAYS_FRIDAYS": True,
"USE_200_SMA_FILTER": True,
"REQUIRE_MACD_CONFIRMATION": True,
"MIN_RISK_REWARD": 2.0,
"PULLBACK_PERCENTAGE": 0.382
}
# Load environment variables
if ENV_PATH.exists():
load_dotenv(ENV_PATH)
else:
# Create placeholder .env file
with open(ENV_PATH, "w") as f:
f.write('APCA_API_KEY_ID="YOUR_API_KEY_HERE"\n')
f.write('APCA_API_SECRET_KEY="YOUR_SECRET_KEY_HERE"\n')
f.write('APCA_API_BASE_URL="https://paper-api.alpaca.markets"\n')
print("⚠️ Created placeholder .env file.")
print(" Please add your Alpaca API keys to .env file")
sys.exit(1)
# Load configuration
if CONFIG_PATH.exists():
with open(CONFIG_PATH, "r") as f:
config = json.load(f)
else:
# Create default config
with open(CONFIG_PATH, "w") as f:
json.dump(DEFAULT_CONFIG, f, indent=4)
config = DEFAULT_CONFIG.copy()
print(f"✅ Created default config file at {CONFIG_PATH}")
# Extract configuration values
SYMBOL = config["SYMBOL"]
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"])
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_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"])
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"])
REQUIRE_CANDLE_PATTERN = bool(config["REQUIRE_CANDLE_PATTERN"])
USE_PIVOT_POINTS = bool(config["USE_PIVOT_POINTS"])
VIX_THRESHOLD = float(config["VIX_THRESHOLD"])
USE_VIX_FILTER = bool(config["USE_VIX_FILTER"])
USE_FIBONACCI = bool(config["USE_FIBONACCI"])
MAX_TRADES_PER_DAY = int(config["MAX_TRADES_PER_DAY"])
SKIP_MONDAYS_FRIDAYS = bool(config["SKIP_MONDAYS_FRIDAYS"])
USE_200_SMA_FILTER = bool(config["USE_200_SMA_FILTER"])
REQUIRE_MACD_CONFIRMATION = bool(config["REQUIRE_MACD_CONFIRMATION"])
MIN_RISK_REWARD = float(config["MIN_RISK_REWARD"])
PULLBACK_PERCENTAGE = float(config["PULLBACK_PERCENTAGE"])
# Initialize Alpaca API
api = tradeapi.REST(
os.getenv('APCA_API_KEY_ID'),
os.getenv('APCA_API_SECRET_KEY'),
os.getenv('APCA_API_BASE_URL'),
api_version='v2'
)
# -----------------------------------------------------------------------------
# Technical Analysis Functions
# -----------------------------------------------------------------------------
def calculate_sma(data, window):
# Simple Moving Average
return data.rolling(window=window).mean()
def calculate_ema(data, window):
# Exponential Moving Average
return data.ewm(span=window, adjust=False).mean()
def calculate_rsi(data, window=14):
# 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()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
return rsi
def calculate_atr(high, low, close, window=14):
# Average True Range
high_low = high - low
high_close_prev = abs(high - close.shift())
low_close_prev = abs(low - close.shift())
true_range = pd.concat([high_low, high_close_prev, low_close_prev], axis=1).max(axis=1)
atr = true_range.rolling(window=window).mean()
return atr
def calculate_adx(high, low, close, window=14):
# Average Directional Index for trend strength
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()
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
plus_di = 100 * (plus_dm.rolling(window=window).mean() / atr)
minus_di = 100 * (minus_dm.rolling(window=window).mean() / atr)
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_macd(close, fast=12, slow=26, signal=9):
# MACD indicator
ema_fast = calculate_ema(close, fast)
ema_slow = calculate_ema(close, slow)
macd_line = ema_fast - ema_slow
signal_line = calculate_ema(macd_line, signal)
histogram = macd_line - signal_line
return macd_line, signal_line, histogram
def calculate_bollinger_bands(close, window=20, num_std=2):
# 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
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']
adx, plus_di, minus_di = calculate_adx(highs, lows, closes, 14)
current_adx = adx.iloc[-1]
atr = calculate_atr(highs, lows, closes, 14)
current_atr = atr.iloc[-1]
atr_percentile = (atr <= current_atr).sum() / len(atr) * 100
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:
hourly_bars = api.get_bars(symbol, "1Hour", limit=50).df
if len(hourly_bars) < 50:
return 'neutral'
closes = hourly_bars['close']
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]
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'
def check_candle_pattern(bars):
# Check for bullish/bearish engulfing patterns
if len(bars) < 2:
return False, False
last = bars.iloc[-1]
prev = bars.iloc[-2]
# Bullish engulfing
bullish_engulfing = (
last['close'] > last['open'] and
prev['close'] < prev['open'] and
last['close'] > prev['open'] and
last['open'] < prev['close']
)
# Bearish engulfing
bearish_engulfing = (
last['close'] < last['open'] and
prev['close'] > prev['open'] and
last['close'] < prev['open'] and
last['open'] > prev['close']
)
return bullish_engulfing, bearish_engulfing
def calculate_pivot_points(symbol):
# Calculate yesterday's pivot points for support/resistance
if not USE_PIVOT_POINTS:
return None, None, None, None, None
try:
yesterday_bars = api.get_bars(symbol, "1Day", limit=2).df
if len(yesterday_bars) < 2:
return None, None, None, None, None
h = yesterday_bars['high'].iloc[-2]
l = yesterday_bars['low'].iloc[-2]
c = yesterday_bars['close'].iloc[-2]
pivot = (h + l + c) / 3
r1 = 2 * pivot - l
r2 = pivot + (h - l)
s1 = 2 * pivot - h
s2 = pivot - (h - l)
return pivot, r1, r2, s1, s2
except Exception as e:
logger.warning(f"⚠️ Could not calculate pivot points: {e}")
return None, None, None, None, None
def calculate_fibonacci_levels(bars, lookback=20):
# Calculate Fibonacci retracement levels
if not USE_FIBONACCI or len(bars) < lookback:
return None, None, None, None, None
recent_bars = bars.tail(lookback)
swing_high = recent_bars['high'].max()
swing_low = recent_bars['low'].min()
diff = swing_high - swing_low
fib_382 = swing_high - (diff * 0.382)
fib_500 = swing_high - (diff * 0.500)
fib_618 = swing_high - (diff * 0.618)
return fib_382, fib_500, fib_618, swing_high, swing_low
def get_vix_level():
# Get current VIX (fear index) level
if not USE_VIX_FILTER:
return 0
try:
vix_bars = api.get_bars("VIX", "1Day", limit=5).df
if len(vix_bars) > 0:
return vix_bars['close'].iloc[-1]
else:
# Estimate from S&P 500 volatility
spy_bars = api.get_bars("SPY", "1Day", limit=20).df
if len(spy_bars) >= 20:
spy_returns = spy_bars['close'].pct_change()
volatility = spy_returns.std() * np.sqrt(252) * 100
return volatility
return 15
except Exception as e:
logger.warning(f"⚠️ Could not get VIX level: {e}")
return 15
def check_200_sma_filter(symbol):
# Check 200-day SMA for major trend direction
if not USE_200_SMA_FILTER:
return 'neutral'
try:
daily_bars = api.get_bars(symbol, "1Day", limit=210).df
if len(daily_bars) < 200:
return 'neutral'
closes = daily_bars['close']
sma_200 = calculate_sma(closes, 200).iloc[-1]
current_price = closes.iloc[-1]
if current_price > sma_200 * 1.01:
return 'bullish'
elif current_price < sma_200 * 0.99:
return 'bearish'
else:
return 'neutral'
except Exception as e:
logger.warning(f"⚠️ Could not check 200 SMA: {e}")
return 'neutral'
def check_macd_confirmation(bars):
# Check MACD for trend confirmation
if not REQUIRE_MACD_CONFIRMATION or len(bars) < 35:
return 'neutral'
closes = bars['close']
macd_line, signal_line, histogram = calculate_macd(closes)
current_macd = macd_line.iloc[-1]
current_signal = signal_line.iloc[-1]
prev_macd = macd_line.iloc[-2]
prev_signal = signal_line.iloc[-2]
# Bullish: MACD crosses above signal
if prev_macd <= prev_signal and current_macd > current_signal:
return 'bullish'
# Bearish: MACD crosses below signal
elif prev_macd >= prev_signal and current_macd < current_signal:
return 'bearish'
# Continuation
elif current_macd > current_signal:
return 'bullish'
elif current_macd < current_signal:
return 'bearish'
return 'neutral'
def should_skip_trading_day():
# Check if today should be skipped (Monday/Friday)
if not SKIP_MONDAYS_FRIDAYS:
return False
today = datetime.now().weekday()
# 0 = Monday, 4 = Friday
if today == 0 or today == 4:
return True
return False
# -----------------------------------------------------------------------------
# Logging Configuration
# -----------------------------------------------------------------------------
LOG_PATH = SCRIPT_DIR / "daytrader.log"
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(LOG_PATH, mode='a'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
# Helper Functions
# -----------------------------------------------------------------------------
def seconds_to_human_readable(seconds):
# Convert seconds to human-readable format
if seconds < 0:
return "0 seconds"
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
time_parts = []
if hours > 0:
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:
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
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
# -----------------------------------------------------------------------------
# Advanced Trading Functions
# -----------------------------------------------------------------------------
def advanced_backtest_strategy():
# Comprehensive backtest with all advanced filters
logger.info("📊 Running advanced backtest with all filters...")
try:
end_date = datetime.now()
start_date = end_date - timedelta(days=BACKTEST_DAYS)
# Format dates as YYYY-MM-DD for Alpaca API
bars = api.get_bars(SYMBOL, "15Min", start=start_date.strftime('%Y-%m-%d'),
end=end_date.strftime('%Y-%m-%d')).df
if len(bars) < 100:
logger.warning("⚠️ Insufficient data for backtest")
return
closes = bars['close']
highs = bars['high']
lows = bars['low']
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)
macd_line, signal_line, histogram = calculate_macd(closes)
# Track performance
initial_balance = 10000
balance = initial_balance
position = 0
entry_price = 0
entry_time = None
stop_loss = 0
trades = []
winning_trades = 0
daily_trades = {}
for i in range(max(SHORT_WINDOW, LONG_WINDOW, BB_WINDOW, 35), len(bars)):
current_price = closes.iloc[i]
current_time = bars.index[i]
current_date = current_time.date()
current_adx = adx.iloc[i]
current_rsi = rsi.iloc[i]
current_atr = atr.iloc[i]
# Check daily trade limit
if current_date not in daily_trades:
daily_trades[current_date] = 0
regime = 'trend' if current_adx > ADX_THRESHOLD else 'range'
macd_signal = 'bullish' if macd_line.iloc[i] > signal_line.iloc[i] else 'bearish'
# Check candle pattern
recent_bars = bars.iloc[max(0, i-1):i+1]
bullish_eng, bearish_eng = check_candle_pattern(recent_bars)
# Generate signals
if regime == 'trend':
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:
if current_price <= lower_bb.iloc[i] and current_rsi < 30:
combined_signal = 1.5
elif current_price >= upper_bb.iloc[i] and current_rsi > 70:
combined_signal = -1.5
else:
combined_signal = 0
# Enter position
if position == 0 and abs(combined_signal) >= 1.2:
if daily_trades[current_date] >= MAX_TRADES_PER_DAY:
continue
if REQUIRE_CANDLE_PATTERN:
if combined_signal > 0 and not bullish_eng:
continue
if combined_signal < 0 and not bearish_eng:
continue
if REQUIRE_MACD_CONFIRMATION:
if combined_signal > 0 and macd_signal != 'bullish':
continue
if combined_signal < 0 and macd_signal != 'bearish':
continue
position = 1 if combined_signal > 0 else -1
entry_price = apply_slippage(current_price, combined_signal > 0)
entry_time = current_time
stop_distance = current_atr * ATR_STOP_MULTIPLIER
if position > 0:
stop_loss = entry_price - stop_distance
else:
stop_loss = entry_price + stop_distance
daily_trades[current_date] += 1
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
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 targets
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
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
avg_win = winning_pnl / winning_trades if winning_trades > 0 else 0
avg_loss = losing_pnl / (total_trades - winning_trades) if (total_trades - winning_trades) > 0 else 0
logger.info(f"📈 Advanced 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" Avg win: ${avg_win:.2f}")
logger.info(f" Avg loss: ${avg_loss:.2f}")
logger.info(f" Final balance: ${balance:.2f}")
if total_trades < 5:
logger.warning("⚠️ Very few trades - filters may be too strict")
if win_rate < 0.45:
logger.warning("⚠️ Win rate below target")
if profit_factor < 1.3:
logger.warning("⚠️ Profit factor < 1.3")
except Exception as e:
error_msg = str(e).lower()
if 'subscription' in error_msg or 'permit' in error_msg:
logger.warning(f"⚠️ Backtest unavailable: Your subscription doesn't permit historical data access")
else:
logger.warning(f"⚠️ Backtest failed: {e}")
def advanced_signal_generator(symbol):
# Advanced signal generation with ALL filters
# Returns: signal ('buy', 'sell', None), strength (0-1), stop_loss_price
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)
# Filters
vix_level = get_vix_level()
if USE_VIX_FILTER and vix_level > VIX_THRESHOLD:
logger.info(f"📉 VIX too high: {vix_level:.1f} > {VIX_THRESHOLD}")
return None, 0, 0
sma_200_trend = check_200_sma_filter(symbol)
if USE_200_SMA_FILTER and sma_200_trend == 'bearish':
logger.info(f"📉 Below 200 SMA - avoiding longs")
volume_ok = check_volume_confirmation(bars)
if not volume_ok:
logger.info(f"📊 Insufficient volume")
return None, 0, 0
bullish_eng, bearish_eng = check_candle_pattern(bars)
macd_signal = check_macd_confirmation(bars)
hourly_trend = check_multiframe_confluence(symbol)
pivot, r1, r2, s1, s2 = calculate_pivot_points(symbol)
fib_382, fib_500, fib_618, swing_high, swing_low = calculate_fibonacci_levels(bars, 20)
regime = detect_market_regime(bars)
if regime == 'low_vol':
logger.info("📉 Low volatility regime")
return None, 0, 0
signal = None
signal_strength = 0
stop_loss = 0
# TREND REGIME
if regime == 'trend':
if current_adx > ADX_THRESHOLD:
# Bullish trend - wait for pullback
if short_ma > long_ma:
pullback_ok = False
if USE_FIBONACCI and fib_382 is not None:
if abs(current_price - fib_382) / current_price < 0.01:
pullback_ok = True
elif current_price < short_ma * 1.005:
pullback_ok = True
if pullback_ok and rsi < 55:
if hourly_trend in ['bullish', 'neutral']:
if REQUIRE_CANDLE_PATTERN and not bullish_eng:
logger.info("❌ No bullish engulfing")
return None, 0, 0
if REQUIRE_MACD_CONFIRMATION and macd_signal != 'bullish':
logger.info("❌ MACD not bullish")
return None, 0, 0
if USE_200_SMA_FILTER and sma_200_trend == 'bearish':
logger.info("❌ Below 200 SMA - no longs")
return None, 0, 0
if USE_PIVOT_POINTS and s1 is not None:
if current_price < s1 * 1.02:
signal = 'buy'
signal_strength = min(1.0, (current_adx / 40) * 0.8 + 0.2)
stop_loss = current_price - (atr * ATR_STOP_MULTIPLIER)
else:
signal = 'buy'
signal_strength = min(1.0, (current_adx / 40) * 0.7 + 0.3)
stop_loss = current_price - (atr * ATR_STOP_MULTIPLIER)
# Bearish trend - wait for pullback
elif short_ma < long_ma:
pullback_ok = False
if USE_FIBONACCI and fib_618 is not None:
if abs(current_price - fib_618) / current_price < 0.01:
pullback_ok = True
elif current_price > short_ma * 0.995:
pullback_ok = True
if pullback_ok and rsi > 45:
if hourly_trend in ['bearish', 'neutral']:
if REQUIRE_CANDLE_PATTERN and not bearish_eng:
logger.info("❌ No bearish engulfing")
return None, 0, 0
if REQUIRE_MACD_CONFIRMATION and macd_signal != 'bearish':
logger.info("❌ MACD not bearish")
return None, 0, 0
if USE_PIVOT_POINTS and r1 is not None:
if current_price > r1 * 0.98:
signal = 'sell'
signal_strength = min(1.0, (current_adx / 40) * 0.8 + 0.2)
stop_loss = current_price + (atr * ATR_STOP_MULTIPLIER)
else:
signal = 'sell'
signal_strength = min(1.0, (current_adx / 40) * 0.7 + 0.3)
stop_loss = current_price + (atr * ATR_STOP_MULTIPLIER)
# RANGE REGIME
elif regime == 'range':
# Oversold at lower band
if current_price <= lower_bb.iloc[-1] and rsi < 30:
if hourly_trend != 'bearish':
if REQUIRE_CANDLE_PATTERN and not bullish_eng:
logger.info("❌ No bullish engulfing in range")
return None, 0, 0
if USE_200_SMA_FILTER and sma_200_trend == 'bearish':
logger.info("❌ Below 200 SMA - no mean reversion longs")
return None, 0, 0
signal = 'buy'
signal_strength = 0.85
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':
if REQUIRE_CANDLE_PATTERN and not bearish_eng:
logger.info("❌ No bearish engulfing in range")
return None, 0, 0
signal = 'sell'
signal_strength = 0.85
stop_loss = current_price + (atr * ATR_STOP_MULTIPLIER)
# HIGH VOL REGIME
elif regime == 'high_vol':
if short_ma > long_ma and rsi < 35:
if hourly_trend == 'bullish':
if REQUIRE_CANDLE_PATTERN and not bullish_eng:
return None, 0, 0
if REQUIRE_MACD_CONFIRMATION and macd_signal != 'bullish':
return None, 0, 0
signal = 'buy'
signal_strength = 0.6
stop_loss = current_price - (atr * ATR_STOP_MULTIPLIER * 1.5)
elif short_ma < long_ma and rsi > 65:
if hourly_trend == 'bearish':
if REQUIRE_CANDLE_PATTERN and not bearish_eng:
return None, 0, 0
if REQUIRE_MACD_CONFIRMATION and macd_signal != 'bearish':
return None, 0, 0
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:
logger.info(f"❌ Signal strength {signal_strength:.2f} < {MIN_SIGNAL_STRENGTH:.2f}")
return None, signal_strength, 0
# Final risk/reward check
if signal and stop_loss != 0:
potential_reward = abs(current_price - stop_loss) * MIN_RISK_REWARD
if USE_PIVOT_POINTS:
if signal == 'buy' and r1 is not None:
actual_reward = r1 - current_price
if actual_reward < potential_reward:
logger.info(f"❌ R:R too low: {actual_reward:.2f} < {potential_reward:.2f}")
return None, signal_strength, 0
elif signal == 'sell' and s1 is not None:
actual_reward = current_price - s1
if actual_reward < potential_reward:
logger.info(f"❌ R:R too low: {actual_reward:.2f} < {potential_reward:.2f}")
return None, signal_strength, 0
return signal, signal_strength, stop_loss
def wait_until_market_open():
# Wait until the market opens
try:
clock = api.get_clock()
except Exception as e:
logger.warning(f"⚠️ Failed to get clock: {e}")
time.sleep(60)
return
now = clock.timestamp
next_open = clock.next_open
if not clock.is_open:
seconds_until_open = (next_open - now).total_seconds()
if seconds_until_open > 0:
readable_time = seconds_to_human_readable(seconds_until_open)
logger.info(f"🕒 Market opens at {format_market_time(next_open)}")
logger.info(f"⏱️ Waiting {readable_time}...")
while seconds_until_open > 0:
sleep_time = min(60, seconds_until_open)
time.sleep(sleep_time)
seconds_until_open -= sleep_time
if sleep_time >= 60:
remaining_readable = seconds_to_human_readable(seconds_until_open)
logger.info(f"⏱️ {remaining_readable} remaining...")
else:
logger.info("✅ Market is open!")
else:
logger.info("✅ Market is open!")
def fetch_equity():
# Fetch the current account equity
try:
account = api.get_account()
return float(account.equity)
except Exception as e:
logger.error(f"❌ Failed to fetch equity: {e}")
return 0.0
def fetch_buying_power():
# Fetch the current buying power
try:
account = api.get_account()
return float(account.buying_power)
except Exception as e:
logger.error(f"❌ Failed to fetch buying power: {e}")
return 0.0
def get_day_trade_count():
# Get the current day trade count
try:
account = api.get_account()
return int(account.daytrade_count)
except Exception as e:
logger.error(f"❌ Failed to fetch day trade count: {e}")
return 0
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}")
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: {shares} shares @ ${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"✅ 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("⏱️ Timeout - switching to market")
api.cancel_order(order.id)
return submit_market_buy(symbol, notional)
except Exception as e:
logger.error(f"❌ Failed 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,
qty=shares,
side="buy",
type="market",
time_in_force="day"
)
logger.info(f"🟢 MARKET BUY: {shares} shares @ ~${execution_price:.2f}")
return execution_price
except Exception as e:
logger.error(f"❌ Failed buy: {e}")
return False
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: {qty} shares @ ${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"✅ 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("⏱️ Timeout - switching to market")
api.cancel_order(order.id)
return submit_market_sell(symbol, qty)
except Exception as e:
logger.error(f"❌ Failed 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,
side="sell",
type="market",
time_in_force="day"
)
logger.info(f"🔴 MARKET SELL: {qty} shares @ ~${execution_price:.2f}")
return execution_price
except Exception as e:
logger.error(f"❌ Failed sell: {e}")
return False
def close_all_positions():
# Close all open positions
try:
positions = api.list_positions()
if not positions:
logger.info("✅ No open positions")
return
logger.warning("⚠️ Closing all positions...")
for pos in positions:
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
try:
timeframe = "15Min"
bars = api.get_bars(symbol, timeframe, limit=limit).df
return bars
except Exception as e:
logger.error(f"❌ Failed to fetch bars: {e}")
return None
def current_position_qty(symbol):
# Get the current position quantity
try:
positions = api.list_positions()
for pos in positions:
if pos.symbol == symbol:
return int(float(pos.qty))
return 0
except Exception as e:
logger.error(f"❌ Failed to fetch positions: {e}")
return 0
def pdt_allows_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()
if equity < 25000:
if day_trade_count >= 3:
logger.error(f"🛑 PDT rule: {day_trade_count} trades in 5-day window")
return False
return True
def get_market_status():
# Get current market status
try:
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
event_type = "open" if not clock.is_open else "close"
return {
"status": status,
"next_event": next_event,
"event_type": event_type,
"timestamp": clock.timestamp
}
except Exception as e:
logger.warning(f"⚠️ Failed to get market status: {e}")
return {
"status": "unknown",
"next_event": None,
"event_type": "unknown",
"timestamp": datetime.now()
}
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
if regime == 'high_vol':
risk_amount *= 0.5
logger.info(f"📊 High vol - reducing position 50%")
stop_distance = abs(entry_price - stop_loss)
if stop_distance == 0:
return MIN_NOTIONAL
position_size = risk_amount / stop_distance * entry_price
position_size = max(MIN_NOTIONAL, position_size)
logger.info(f"💰 Position: Risk=${risk_amount:.2f}, Stop=${stop_distance:.2f}, Size=${position_size:.2f}")
return position_size
def should_trade_based_on_market_hours():
# Avoid trading during low-volume periods
if not MARKET_HOURS_FILTER:
return True
now = datetime.now().time()
# Avoid first 30 min
open_buffer_end = datetime.strptime("10:00", "%H:%M").time()
# Avoid last 30 min
close_buffer_start = datetime.strptime("15:30", "%H:%M").time()
if now < open_buffer_end:
return False
if now >= close_buffer_start:
return False
return True
def atr_based_trailing_stop(symbol, entry_price, current_price, stop_loss, position_type='long'):
# ATR-based trailing stop loss
if not USE_TRAILING_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
# 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)
# Update trailing stop
if not hasattr(atr_based_trailing_stop, 'trailing_stop'):
atr_based_trailing_stop.trailing_stop = stop_loss
if position_type == 'long':
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 → ${new_stop:.2f}")
if current_price <= atr_based_trailing_stop.trailing_stop:
logger.info(f"🛑 Trailing stop hit @ ${current_price:.2f}")
return True
elif position_type == 'short':
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 → ${new_stop:.2f}")
if current_price >= atr_based_trailing_stop.trailing_stop:
logger.info(f"🛑 Trailing stop hit @ ${current_price:.2f}")
return True
return False
def scale_out_profit_taking(symbol, entry_price, current_price, stop_loss, position_type='long'):
# Scale out at profit targets
position_qty = current_position_qty(symbol)
if position_qty == 0:
return False
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
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:
if USE_LIMIT_ORDERS:
limit_price = current_price
submit_limit_sell(symbol, partial_qty, limit_price)
else:
submit_market_sell(symbol, partial_qty)
logger.info(f"🎯 Target 1 ({PROFIT_TARGET_1}R) - 50% out @ ${current_price:.2f}")
# Move stop to breakeven
atr_based_trailing_stop.trailing_stop = entry_price
logger.info(f"🔒 Stop → breakeven: ${entry_price:.2f}")
return True
# Second target: 3R
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 ({PROFIT_TARGET_2}R) - Full exit @ ${current_price:.2f}")
return True
return False
def get_current_price(symbol):
# Get current price for a symbol
try:
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 price: {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 - Continuous Operation
# -----------------------------------------------------------------------------
def main():
# Main trading function - runs continuously 24/7
logger.info("🚀 Starting daytrader.py - continuous operation")
# Validate API connectivity and credentials
logger.info("🔍 Validating API connectivity...")
try:
account = api.get_account()
logger.info(f"✅ API connected successfully")
logger.info(f"✅ Account ID: {account.id}")
logger.info(f"✅ Equity: ${float(account.equity):.2f}")
logger.info(f"✅ Buying Power: ${float(account.buying_power):.2f}")
logger.info(f"✅ Day Trade Count: {int(account.daytrade_count)}")
logger.info(f"✅ Pattern Day Trader: {account.pattern_day_trader}")
# Test market data access
test_bars = api.get_bars(SYMBOL, "1Day", limit=1).df
if len(test_bars) > 0:
logger.info(f"✅ Market data access verified for {SYMBOL}")
else:
logger.warning(f"⚠️ No market data returned for {SYMBOL}")
# Test clock access
clock = api.get_clock()
logger.info(f"✅ Clock access verified - Market is {'OPEN' if clock.is_open else 'CLOSED'}")
except Exception as e:
error_msg = str(e).lower()
logger.error(f"❌ API validation failed")
if 'unauthorized' in error_msg or 'forbidden' in error_msg:
logger.error(f"🔑 Invalid API credentials detected")
logger.error(f"Please update your .env file with valid API keys")
else:
logger.error(f"Error: {e}")
logger.error(f"Please check your .env file and network connection")
return
# Run backtest once at startup (optional - continues if it fails)
try:
advanced_backtest_strategy()
except Exception as e:
logger.warning(f"⚠️ Backtest skipped: {e}")
logger.info(f"ℹ️ Continuing without backtest - this is optional")
# Track daily state
last_reset_date = None
trades_today = 0
try:
while True: # Infinite loop for continuous operation
try:
# Check if we need to reset daily counters
current_date = datetime.now().date()
if last_reset_date != current_date:
trades_today = 0
last_reset_date = current_date
logger.info(f"📅 New day: {current_date}")
# 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')
# Check if should skip today
if should_skip_trading_day():
day_name = datetime.now().strftime("%A")
logger.info(f"📅 Skipping {day_name} - monitoring mode")
time.sleep(3600) # Sleep 1 hour
continue
# Display market status
market_info = get_market_status()
if market_info['status'] == 'closed':
logger.info(f"🏛️ Market closed")
logger.info(f"📅 Next open: {format_market_time(market_info['next_event'])}")
wait_until_market_open()
continue
# Market is open
logger.info(f"🏛️ Market OPEN - starting session")
# Record opening equity
opening_equity = fetch_equity()
if opening_equity == 0:
logger.error("💥 No equity. Waiting 5 min...")
time.sleep(300)
continue
logger.info(f"💰 Opening equity: ${opening_equity:.2f}")
# Get VIX and 200 SMA
vix_level = get_vix_level()
logger.info(f"📊 VIX: {vix_level:.1f}")
sma_200_trend = check_200_sma_filter(SYMBOL)
logger.info(f"📈 200 SMA: {sma_200_trend.upper()}")
# Display config
logger.info(f"⚙️ Config: {SYMBOL}, Risk={RISK_PER_TRADE:.2%}, Trades={trades_today}/{MAX_TRADES_PER_DAY}")
# Session variables
trade_count = 0
entry_price = 0
entry_time = None
stop_loss = 0
position_active = False
position_type = None
total_pnl = 0
# Trading session loop
while True:
# Check market still open
try:
clock = api.get_clock()
if not clock.is_open:
logger.info("❌ Market closed")
break
except Exception as e:
logger.warning(f"⚠️ Clock check failed: {e}")
time.sleep(60)
continue
# Check day changed
if datetime.now().date() != current_date:
logger.info("📅 Day changed - resetting")
break
# Check drawdown
current_equity = fetch_equity()
drawdown = (opening_equity - current_equity) / opening_equity
if drawdown > MAX_DRAWDOWN:
logger.error(f"💸 Max drawdown: {drawdown:.2%}")
break
# Market hours filter
if not should_trade_based_on_market_hours():
time.sleep(300)
continue
# PDT check
if not pdt_allows_new_trade():
logger.error("🛑 PDT violation")
break
# Get current price
current_price = get_current_price(SYMBOL)
if current_price == 0:
logger.warning("⚠️ No price, retrying...")
time.sleep(60)
continue
# Manage existing position
if position_active:
# Time-based exit
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 ({MAX_HOLD_TIME//60} min)")
qty = current_position_qty(SYMBOL)
if qty > 0:
submit_market_sell(SYMBOL, qty)
position_active = False
trade_count += 1
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
# Profit targets
if scale_out_profit_taking(SYMBOL, entry_price, current_price, stop_loss, position_type):
remaining_qty = current_position_qty(SYMBOL)
if remaining_qty == 0:
position_active = False
trade_pnl = (current_price - entry_price) * 100
total_pnl += trade_pnl
logger.info(f"✅ Position closed (PnL: ${trade_pnl:.2f})")
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
# Trailing stop
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 hit")
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
# Daily trade limit
if trades_today >= MAX_TRADES_PER_DAY:
logger.info(f"📊 Daily limit ({MAX_TRADES_PER_DAY}) - monitoring only")
time.sleep(POLL_INTERVAL)
continue
# Generate signal
signal, strength, signal_stop_loss = advanced_signal_generator(SYMBOL)
# Get regime
bars = get_recent_bars(SYMBOL, 50)
if bars is not None:
regime = detect_market_regime(bars)
else:
regime = 'unknown'
# Execute trades
if signal in ['buy', 'sell'] and not position_active:
buying_power = fetch_buying_power()
position_size = calculate_position_size(current_equity, signal_stop_loss, current_price, regime)
if buying_power >= position_size:
# Use limit orders
if USE_LIMIT_ORDERS and signal == 'buy':
bid, ask = get_bid_ask(SYMBOL)
limit_price = bid
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
trades_today += 1
entry_price = execution_price
entry_time = datetime.now()
stop_loss = signal_stop_loss
position_active = True
position_type = 'long' if signal == 'buy' else 'short'
risk_amount = abs(entry_price - stop_loss) / entry_price
logger.info(f"✅ {signal.upper()} 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} ({trades_today}/{MAX_TRADES_PER_DAY})")
# Initialize trailing stop
atr_based_trailing_stop.trailing_stop = stop_loss
else:
logger.warning(f"⚠️ Insufficient buying power: ${buying_power:.2f} < ${position_size:.2f}")
# Status display
position_status = f"{position_type.upper()}" if position_active else "FLAT"
try:
current_time = clock.timestamp.strftime("%I:%M:%S %p")
except:
current_time = datetime.now().strftime("%I:%M:%S %p")
hourly_trend = check_multiframe_confluence(SYMBOL)
status_msg = f"⏱️ {current_time} | {position_status} | {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:{hourly_trend} | VIX:{vix_level:.1f} | {trades_today}/{MAX_TRADES_PER_DAY}"
logger.info(status_msg)
time.sleep(POLL_INTERVAL)
# End of trading day
logger.info("🔚 Session ending...")
close_all_positions()
final_equity = fetch_equity()
session_pnl = final_equity - opening_equity
session_pnl_pct = (session_pnl / opening_equity) * 100 if opening_equity > 0 else 0
logger.info(f"📊 Summary: {trade_count} trades")
logger.info(f"💰 Final: ${final_equity:.2f} (PNL: ${session_pnl:+.2f}, {session_pnl_pct:+.2f}%)")
logger.info("✅ Day complete. Waiting for next session...")
# Sleep before checking again
time.sleep(3600)
except Exception as e:
logger.error(f"💥 Session error: {e}")
import traceback
logger.error(traceback.format_exc())
logger.info("⏳ Waiting 5 min before retry...")
time.sleep(300)
except KeyboardInterrupt:
logger.info("🛑 User interrupt")
close_all_positions()
except Exception as e:
logger.error(f"💥 Fatal error: {e}")
import traceback
logger.error(traceback.format_exc())
finally:
logger.info("🔚 Shutdown")
if __name__ == "__main__":
main()