1638 lines
62 KiB
Python
1638 lines
62 KiB
Python
#!/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()
|