Files
Python/daytrader.py
T

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()