1258 lines
47 KiB
Python
1258 lines
47 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"
|
|
|
|
# Default 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.75,
|
|
"BACKTEST_DAYS": 90,
|
|
"USE_LIMIT_ORDERS": True,
|
|
"LIMIT_ORDER_TIMEOUT": 60,
|
|
"ADX_THRESHOLD": 20,
|
|
"VOLUME_MULTIPLIER": 1.2,
|
|
"ATR_STOP_MULTIPLIER": 1.5,
|
|
"MAX_HOLD_TIME": 7200,
|
|
"REGIME_DETECTION": True,
|
|
"MULTIFRAME_FILTER": True,
|
|
"BB_WINDOW": 20,
|
|
"BB_STD": 2.0,
|
|
"USE_EMA": True
|
|
}
|
|
|
|
# Load environment variables
|
|
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"])
|
|
|
|
# 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):
|
|
# Calculate Simple Moving Average
|
|
return data.rolling(window=window).mean()
|
|
|
|
def calculate_ema(data, window):
|
|
# Calculate Exponential Moving Average
|
|
return data.ewm(span=window, adjust=False).mean()
|
|
|
|
def calculate_rsi(data, window=14):
|
|
# Calculate Relative Strength Index
|
|
delta = data.diff()
|
|
gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
|
|
loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
|
|
rs = gain / loss
|
|
rsi = 100 - (100 / (1 + rs))
|
|
return rsi
|
|
|
|
def calculate_atr(high, low, close, window=14):
|
|
# Calculate 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):
|
|
# Calculate Average Directional Index (ADX) for trend strength
|
|
# Calculate True Range
|
|
tr1 = high - low
|
|
tr2 = abs(high - close.shift())
|
|
tr3 = abs(low - close.shift())
|
|
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
|
atr = tr.rolling(window=window).mean()
|
|
|
|
# Calculate Directional Movement
|
|
up_move = high - high.shift()
|
|
down_move = low.shift() - low
|
|
|
|
plus_dm = pd.Series(0.0, index=close.index)
|
|
minus_dm = pd.Series(0.0, index=close.index)
|
|
|
|
plus_dm[(up_move > down_move) & (up_move > 0)] = up_move
|
|
minus_dm[(down_move > up_move) & (down_move > 0)] = down_move
|
|
|
|
# Smooth the directional indicators
|
|
plus_di = 100 * (plus_dm.rolling(window=window).mean() / atr)
|
|
minus_di = 100 * (minus_dm.rolling(window=window).mean() / atr)
|
|
|
|
# Calculate DX and ADX
|
|
dx = 100 * abs(plus_di - minus_di) / (plus_di + minus_di)
|
|
adx = dx.rolling(window=window).mean()
|
|
|
|
return adx, plus_di, minus_di
|
|
|
|
def calculate_bollinger_bands(close, window=20, num_std=2):
|
|
# Calculate Bollinger Bands for mean reversion
|
|
if USE_EMA:
|
|
middle = calculate_ema(close, window)
|
|
else:
|
|
middle = calculate_sma(close, window)
|
|
|
|
std = close.rolling(window=window).std()
|
|
upper = middle + (std * num_std)
|
|
lower = middle - (std * num_std)
|
|
|
|
return upper, middle, lower
|
|
|
|
def check_volume_confirmation(bars):
|
|
# Check if current volume exceeds threshold
|
|
if 'volume' not in bars.columns or len(bars) < 20:
|
|
return True # Default to True if no volume data
|
|
|
|
avg_volume = bars['volume'].rolling(window=20).mean().iloc[-1]
|
|
current_volume = bars['volume'].iloc[-1]
|
|
|
|
return current_volume >= (avg_volume * VOLUME_MULTIPLIER)
|
|
|
|
def detect_market_regime(bars):
|
|
# Detect market regime: trending, ranging, high_vol, low_vol
|
|
if len(bars) < 50:
|
|
return 'unknown'
|
|
|
|
closes = bars['close']
|
|
highs = bars['high']
|
|
lows = bars['low']
|
|
|
|
# Calculate ADX for trend strength
|
|
adx, plus_di, minus_di = calculate_adx(highs, lows, closes, 14)
|
|
current_adx = adx.iloc[-1]
|
|
|
|
# Calculate volatility percentile
|
|
atr = calculate_atr(highs, lows, closes, 14)
|
|
current_atr = atr.iloc[-1]
|
|
atr_percentile = (atr <= current_atr).sum() / len(atr) * 100
|
|
|
|
# Determine regime
|
|
if atr_percentile > 70:
|
|
return 'high_vol'
|
|
elif atr_percentile < 30:
|
|
return 'low_vol'
|
|
elif current_adx > ADX_THRESHOLD:
|
|
return 'trend'
|
|
else:
|
|
return 'range'
|
|
|
|
def check_multiframe_confluence(symbol):
|
|
# Check hourly timeframe for trend alignment
|
|
if not MULTIFRAME_FILTER:
|
|
return 'neutral'
|
|
|
|
try:
|
|
# Get hourly data
|
|
hourly_bars = api.get_bars(symbol, "1Hour", limit=50).df
|
|
if len(hourly_bars) < 50:
|
|
return 'neutral'
|
|
|
|
closes = hourly_bars['close']
|
|
|
|
# Calculate hourly EMAs
|
|
if USE_EMA:
|
|
ema_short = calculate_ema(closes, 20)
|
|
ema_long = calculate_ema(closes, 50)
|
|
else:
|
|
ema_short = calculate_sma(closes, 20)
|
|
ema_long = calculate_sma(closes, 50)
|
|
|
|
current_short = ema_short.iloc[-1]
|
|
current_long = ema_long.iloc[-1]
|
|
current_price = closes.iloc[-1]
|
|
|
|
# Determine hourly trend
|
|
if current_short > current_long and current_price > current_short:
|
|
return 'bullish'
|
|
elif current_short < current_long and current_price < current_short:
|
|
return 'bearish'
|
|
else:
|
|
return 'neutral'
|
|
|
|
except Exception as e:
|
|
logger.warning(f"⚠️ Could not check multiframe confluence: {e}")
|
|
return 'neutral'
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Logging Configuration
|
|
# -----------------------------------------------------------------------------
|
|
|
|
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 (hours, minutes, seconds).
|
|
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
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Enhanced Trading Functions
|
|
# -----------------------------------------------------------------------------
|
|
|
|
def enhanced_backtest_strategy():
|
|
# Comprehensive backtest with improved strategy
|
|
logger.info("📊 Running enhanced backtest with improved strategy...")
|
|
|
|
try:
|
|
end_date = datetime.now()
|
|
start_date = end_date - timedelta(days=BACKTEST_DAYS)
|
|
|
|
bars = api.get_bars(SYMBOL, "15Min", start=start_date.isoformat(),
|
|
end=end_date.isoformat()).df
|
|
if len(bars) < 100:
|
|
logger.warning("⚠️ Insufficient data for backtest")
|
|
return True
|
|
|
|
# Enhanced backtest with new strategy
|
|
closes = bars['close']
|
|
highs = bars['high']
|
|
lows = bars['low']
|
|
|
|
# Calculate indicators
|
|
if USE_EMA:
|
|
short_ma = calculate_ema(closes, SHORT_WINDOW)
|
|
long_ma = calculate_ema(closes, LONG_WINDOW)
|
|
else:
|
|
short_ma = calculate_sma(closes, SHORT_WINDOW)
|
|
long_ma = calculate_sma(closes, LONG_WINDOW)
|
|
|
|
rsi = calculate_rsi(closes, 14)
|
|
adx, plus_di, minus_di = calculate_adx(highs, lows, closes, 14)
|
|
atr = calculate_atr(highs, lows, closes, 14)
|
|
upper_bb, middle_bb, lower_bb = calculate_bollinger_bands(closes, BB_WINDOW, BB_STD)
|
|
|
|
# Track performance
|
|
initial_balance = 10000
|
|
balance = initial_balance
|
|
position = 0
|
|
entry_price = 0
|
|
entry_time = None
|
|
stop_loss = 0
|
|
trades = []
|
|
winning_trades = 0
|
|
|
|
for i in range(max(SHORT_WINDOW, LONG_WINDOW, BB_WINDOW, 20), len(bars)):
|
|
current_price = closes.iloc[i]
|
|
current_time = bars.index[i]
|
|
current_adx = adx.iloc[i]
|
|
current_rsi = rsi.iloc[i]
|
|
current_atr = atr.iloc[i]
|
|
|
|
# Determine regime
|
|
regime = 'trend' if current_adx > ADX_THRESHOLD else 'range'
|
|
|
|
# Generate signals based on regime
|
|
if regime == 'trend':
|
|
# Trend-following logic
|
|
ma_signal = 1 if short_ma.iloc[i] > long_ma.iloc[i] else -1
|
|
rsi_signal = 1 if current_rsi < 65 else (-1 if current_rsi > 35 else 0)
|
|
combined_signal = ma_signal + (rsi_signal * 0.3)
|
|
else:
|
|
# Mean reversion logic (Bollinger Bands)
|
|
if current_price <= lower_bb.iloc[i] and current_rsi < 30:
|
|
combined_signal = 1.5 # Strong buy
|
|
elif current_price >= upper_bb.iloc[i] and current_rsi > 70:
|
|
combined_signal = -1.5 # Strong sell
|
|
else:
|
|
combined_signal = 0
|
|
|
|
# Check volume (simplified for backtest)
|
|
volume_ok = True
|
|
|
|
# Enter position
|
|
if position == 0 and abs(combined_signal) >= 1.2 and volume_ok:
|
|
position = 1 if combined_signal > 0 else -1
|
|
entry_price = apply_slippage(current_price, combined_signal > 0)
|
|
entry_time = current_time
|
|
|
|
# Set ATR-based stop loss
|
|
stop_distance = current_atr * ATR_STOP_MULTIPLIER
|
|
if position > 0:
|
|
stop_loss = entry_price - stop_distance
|
|
else:
|
|
stop_loss = entry_price + stop_distance
|
|
|
|
trades.append({
|
|
'entry_price': entry_price,
|
|
'position': position,
|
|
'entry_time': entry_time,
|
|
'stop_loss': stop_loss,
|
|
'regime': regime
|
|
})
|
|
|
|
# Exit position
|
|
elif position != 0:
|
|
exit_triggered = False
|
|
exit_price = None
|
|
exit_reason = None
|
|
|
|
# Stop loss check
|
|
if position > 0 and current_price <= stop_loss:
|
|
exit_triggered = True
|
|
exit_price = apply_slippage(stop_loss, False)
|
|
exit_reason = 'stop_loss'
|
|
elif position < 0 and current_price >= stop_loss:
|
|
exit_triggered = True
|
|
exit_price = apply_slippage(stop_loss, False)
|
|
exit_reason = 'stop_loss'
|
|
|
|
# Time-based exit
|
|
time_in_trade = (current_time - entry_time).total_seconds()
|
|
if time_in_trade > MAX_HOLD_TIME:
|
|
exit_triggered = True
|
|
exit_price = apply_slippage(current_price, False)
|
|
exit_reason = 'time_limit'
|
|
|
|
# Profit target exits
|
|
pnl_pct = (current_price - entry_price) / entry_price * position
|
|
risk_amount = abs(entry_price - stop_loss) / entry_price
|
|
|
|
if pnl_pct >= (risk_amount * PROFIT_TARGET_1):
|
|
exit_triggered = True
|
|
exit_price = apply_slippage(current_price, False)
|
|
exit_reason = 'target_1'
|
|
|
|
# Signal reversal
|
|
exit_signal = -1 if position > 0 else 1
|
|
if (combined_signal * exit_signal) > 0.8:
|
|
exit_triggered = True
|
|
exit_price = apply_slippage(current_price, False)
|
|
exit_reason = 'signal_reversal'
|
|
|
|
if exit_triggered:
|
|
pnl = (exit_price - entry_price) * position
|
|
balance += pnl
|
|
|
|
if pnl > 0:
|
|
winning_trades += 1
|
|
|
|
position = 0
|
|
trades[-1]['exit_price'] = exit_price
|
|
trades[-1]['pnl'] = pnl
|
|
trades[-1]['exit_reason'] = exit_reason
|
|
|
|
# Calculate statistics
|
|
total_trades = len([t for t in trades if 'exit_price' in t])
|
|
win_rate = winning_trades / total_trades if total_trades > 0 else 0
|
|
total_return = (balance - initial_balance) / initial_balance
|
|
|
|
# Calculate additional metrics
|
|
winning_pnl = sum([t['pnl'] for t in trades if 'pnl' in t and t['pnl'] > 0])
|
|
losing_pnl = sum([abs(t['pnl']) for t in trades if 'pnl' in t and t['pnl'] < 0])
|
|
profit_factor = winning_pnl / losing_pnl if losing_pnl > 0 else 0
|
|
|
|
logger.info(f"📈 Enhanced Backtest Results:")
|
|
logger.info(f" Total trades: {total_trades}")
|
|
logger.info(f" Win rate: {win_rate:.1%}")
|
|
logger.info(f" Total return: {total_return:.1%}")
|
|
logger.info(f" Profit factor: {profit_factor:.2f}")
|
|
logger.info(f" Final balance: ${balance:.2f}")
|
|
|
|
if total_trades < 5:
|
|
logger.warning("⚠️ Very few trades generated - consider adjusting parameters")
|
|
return True
|
|
|
|
if win_rate < 0.35:
|
|
logger.warning("⚠️ Low win rate in backtest - strategy may need optimization")
|
|
return True
|
|
|
|
if profit_factor < 1.0:
|
|
logger.warning("⚠️ Profit factor < 1.0 - losing more than winning")
|
|
return True
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.warning(f"⚠️ Backtest failed: {e}")
|
|
return True
|
|
|
|
def enhanced_signal_generator(symbol):
|
|
bars = get_recent_bars(symbol, 100)
|
|
if bars is None or len(bars) < 50:
|
|
return None, 0, 0
|
|
|
|
closes = bars['close']
|
|
highs = bars['high']
|
|
lows = bars['low']
|
|
current_price = closes.iloc[-1]
|
|
|
|
# Calculate indicators
|
|
if USE_EMA:
|
|
short_ma = calculate_ema(closes, SHORT_WINDOW).iloc[-1]
|
|
long_ma = calculate_ema(closes, LONG_WINDOW).iloc[-1]
|
|
else:
|
|
short_ma = calculate_sma(closes, SHORT_WINDOW).iloc[-1]
|
|
long_ma = calculate_sma(closes, LONG_WINDOW).iloc[-1]
|
|
|
|
rsi = calculate_rsi(closes, 14).iloc[-1]
|
|
adx, plus_di, minus_di = calculate_adx(highs, lows, closes, 14)
|
|
current_adx = adx.iloc[-1]
|
|
atr = calculate_atr(highs, lows, closes, 14).iloc[-1]
|
|
upper_bb, middle_bb, lower_bb = calculate_bollinger_bands(closes, BB_WINDOW, BB_STD)
|
|
|
|
# Volume confirmation
|
|
volume_ok = check_volume_confirmation(bars)
|
|
if not volume_ok:
|
|
return None, 0, 0
|
|
|
|
# Multi-timeframe filter
|
|
hourly_trend = check_multiframe_confluence(symbol)
|
|
|
|
# Detect regime
|
|
regime = detect_market_regime(bars)
|
|
|
|
# Avoid low volatility regimes
|
|
if regime == 'low_vol':
|
|
logger.info("📉 Low volatility regime detected - avoiding trade")
|
|
return None, 0, 0
|
|
|
|
# Initialize signal
|
|
signal = None
|
|
signal_strength = 0
|
|
stop_loss = 0
|
|
|
|
# TREND REGIME: Trend-following with pullbacks
|
|
if regime == 'trend':
|
|
if current_adx > ADX_THRESHOLD:
|
|
# Bullish trend with pullback
|
|
if short_ma > long_ma and current_price < short_ma and rsi < 50:
|
|
if hourly_trend in ['bullish', 'neutral']:
|
|
signal = 'buy'
|
|
signal_strength = min(1.0, (current_adx / 40) * 0.7 + 0.3)
|
|
stop_loss = current_price - (atr * ATR_STOP_MULTIPLIER)
|
|
|
|
# Bearish trend with pullback
|
|
elif short_ma < long_ma and current_price > short_ma and rsi > 50:
|
|
if hourly_trend in ['bearish', 'neutral']:
|
|
signal = 'sell'
|
|
signal_strength = min(1.0, (current_adx / 40) * 0.7 + 0.3)
|
|
stop_loss = current_price + (atr * ATR_STOP_MULTIPLIER)
|
|
|
|
# RANGE REGIME: Mean reversion (Bollinger Bands)
|
|
elif regime == 'range':
|
|
bb_width = (upper_bb.iloc[-1] - lower_bb.iloc[-1]) / middle_bb.iloc[-1]
|
|
|
|
# Oversold at lower band
|
|
if current_price <= lower_bb.iloc[- 1] and rsi < 30:
|
|
if hourly_trend != 'bearish':
|
|
signal = 'buy'
|
|
signal_strength = 0.8
|
|
stop_loss = current_price - (atr * ATR_STOP_MULTIPLIER)
|
|
|
|
# Overbought at upper band
|
|
elif current_price >= upper_bb.iloc[-1] and rsi > 70:
|
|
if hourly_trend != 'bullish':
|
|
signal = 'sell'
|
|
signal_strength = 0.8
|
|
stop_loss = current_price + (atr * ATR_STOP_MULTIPLIER)
|
|
|
|
# HIGH VOL REGIME: Reduce position sizing (handled elsewhere)
|
|
elif regime == 'high_vol':
|
|
# Still generate signals but will reduce position size
|
|
if short_ma > long_ma and rsi < 40:
|
|
if hourly_trend in ['bullish', 'neutral']:
|
|
signal = 'buy'
|
|
signal_strength = 0.6
|
|
stop_loss = current_price - (atr * ATR_STOP_MULTIPLIER * 1.5)
|
|
elif short_ma < long_ma and rsi > 60:
|
|
if hourly_trend in ['bearish', 'neutral']:
|
|
signal = 'sell'
|
|
signal_strength = 0.6
|
|
stop_loss = current_price + (atr * ATR_STOP_MULTIPLIER * 1.5)
|
|
|
|
# Check minimum signal strength
|
|
if signal_strength < MIN_SIGNAL_STRENGTH:
|
|
return None, signal_strength, 0
|
|
|
|
return signal, signal_strength, stop_loss
|
|
|
|
def wait_until_market_open():
|
|
# Wait until the market opens.
|
|
clock = api.get_clock()
|
|
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.day_trade_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} - skipping.")
|
|
return False
|
|
|
|
try:
|
|
shares = int(notional / limit_price)
|
|
|
|
if shares == 0:
|
|
logger.warning(f"⚠️ Cannot buy fractional shares with ${notional:.2f}")
|
|
return False
|
|
|
|
order = api.submit_order(
|
|
symbol=symbol,
|
|
qty=shares,
|
|
side="buy",
|
|
type="limit",
|
|
limit_price=round(limit_price, 2),
|
|
time_in_force="gtc"
|
|
)
|
|
|
|
logger.info(f"🟢 LIMIT BUY order submitted: {shares} shares of {symbol} @ ${limit_price:.2f}")
|
|
|
|
# Wait for fill or timeout
|
|
start_time = time.time()
|
|
while (time.time() - start_time) < LIMIT_ORDER_TIMEOUT:
|
|
order_status = api.get_order(order.id)
|
|
if order_status.status == 'filled':
|
|
filled_price = float(order_status.filled_avg_price)
|
|
logger.info(f"✅ Limit buy FILLED @ ${filled_price:.2f}")
|
|
return filled_price
|
|
elif order_status.status in ['cancelled', 'expired', 'rejected']:
|
|
logger.warning(f"⚠️ Limit order {order_status.status}")
|
|
return False
|
|
time.sleep(2)
|
|
|
|
# Timeout - cancel and use market order
|
|
logger.warning("⏱️ Limit order timeout - switching to market order")
|
|
api.cancel_order(order.id)
|
|
return submit_market_buy(symbol, notional)
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to submit limit buy: {e}")
|
|
return False
|
|
|
|
def submit_market_buy(symbol, notional):
|
|
# Submit a market buy order (fallback)
|
|
try:
|
|
current_price = get_current_price(symbol)
|
|
if current_price == 0:
|
|
return False
|
|
|
|
execution_price = apply_slippage(current_price, True)
|
|
shares = int(notional / execution_price)
|
|
|
|
if shares == 0:
|
|
return False
|
|
|
|
api.submit_order(
|
|
symbol=symbol,
|
|
qty=shares,
|
|
side="buy",
|
|
type="market",
|
|
time_in_force="day"
|
|
)
|
|
logger.info(f"🟢 MARKET BUY {shares} shares of {symbol} at ~${execution_price:.2f}")
|
|
return execution_price
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to buy {symbol}: {e}")
|
|
return False
|
|
|
|
def submit_limit_sell(symbol, qty, limit_price):
|
|
# Submit a limit sell order
|
|
try:
|
|
order = api.submit_order(
|
|
symbol=symbol,
|
|
qty=qty,
|
|
side="sell",
|
|
type="limit",
|
|
limit_price=round(limit_price, 2),
|
|
time_in_force="gtc"
|
|
)
|
|
|
|
logger.info(f"🔴 LIMIT SELL order submitted: {qty} shares of {symbol} @ ${limit_price:.2f}")
|
|
|
|
# Wait for fill or timeout
|
|
start_time = time.time()
|
|
while (time.time() - start_time) < LIMIT_ORDER_TIMEOUT:
|
|
order_status = api.get_order(order.id)
|
|
if order_status.status == 'filled':
|
|
filled_price = float(order_status.filled_avg_price)
|
|
logger.info(f"✅ Limit sell FILLED @ ${filled_price:.2f}")
|
|
return filled_price
|
|
elif order_status.status in ['cancelled', 'expired', 'rejected']:
|
|
logger.warning(f"⚠️ Limit order {order_status.status}")
|
|
return False
|
|
time.sleep(2)
|
|
|
|
# Timeout - cancel and use market order
|
|
logger.warning("⏱️ Limit order timeout - switching to market order")
|
|
api.cancel_order(order.id)
|
|
return submit_market_sell(symbol, qty)
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to submit limit sell: {e}")
|
|
return False
|
|
|
|
def submit_market_sell(symbol, qty):
|
|
# Submit a market sell order (fallback)
|
|
try:
|
|
current_price = get_current_price(symbol)
|
|
if current_price == 0:
|
|
return False
|
|
|
|
execution_price = apply_slippage(current_price, False)
|
|
|
|
api.submit_order(
|
|
symbol=symbol,
|
|
qty=qty,
|
|
side="sell",
|
|
type="market",
|
|
time_in_force="day"
|
|
)
|
|
logger.info(f"🔴 MARKET SELL {qty} shares of {symbol} at ~${execution_price:.2f}")
|
|
return execution_price
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to sell {symbol}: {e}")
|
|
return False
|
|
|
|
def close_all_positions():
|
|
# Close all open positions.
|
|
try:
|
|
positions = api.list_positions()
|
|
if not positions:
|
|
logger.info("✅ No open positions to close.")
|
|
return
|
|
|
|
logger.warning("⚠️ Closing all open 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 for {symbol}: {e}")
|
|
return None
|
|
|
|
def current_position_qty(symbol):
|
|
# Get the current position quantity for a symbol.
|
|
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 triggered: {day_trade_count} day-trades in rolling 5-day window")
|
|
return False
|
|
|
|
return True
|
|
|
|
def get_market_status():
|
|
# Get current market status and next open/close times.
|
|
clock = api.get_clock()
|
|
status = "open" if clock.is_open else "closed"
|
|
next_event = clock.next_open if not clock.is_open else clock.next_close
|
|
event_type = "open" if not clock.is_open else "close"
|
|
|
|
return {
|
|
"status": status,
|
|
"next_event": next_event,
|
|
"event_type": event_type,
|
|
"timestamp": clock.timestamp
|
|
}
|
|
|
|
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
|
|
|
|
# Adjust for high volatility regime
|
|
if regime == 'high_vol':
|
|
risk_amount *= 0.5
|
|
logger.info(f"📊 High volatility - reducing position size by 50%")
|
|
|
|
stop_distance = abs(entry_price - stop_loss)
|
|
if stop_distance == 0:
|
|
return MIN_NOTIONAL
|
|
|
|
position_size = risk_amount / stop_distance * entry_price
|
|
|
|
# Ensure minimum notional
|
|
position_size = max(MIN_NOTIONAL, position_size)
|
|
|
|
logger.info(f"💰 Position sizing: 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 minutes
|
|
market_open = datetime.strptime("09:30", "%H:%M").time()
|
|
open_buffer_end = datetime.strptime("10:00", "%H:%M").time()
|
|
|
|
# Avoid last 30 minutes
|
|
market_close = datetime.strptime("16:00", "%H:%M").time()
|
|
close_buffer_start = datetime.strptime("15:30", "%H:%M").time()
|
|
|
|
if now < open_buffer_end:
|
|
logger.info("⏳ Waiting for opening volatility to settle (10:00 AM)")
|
|
return False
|
|
|
|
if now >= close_buffer_start:
|
|
logger.info("⏳ Avoiding late-day trading (after 3:30 PM)")
|
|
return False
|
|
|
|
return True
|
|
|
|
def atr_based_trailing_stop(symbol, entry_price, current_price, stop_loss, position_type='long'):
|
|
# Implement ATR-based trailing stop loss
|
|
if not USE_TRAILING_STOP:
|
|
# Just check fixed stop
|
|
if position_type == 'long' and current_price <= stop_loss:
|
|
return True
|
|
elif position_type == 'short' and current_price >= stop_loss:
|
|
return True
|
|
return False
|
|
|
|
position_qty = current_position_qty(symbol)
|
|
if position_qty == 0:
|
|
return False
|
|
|
|
# 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':
|
|
# Update trailing stop as price rises
|
|
new_stop = current_price - trail_distance
|
|
if new_stop > atr_based_trailing_stop.trailing_stop:
|
|
atr_based_trailing_stop.trailing_stop = new_stop
|
|
logger.info(f"📈 Trailing stop updated to ${new_stop:.2f}")
|
|
|
|
# Check if stop hit
|
|
if current_price <= atr_based_trailing_stop.trailing_stop:
|
|
logger.info(f"🛑 Trailing stop hit at ${current_price:.2f}")
|
|
return True
|
|
|
|
elif position_type == 'short':
|
|
# Update trailing stop as price falls
|
|
new_stop = current_price + trail_distance
|
|
if new_stop < atr_based_trailing_stop.trailing_stop:
|
|
atr_based_trailing_stop.trailing_stop = new_stop
|
|
logger.info(f"📉 Trailing stop updated to ${new_stop:.2f}")
|
|
|
|
# Check if stop hit
|
|
if current_price >= atr_based_trailing_stop.trailing_stop:
|
|
logger.info(f"🛑 Trailing stop hit at ${current_price:.2f}")
|
|
return True
|
|
|
|
return False
|
|
|
|
def scale_out_profit_taking(symbol, entry_price, current_price, stop_loss, position_type='long'):
|
|
# Scale out of position at profit targets
|
|
position_qty = current_position_qty(symbol)
|
|
if position_qty == 0:
|
|
return False
|
|
|
|
# Calculate R (risk amount)
|
|
risk_distance = abs(entry_price - stop_loss)
|
|
|
|
if position_type == 'long':
|
|
profit_pct = (current_price - entry_price) / entry_price
|
|
profit_in_r = (current_price - entry_price) / risk_distance if risk_distance > 0 else 0
|
|
else:
|
|
profit_pct = (entry_price - current_price) / entry_price
|
|
profit_in_r = (entry_price - current_price) / risk_distance if risk_distance > 0 else 0
|
|
|
|
# First target: 1.5R - scale out 50%
|
|
if profit_in_r >= PROFIT_TARGET_1:
|
|
if not hasattr(scale_out_profit_taking, 'target_1_hit'):
|
|
scale_out_profit_taking.target_1_hit = True
|
|
partial_qty = position_qty // 2
|
|
|
|
if partial_qty > 0:
|
|
# Use limit order at current ask/bid
|
|
if USE_LIMIT_ORDERS:
|
|
limit_price = current_price if position_type == 'long' else current_price
|
|
submit_limit_sell(symbol, partial_qty, limit_price)
|
|
else:
|
|
submit_market_sell(symbol, partial_qty)
|
|
|
|
logger.info(f"🎯 Target 1 hit ({PROFIT_TARGET_1}R) - Scaled out 50% at ${current_price:.2f}")
|
|
|
|
# Move stop to breakeven
|
|
atr_based_trailing_stop.trailing_stop = entry_price
|
|
logger.info(f"🔒 Stop moved to breakeven: ${entry_price:.2f}")
|
|
return True
|
|
|
|
# Second target: 3R - close remaining position
|
|
if profit_in_r >= PROFIT_TARGET_2:
|
|
remaining_qty = current_position_qty(symbol)
|
|
if remaining_qty > 0:
|
|
if USE_LIMIT_ORDERS:
|
|
limit_price = current_price
|
|
submit_limit_sell(symbol, remaining_qty, limit_price)
|
|
else:
|
|
submit_market_sell(symbol, remaining_qty)
|
|
|
|
logger.info(f"🎯🎯 Target 2 hit ({PROFIT_TARGET_2}R) - Full exit at ${current_price:.2f}")
|
|
return True
|
|
|
|
return False
|
|
|
|
def get_current_price(symbol):
|
|
# Get current price for a symbol
|
|
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 current price for {symbol}: {e}")
|
|
return 0
|
|
|
|
def get_bid_ask(symbol):
|
|
# Get current bid/ask prices
|
|
try:
|
|
quote = api.get_latest_quote(symbol)
|
|
return float(quote.bid_price), float(quote.ask_price)
|
|
except Exception as e:
|
|
logger.warning(f"⚠️ Could not get bid/ask: {e}")
|
|
current_price = get_current_price(symbol)
|
|
return current_price, current_price
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Main Trading Loop
|
|
# -----------------------------------------------------------------------------
|
|
|
|
def main():
|
|
logger.info("🚀 Starting daytrader.py...")
|
|
|
|
# Run enhanced backtest first
|
|
if not enhanced_backtest_strategy():
|
|
logger.error("❌ Backtest failed. Exiting...")
|
|
return
|
|
|
|
# Display current market status
|
|
market_info = get_market_status()
|
|
logger.info(f"🏛️ Market is currently {market_info['status'].upper()}")
|
|
|
|
if market_info['status'] == 'closed':
|
|
logger.info(f"📅 Next market {market_info['event_type']}: {format_market_time(market_info['next_event'])}")
|
|
|
|
# Wait for market to open
|
|
wait_until_market_open()
|
|
|
|
# Record opening equity
|
|
opening_equity = fetch_equity()
|
|
if opening_equity == 0:
|
|
logger.error("💥 No equity available. Exiting...")
|
|
return
|
|
|
|
logger.info(f"💰 Opening equity: ${opening_equity:.2f}")
|
|
|
|
# Display enhanced trading parameters
|
|
logger.info(f"⚙️ ENHANCED trading configuration:")
|
|
logger.info(f" Symbol: {SYMBOL}")
|
|
logger.info(f" Risk per trade: {RISK_PER_TRADE:.2%} (ATR-based stops)")
|
|
logger.info(f" MA Windows: {SHORT_WINDOW}/{LONG_WINDOW} ({'EMA' if USE_EMA else 'SMA'})")
|
|
logger.info(f" Poll interval: {POLL_INTERVAL}s ({POLL_INTERVAL//60} min)")
|
|
logger.info(f" Signal strength threshold: {MIN_SIGNAL_STRENGTH:.1%}")
|
|
logger.info(f" Profit targets: {PROFIT_TARGET_1}R / {PROFIT_TARGET_2}R")
|
|
logger.info(f" ATR stop multiplier: {ATR_STOP_MULTIPLIER}x")
|
|
logger.info(f" Max hold time: {MAX_HOLD_TIME//60} minutes")
|
|
logger.info(f" Limit orders: {USE_LIMIT_ORDERS}")
|
|
logger.info(f" Multi-timeframe filter: {MULTIFRAME_FILTER}")
|
|
logger.info(f" Regime detection: {REGIME_DETECTION}")
|
|
|
|
# Main trading loop variables
|
|
trade_count = 0
|
|
entry_price = 0
|
|
entry_time = None
|
|
stop_loss = 0
|
|
position_active = False
|
|
position_type = None
|
|
total_pnl = 0
|
|
|
|
# Reset function attributes
|
|
if hasattr(scale_out_profit_taking, 'target_1_hit'):
|
|
delattr(scale_out_profit_taking, 'target_1_hit')
|
|
if hasattr(atr_based_trailing_stop, 'trailing_stop'):
|
|
delattr(atr_based_trailing_stop, 'trailing_stop')
|
|
|
|
try:
|
|
while True:
|
|
# Check if market is open
|
|
clock = api.get_clock()
|
|
if not clock.is_open:
|
|
logger.info("❌ Market is closed. Exiting...")
|
|
break
|
|
|
|
# Check equity drop
|
|
current_equity = fetch_equity()
|
|
drawdown = (opening_equity - current_equity) / opening_equity
|
|
|
|
if drawdown > MAX_DRAWDOWN:
|
|
logger.error(f"💸 Maximum drawdown exceeded: {drawdown:.2%}. Stopping...")
|
|
break
|
|
|
|
# Market hours filter
|
|
if not should_trade_based_on_market_hours():
|
|
time.sleep(POLL_INTERVAL)
|
|
continue
|
|
|
|
# Check PDT rule
|
|
if not pdt_allows_new_trade():
|
|
logger.error("🛑 PDT rule violation. Stopping...")
|
|
break
|
|
|
|
# Get current price
|
|
current_price = get_current_price(SYMBOL)
|
|
if current_price == 0:
|
|
logger.warning("⚠️ Could not fetch current price, skipping iteration")
|
|
time.sleep(POLL_INTERVAL)
|
|
continue
|
|
|
|
# Manage existing position
|
|
if position_active:
|
|
# Time-based exit (max hold time)
|
|
if entry_time:
|
|
time_in_trade = (datetime.now() - entry_time).total_seconds()
|
|
if time_in_trade > MAX_HOLD_TIME:
|
|
logger.info(f"⏰ Max hold time reached ({MAX_HOLD_TIME//60} min) - exiting position")
|
|
qty = current_position_qty(SYMBOL)
|
|
if qty > 0:
|
|
submit_market_sell(SYMBOL, qty)
|
|
position_active = False
|
|
trade_count += 1
|
|
# Reset function attributes
|
|
if hasattr(scale_out_profit_taking, 'target_1_hit'):
|
|
delattr(scale_out_profit_taking, 'target_1_hit')
|
|
if hasattr(atr_based_trailing_stop, 'trailing_stop'):
|
|
delattr(atr_based_trailing_stop, 'trailing_stop')
|
|
time.sleep(POLL_INTERVAL)
|
|
continue
|
|
|
|
# Check profit targets (scale out strategy)
|
|
if scale_out_profit_taking(SYMBOL, entry_price, current_price, stop_loss, position_type):
|
|
# Check if fully closed
|
|
remaining_qty = current_position_qty(SYMBOL)
|
|
if remaining_qty == 0:
|
|
position_active = False
|
|
trade_pnl = (current_price - entry_price) * 100 # Approximate
|
|
total_pnl += trade_pnl
|
|
logger.info(f"✅ Position fully closed (Approx PnL: ${trade_pnl:.2f})")
|
|
# Reset function attributes
|
|
if hasattr(scale_out_profit_taking, 'target_1_hit'):
|
|
delattr(scale_out_profit_taking, 'target_1_hit')
|
|
if hasattr(atr_based_trailing_stop, 'trailing_stop'):
|
|
delattr(atr_based_trailing_stop, 'trailing_stop')
|
|
time.sleep(POLL_INTERVAL)
|
|
continue
|
|
|
|
# Check trailing stop loss
|
|
if atr_based_trailing_stop(SYMBOL, entry_price, current_price, stop_loss, position_type):
|
|
qty = current_position_qty(SYMBOL)
|
|
if qty > 0:
|
|
submit_market_sell(SYMBOL, qty)
|
|
position_active = False
|
|
trade_count += 1
|
|
logger.info(f"🛑 Stop loss triggered - position closed")
|
|
# Reset function attributes
|
|
if hasattr(scale_out_profit_taking, 'target_1_hit'):
|
|
delattr(scale_out_profit_taking, 'target_1_hit')
|
|
if hasattr(atr_based_trailing_stop, 'trailing_stop'):
|
|
delattr(atr_based_trailing_stop, 'trailing_stop')
|
|
time.sleep(POLL_INTERVAL)
|
|
continue
|
|
|
|
# Generate trading signal (ENHANCED)
|
|
signal, strength, signal_stop_loss = enhanced_signal_generator(SYMBOL)
|
|
|
|
# Get market regime for logging
|
|
bars = get_recent_bars(SYMBOL, 50)
|
|
if bars is not None:
|
|
regime = detect_market_regime(bars)
|
|
else:
|
|
regime = 'unknown'
|
|
|
|
# Execute trades based on signal
|
|
if signal in ['buy', 'sell'] and not position_active:
|
|
buying_power = fetch_buying_power()
|
|
|
|
# Calculate position size based on risk and stop loss
|
|
position_size = calculate_position_size(current_equity, signal_stop_loss, current_price, regime)
|
|
|
|
if buying_power >= position_size:
|
|
# Use limit orders for better execution
|
|
if USE_LIMIT_ORDERS and signal == 'buy':
|
|
bid, ask = get_bid_ask(SYMBOL)
|
|
limit_price = bid # Buy at bid for better fill
|
|
execution_price = submit_limit_buy(SYMBOL, position_size, limit_price)
|
|
else:
|
|
execution_price = submit_market_buy(SYMBOL, position_size)
|
|
|
|
if execution_price:
|
|
trade_count += 1
|
|
entry_price = 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()} order executed")
|
|
logger.info(f" Entry: ${entry_price:.2f}, Stop: ${stop_loss:.2f}, Risk: {risk_amount:.2%}")
|
|
logger.info(f" Regime: {regime}, Strength: {strength:.2f}, Trade #{trade_count}")
|
|
|
|
# Initialize trailing stop
|
|
atr_based_trailing_stop.trailing_stop = stop_loss
|
|
else:
|
|
logger.warning(f"⚠️ Insufficient buying power: ${buying_power:.2f} < ${position_size:.2f}")
|
|
|
|
# Display current status
|
|
position_status = f"{position_type.upper()}" if position_active else "FLAT"
|
|
current_time = clock.timestamp.strftime("%I:%M:%S %p")
|
|
hourly_trend = check_multiframe_confluence(SYMBOL)
|
|
|
|
status_msg = f"⏱️ {current_time} | {position_status} | Regime: {regime.upper()}"
|
|
if position_active:
|
|
pnl_pct = ((current_price - entry_price) / entry_price) * 100 if position_type == 'long' else ((entry_price - current_price) / entry_price) * 100
|
|
status_msg += f" | PnL: {pnl_pct:+.2f}%"
|
|
status_msg += f" | H-Trend: {hourly_trend} | Next poll: {POLL_INTERVAL//60}m"
|
|
|
|
logger.info(status_msg)
|
|
time.sleep(POLL_INTERVAL)
|
|
|
|
except KeyboardInterrupt:
|
|
logger.info("🛑 Script interrupted by user")
|
|
except Exception as e:
|
|
logger.error(f"💥 Unexpected error: {e}")
|
|
import traceback
|
|
logger.error(traceback.format_exc())
|
|
finally:
|
|
logger.info("🔚 Script ending. Closing any remaining positions...")
|
|
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"📊 Session summary: {trade_count} trades executed")
|
|
logger.info(f"💰 Final equity: ${final_equity:.2f} (PNL: ${session_pnl:+.2f}, {session_pnl_pct:+.2f}%)")
|
|
logger.info("✅ ENHANCED daytrader.py finished.")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|