Files
alpaca-trader/alpaca_trader/engine.py
T

916 lines
41 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import sys
import logging
import json
import time
from datetime import datetime, timedelta
from pathlib import Path
from dotenv import load_dotenv
import pandas as pd
import numpy as np
import pytz
from .api import AlpacaClient
from .indicators import sma, ema, rsi, atr, adx, macd, bollinger
from .filters import check_volume, check_candle_pattern, check_macd_confirmation, check_200_sma_filter, detect_market_regime, get_vix
from .filters import check_multiframe_confluence
from .utils import EASTERN, seconds_to_human_readable
SCRIPT_DIR = Path(__file__).parent
LOG_PATH = SCRIPT_DIR / "trading.log"
DEBUG_LOG_PATH = SCRIPT_DIR / "debug.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__)
debug_logger = logging.getLogger('debug')
debug_logger.setLevel(logging.DEBUG)
debug_handler = logging.FileHandler(DEBUG_LOG_PATH, mode='a')
debug_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
debug_logger.addHandler(debug_handler)
debug_logger.propagate = False
CONFIG_PATH = SCRIPT_DIR / "config.json"
ENV_PATH = SCRIPT_DIR / ".env"
DEFAULT_CONFIG = {
"DEBUG_MODE": True,
"SYMBOL": "SPY",
"BAR_TIMEFRAME": "5Min",
"RISK_PER_TRADE": 0.01,
"SHORT_WINDOW": 10,
"LONG_WINDOW": 30,
"MIN_NOTIONAL": 1.0,
"POLL_INTERVAL": 60,
"MAX_DRAWDOWN": 0.08,
"PDT_RULE": True,
"USE_TRAILING_STOP": True,
"PROFIT_TARGET_1": 2.0,
"PROFIT_TARGET_2": 4.0,
"VOLATILITY_ADJUSTMENT": True,
"MARKET_HOURS_FILTER": False,
"ENABLE_SLIPPAGE": True,
"SLIPPAGE_PCT": 0.0005,
"COMMISSION_PCT": 0.0005,
"MIN_SIGNAL_STRENGTH": 0.3,
"BACKTEST_DAYS": 90,
"USE_LIMIT_ORDERS": False,
"LIMIT_ORDER_TIMEOUT": 60,
"ADX_THRESHOLD": 25,
"VOLUME_MULTIPLIER": 0.5,
"ATR_STOP_MULTIPLIER": 2.0,
"MAX_HOLD_TIME": 3600,
"REGIME_DETECTION": True,
"MULTIFRAME_FILTER": False,
"BB_WINDOW": 20,
"BB_STD": 2.0,
"USE_EMA": True,
"REQUIRE_CANDLE_PATTERN": False,
"USE_PIVOT_POINTS": False,
"VIX_THRESHOLD": 30,
"USE_VIX_FILTER": False,
"USE_FIBONACCI": False,
"MAX_TRADES_PER_DAY": 5,
"SKIP_MONDAYS_FRIDAYS": False,
"USE_200_SMA_FILTER": False,
"REQUIRE_MACD_CONFIRMATION": False,
"MIN_RISK_REWARD": 2.0,
"PULLBACK_PERCENTAGE": 0.382,
"ENABLE_SHORT_SELLING": True,
"RSI_BUY_MAX": 55,
"RSI_SELL_MIN": 45,
"RSI_SELL_MAX": 70,
"RSI_RANGE_OVERSOLD": 30,
"RSI_RANGE_OVERBOUGHT": 70,
"REQUIRE_MA_CROSSOVER": True,
"CROSSOVER_LOOKBACK": 5
}
if not ENV_PATH.exists():
placeholder = (
'APCA_API_KEY_ID="YOUR_REAL_KEY_ID"\n'
'APCA_API_SECRET_KEY="YOUR_REAL_SECRET_KEY"\n'
'APCA_API_BASE_URL="https://paper-api.alpaca.markets"\n'
)
with open(ENV_PATH, "w") as f:
f.write(placeholder)
load_dotenv(ENV_PATH)
logger.warning("⚠️ .env file was missing – a placeholder has been created at:")
logger.warning(f" {ENV_PATH}")
logger.warning(" Edit this file and replace the placeholder values with your real Alpaca API credentials.")
logger.warning(' Example lines to replace:')
logger.warning(' APCA_API_KEY_ID="YOUR_REAL_KEY_ID"')
logger.warning(' APCA_API_SECRET_KEY="YOUR_REAL_SECRET_KEY"')
logger.warning(' After editing, restart the script.')
sys.exit(1)
else:
load_dotenv(ENV_PATH)
if CONFIG_PATH.exists():
try:
with open(CONFIG_PATH, "r") as f:
config = json.load(f)
except json.JSONDecodeError:
print("⚠️ config.json is invalid – recreating with defaults")
config = DEFAULT_CONFIG.copy()
with open(CONFIG_PATH, "w") as f:
json.dump(DEFAULT_CONFIG, f, indent=4)
else:
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}")
DEBUG_MODE = bool(config.get("DEBUG_MODE", False))
SYMBOL = config["SYMBOL"]
BAR_TIMEFRAME = config.get("BAR_TIMEFRAME", "5Min")
RISK_PER_TRADE = float(config["RISK_PER_TRADE"])
SHORT_WINDOW = int(config["SHORT_WINDOW"])
LONG_WINDOW = int(config["LONG_WINDOW"])
if SHORT_WINDOW >= LONG_WINDOW:
logger.error(f"⚠️ Configuration error: SHORT_WINDOW ({SHORT_WINDOW}) must be less than LONG_WINDOW ({LONG_WINDOW})")
sys.exit(1)
try:
test_client = AlpacaClient(
os.getenv("APCA_API_KEY_ID"),
os.getenv("APCA_API_SECRET_KEY"),
os.getenv("APCA_API_BASE_URL", "https://paper-api.alpaca.markets"),
api_version="v2"
)
test_client.get_account()
logger.info("✅ API credentials validated")
except Exception as e:
logger.error(f"⚠️ Invalid API credentials: {e}")
logger.error(" Please check your .env file and ensure your Alpaca API keys are correct")
sys.exit(1)
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"])
ENABLE_SHORT_SELLING = bool(config.get("ENABLE_SHORT_SELLING", False))
RSI_BUY_MAX = float(config.get("RSI_BUY_MAX", 55))
RSI_SELL_MIN = float(config.get("RSI_SELL_MIN", 45))
RSI_SELL_MAX = float(config.get("RSI_SELL_MAX", 70))
RSI_RANGE_OVERSOLD = float(config.get("RSI_RANGE_OVERSOLD", 30))
RSI_RANGE_OVERBOUGHT = float(config.get("RSI_RANGE_OVERBOUGHT", 70))
REQUIRE_MA_CROSSOVER = bool(config.get("REQUIRE_MA_CROSSOVER", True))
CROSSOVER_LOOKBACK = int(config.get("CROSSOVER_LOOKBACK", 5))
api = AlpacaClient(
os.getenv('APCA_API_KEY_ID'),
os.getenv('APCA_API_SECRET_KEY'),
os.getenv('APCA_API_BASE_URL'),
api_version='v2'
)
class SignalState:
def __init__(self):
self.last_bullish_crossover_bar = -999
self.last_bearish_crossover_bar = -999
def reset(self):
self.last_bullish_crossover_bar = -999
self.last_bearish_crossover_bar = -999
signal_state = SignalState()
def debug_print(message):
if DEBUG_MODE:
debug_logger.debug(f"🔎 {message}")
print(f"{datetime.now(EASTERN).strftime('%Y-%m-%d %H:%M:%S,%f')[:-3]} - DEBUG - 🔎 {message}", flush=True)
def fetch_equity():
debug_print("Fetching account equity")
account = api.get_account()
equity = float(account.equity)
debug_print(f"Current equity: ${equity:.2f}")
return equity
def fetch_buying_power():
debug_print("Fetching buying power")
account = api.get_account()
bp = float(account.buying_power)
debug_print(f"Buying power: ${bp:.2f}")
return bp
def get_recent_bars(symbol, limit=100):
debug_print(f"Fetching {limit} bars for {symbol} ({BAR_TIMEFRAME})")
try:
bars = api.get_bars(symbol, BAR_TIMEFRAME, limit=limit)
if bars is None or len(bars) == 0:
debug_print(f"No bars returned for {symbol}")
return None
debug_print(f"Retrieved {len(bars)} bars")
return bars
except Exception as e:
logger.error(f"Error fetching bars: {e}")
debug_print(f"Error fetching bars: {e}")
return None
def current_position_qty(symbol):
debug_print(f"Checking position for {symbol}")
try:
positions = api.list_positions()
for pos in positions:
if pos.symbol == symbol:
qty = float(pos.qty)
debug_print(f"Found position: {qty} shares")
return qty
debug_print("No position found")
return 0
except Exception as e:
debug_print(f"Error checking position: {e}")
return 0
def close_all_positions():
debug_print("Closing all positions")
try:
api.close_all_positions()
logger.info("✅ All positions closed")
debug_print("All positions closed successfully")
except Exception as e:
logger.error(f"Error closing positions: {e}")
debug_print(f"Error closing positions: {e}")
def get_bid_ask(symbol):
debug_print(f"Getting bid/ask for {symbol}")
try:
quote = api.get_latest_quote(symbol)
bid = float(quote.bid_price)
ask = float(quote.ask_price)
debug_print(f"Bid: ${bid:.2f}, Ask: ${ask:.2f}")
return bid, ask
except Exception as e:
logger.error(f"Error getting quote: {e}")
debug_print(f"Error getting quote: {e}")
return None, None
def submit_market_buy(symbol, position_size):
debug_print(f"Submitting market buy order: {symbol}, size=${position_size:.2f}")
try:
execution_price = api.place_order(symbol, "buy", position_size, None, LIMIT_ORDER_TIMEOUT)
if execution_price:
logger.info(f"🟢 BUY {symbol} @ ${execution_price:.2f}")
debug_print(f"Buy order filled @ ${execution_price:.2f}")
return execution_price
except Exception as e:
logger.error(f"Buy order failed: {e}")
debug_print(f"Buy order failed: {e}")
return None
def submit_market_sell(symbol, qty):
debug_print(f"Submitting market sell order: {symbol}, qty={qty}")
try:
shares = int(qty)
order = api.submit_order(symbol=symbol, qty=shares, side="sell", type="market", time_in_force="day")
status = api.get_order(order.id)
while status.status not in {"filled", "cancelled", "expired", "rejected"}:
time.sleep(0.5)
status = api.get_order(order.id)
if status.status == "filled":
price = float(status.filled_avg_price)
logger.info(f"🔴 SELL {symbol} @ ${price:.2f}")
debug_print(f"Sell order filled @ ${price:.2f}")
return price
except Exception as e:
logger.error(f"Sell order failed: {e}")
debug_print(f"Sell order failed: {e}")
return None
def submit_limit_buy(symbol, position_size, limit_price):
debug_print(f"Submitting limit buy: {symbol}, size=${position_size:.2f}, limit=${limit_price:.2f}")
try:
execution_price = api.place_order(symbol, "buy", position_size, limit_price, LIMIT_ORDER_TIMEOUT)
if execution_price:
logger.info(f"🟢 BUY {symbol} @ ${execution_price:.2f}")
debug_print(f"Limit buy filled @ ${execution_price:.2f}")
else:
debug_print("Limit order timeout, attempting market order")
execution_price = api.place_order(symbol, "buy", position_size, None, LIMIT_ORDER_TIMEOUT)
if execution_price:
logger.info(f"🟢 BUY {symbol} @ ${execution_price:.2f} (market)")
debug_print(f"Market buy filled @ ${execution_price:.2f}")
return execution_price
except Exception as e:
logger.error(f"Buy order failed: {e}")
debug_print(f"Buy order failed: {e}")
return None
def submit_short_sell(symbol, position_size):
debug_print(f"Submitting short sell: {symbol}, size=${position_size:.2f}")
try:
execution_price = api.place_order(symbol, "sell", position_size, None, LIMIT_ORDER_TIMEOUT)
if execution_price:
logger.info(f"🔴 SHORT {symbol} @ ${execution_price:.2f}")
debug_print(f"Short sell filled @ ${execution_price:.2f}")
return execution_price
except Exception as e:
logger.error(f"Short sell failed: {e}")
debug_print(f"Short sell failed: {e}")
return None
def submit_limit_short_sell(symbol, position_size, limit_price):
debug_print(f"Submitting limit short: {symbol}, size=${position_size:.2f}, limit=${limit_price:.2f}")
try:
execution_price = api.place_order(symbol, "sell", position_size, limit_price, LIMIT_ORDER_TIMEOUT)
if execution_price:
logger.info(f"🔴 SHORT {symbol} @ ${execution_price:.2f}")
debug_print(f"Limit short filled @ ${execution_price:.2f}")
else:
debug_print("Limit order timeout, attempting market order")
execution_price = api.place_order(symbol, "sell", position_size, None, LIMIT_ORDER_TIMEOUT)
if execution_price:
logger.info(f"🔴 SHORT {symbol} @ ${execution_price:.2f} (market)")
debug_print(f"Market short filled @ ${execution_price:.2f}")
return execution_price
except Exception as e:
logger.error(f"Short sell failed: {e}")
debug_print(f"Short sell failed: {e}")
return None
def submit_buy_to_cover(symbol, qty):
debug_print(f"Submitting buy to cover: {symbol}, qty={qty}")
try:
shares = int(qty)
order = api.submit_order(symbol=symbol, qty=shares, side="buy", type="market", time_in_force="day")
status = api.get_order(order.id)
while status.status not in {"filled", "cancelled", "expired", "rejected"}:
time.sleep(0.5)
status = api.get_order(order.id)
if status.status == "filled":
price = float(status.filled_avg_price)
logger.info(f"🟢 COVER {symbol} @ ${price:.2f}")
debug_print(f"Buy to cover filled @ ${price:.2f}")
return price
except Exception as e:
logger.error(f"Buy to cover failed: {e}")
debug_print(f"Buy to cover failed: {e}")
return None
def calculate_position_size(equity, stop_loss, current_price):
debug_print(f"Calculating position size: equity=${equity:.2f}, stop=${stop_loss:.2f}, price=${current_price:.2f}")
risk_amount = equity * RISK_PER_TRADE
price_risk = abs(current_price - stop_loss)
if price_risk == 0:
debug_print("Price risk is zero, returning MIN_NOTIONAL")
return MIN_NOTIONAL
shares = risk_amount / price_risk
position_value = shares * current_price
max_position = equity * 0.25
if position_value > max_position:
position_value = max_position
debug_print(f"Position capped at 25% equity: ${position_value:.2f}")
if position_value < MIN_NOTIONAL:
position_value = MIN_NOTIONAL
debug_print(f"Position set to minimum: ${position_value:.2f}")
debug_print(f"Calculated position size: ${position_value:.2f}")
return position_value
def advanced_signal_generator(symbol):
debug_print(f"Generating signal for {symbol}")
bars = get_recent_bars(symbol, 200)
if bars is None or len(bars) < LONG_WINDOW:
debug_print("Insufficient data for signal generation")
return None, 0, 0, None
closes = bars['close']
highs = bars['high']
lows = bars['low']
current_price = closes.iloc[-1]
debug_print("Calculating indicators...")
if USE_EMA:
short_ma_series = ema(closes, SHORT_WINDOW)
long_ma_series = ema(closes, LONG_WINDOW)
short_ma = short_ma_series.iloc[-1]
long_ma = long_ma_series.iloc[-1]
else:
short_ma_series = sma(closes, SHORT_WINDOW)
long_ma_series = sma(closes, LONG_WINDOW)
short_ma = short_ma_series.iloc[-1]
long_ma = long_ma_series.iloc[-1]
bullish_crossover = False
bearish_crossover = False
if REQUIRE_MA_CROSSOVER and len(bars) >= LONG_WINDOW + CROSSOVER_LOOKBACK:
current_bar_index = len(bars) - 1
for i in range(1, CROSSOVER_LOOKBACK + 1):
bar_index = current_bar_index - i
if short_ma_series.iloc[-i-1] <= long_ma_series.iloc[-i-1] and short_ma_series.iloc[-i] > long_ma_series.iloc[-i]:
if bar_index > signal_state.last_bullish_crossover_bar:
bullish_crossover = True
signal_state.last_bullish_crossover_bar = bar_index
debug_print(f"Bullish crossover detected {i} bars ago")
break
for i in range(1, CROSSOVER_LOOKBACK + 1):
bar_index = current_bar_index - i
if short_ma_series.iloc[-i-1] >= long_ma_series.iloc[-i-1] and short_ma_series.iloc[-i] < long_ma_series.iloc[-i]:
if bar_index > signal_state.last_bearish_crossover_bar:
bearish_crossover = True
signal_state.last_bearish_crossover_bar = bar_index
debug_print(f"Bearish crossover detected {i} bars ago")
break
rsi_val = rsi(closes, 14).iloc[-1]
adx_val = adx(highs, lows, closes).iloc[-1]
atr_val = atr(highs, lows, closes).iloc[-1]
upper, middle, lower = bollinger(closes, BB_WINDOW, BB_STD)
debug_print(f"Indicators: MA_short={short_ma:.2f}, MA_long={long_ma:.2f}, RSI={rsi_val:.1f}, ADX={adx_val:.1f}")
vix_level = get_vix(api, SYMBOL, USE_VIX_FILTER)
if USE_VIX_FILTER and vix_level > VIX_THRESHOLD:
debug_print(f"VIX filter triggered: {vix_level:.1f} > {VIX_THRESHOLD}")
return None, 0, 0, None
if not check_volume(bars, VOLUME_MULTIPLIER):
debug_print("Volume filter failed")
return None, 0, 0, None
bullish_pattern, bearish_pattern = check_candle_pattern(bars)
macd_signal = check_macd_confirmation(bars)
multiframe_trend = check_multiframe_confluence(SYMBOL, USE_EMA, api) if MULTIFRAME_FILTER else "neutral"
regime = detect_market_regime(bars, ADX_THRESHOLD) if REGIME_DETECTION else "trend"
debug_print(f"Filters: regime={regime}, multiframe={multiframe_trend}, macd={macd_signal}")
signal = None
strength = 0
stop = 0
position_type = None
if regime == "trend":
if short_ma > long_ma and rsi_val < RSI_BUY_MAX:
if REQUIRE_MA_CROSSOVER and not bullish_crossover:
debug_print("Bullish signal rejected: no recent crossover")
elif REQUIRE_CANDLE_PATTERN and not bullish_pattern:
debug_print("Bullish signal rejected: candle pattern required")
elif REQUIRE_MACD_CONFIRMATION and macd_signal != "bullish":
debug_print("Bullish signal rejected: MACD confirmation required")
else:
signal = "buy"
strength = min(1.0, (adx_val / 40) * 0.7 + 0.3)
stop = current_price - atr_val * ATR_STOP_MULTIPLIER
position_type = "long"
debug_print(f"BUY signal: strength={strength:.2f}, stop=${stop:.2f}")
if short_ma < long_ma and rsi_val < RSI_SELL_MAX:
if REQUIRE_MA_CROSSOVER and not bearish_crossover:
debug_print("Bearish signal rejected: no recent crossover")
elif REQUIRE_CANDLE_PATTERN and not bearish_pattern:
debug_print("Bearish signal rejected: candle pattern required")
elif REQUIRE_MACD_CONFIRMATION and macd_signal != "bearish":
debug_print("Bearish signal rejected: MACD confirmation required")
else:
signal = "sell"
strength = min(1.0, (adx_val / 40) * 0.7 + 0.3)
stop = current_price + atr_val * ATR_STOP_MULTIPLIER
position_type = "short"
debug_print(f"SELL signal: strength={strength:.2f}, stop=${stop:.2f}")
elif regime == "range":
if current_price <= lower.iloc[-1] and rsi_val < RSI_RANGE_OVERSOLD:
if REQUIRE_CANDLE_PATTERN and not bullish_pattern:
debug_print("Range buy rejected: candle pattern required")
else:
signal = "buy"
strength = 0.85
stop = current_price - atr_val * ATR_STOP_MULTIPLIER
position_type = "long"
debug_print(f"Range BUY signal: strength={strength:.2f}, stop=${stop:.2f}")
if current_price >= upper.iloc[-1] and rsi_val > RSI_RANGE_OVERBOUGHT:
if REQUIRE_CANDLE_PATTERN and not bearish_pattern:
debug_print("Range sell rejected: candle pattern required")
else:
signal = "sell"
strength = 0.85
stop = current_price + atr_val * ATR_STOP_MULTIPLIER
position_type = "short"
debug_print(f"Range SELL signal: strength={strength:.2f}, stop=${stop:.2f}")
if strength < MIN_SIGNAL_STRENGTH:
debug_print(f"Signal rejected: strength {strength:.2f} < {MIN_SIGNAL_STRENGTH}")
return None, 0, 0, None
return signal, strength, stop, position_type
def scale_out_profit_taking(symbol, entry_price, current_price, stop_loss, position_type):
debug_print(f"Checking scale out: entry=${entry_price:.2f}, current=${current_price:.2f}")
if position_type == 'long':
profit_pct = ((current_price - entry_price) / entry_price) * 100
else:
profit_pct = ((entry_price - current_price) / entry_price) * 100
risk_pct = abs((entry_price - stop_loss) / entry_price) * 100
target_1_pct = risk_pct * PROFIT_TARGET_1
target_2_pct = risk_pct * PROFIT_TARGET_2
if not hasattr(scale_out_profit_taking, "target_1_hit"):
scale_out_profit_taking.target_1_hit = False
if profit_pct >= target_1_pct and not scale_out_profit_taking.target_1_hit:
qty = current_position_qty(symbol)
if qty != 0:
half_qty = int(qty / 2)
if half_qty > 0:
debug_print(f"Target 1 hit ({target_1_pct:.2f}%), scaling out {half_qty} shares")
if position_type == 'long':
submit_market_sell(symbol, half_qty)
else:
submit_buy_to_cover(symbol, half_qty)
scale_out_profit_taking.target_1_hit = True
logger.info(f"💰 Partial profit @ {profit_pct:.2f}% ({half_qty} shares)")
debug_print(f"Partial profit taken: {half_qty} shares @ {profit_pct:.2f}%")
if profit_pct >= target_2_pct:
qty = current_position_qty(symbol)
if qty != 0:
debug_print(f"Target 2 hit ({target_2_pct:.2f}%), closing remaining {qty} shares")
if position_type == 'long':
submit_market_sell(symbol, qty)
else:
submit_buy_to_cover(symbol, qty)
logger.info(f"💰💰 Full profit @ {profit_pct:.2f}%")
debug_print(f"Full profit target hit: closed @ {profit_pct:.2f}%")
return True
return False
def atr_based_trailing_stop(symbol, entry_price, current_price, initial_stop, position_type):
debug_print(f"Checking trailing stop: entry=${entry_price:.2f}, current=${current_price:.2f}")
if not hasattr(atr_based_trailing_stop, "trailing_stop"):
atr_based_trailing_stop.trailing_stop = initial_stop
debug_print(f"Initialized trailing stop: ${initial_stop:.2f}")
bars = get_recent_bars(symbol, 50)
if bars is None or len(bars) < 14:
debug_print("Insufficient data for ATR calculation")
return False
current_atr = atr(bars['high'], bars['low'], bars['close']).iloc[-1]
if position_type == 'long':
new_stop = current_price - (current_atr * ATR_STOP_MULTIPLIER)
if new_stop > atr_based_trailing_stop.trailing_stop:
debug_print(f"Updating trailing stop: ${atr_based_trailing_stop.trailing_stop:.2f} -> ${new_stop:.2f}")
atr_based_trailing_stop.trailing_stop = new_stop
if current_price <= atr_based_trailing_stop.trailing_stop:
debug_print(f"Long stop hit: ${current_price:.2f} <= ${atr_based_trailing_stop.trailing_stop:.2f}")
return True
else:
new_stop = current_price + (current_atr * ATR_STOP_MULTIPLIER)
if new_stop < atr_based_trailing_stop.trailing_stop:
debug_print(f"Updating trailing stop: ${atr_based_trailing_stop.trailing_stop:.2f} -> ${new_stop:.2f}")
atr_based_trailing_stop.trailing_stop = new_stop
if current_price >= atr_based_trailing_stop.trailing_stop:
debug_print(f"Short stop hit: ${current_price:.2f} >= ${atr_based_trailing_stop.trailing_stop:.2f}")
return True
return False
def main():
logger.info("🚀 Trading engine starting...")
debug_print("Trading engine initialized")
logger.info(f"📊 Symbol: {SYMBOL}, Timeframe: {BAR_TIMEFRAME}")
logger.info(f"⚙️ Risk/Trade: {RISK_PER_TRADE*100:.2f}%, Stop Mult: {ATR_STOP_MULTIPLIER}x")
try:
while True:
try:
clock = api.get_clock()
if not clock.is_open:
next_open = clock.next_open.astimezone(EASTERN)
wait_time = (next_open - datetime.now(EASTERN)).total_seconds()
logger.info(f"🌙 Market closed. Next open: {next_open.strftime('%I:%M %p ET on %A, %B %d')}")
debug_print(f"Market closed, waiting {seconds_to_human_readable(int(wait_time))} until next open")
time.sleep(min(wait_time, 3600))
continue
logger.info("🔔 Market open - session starting")
debug_print("Market open, starting trading session")
opening_equity = fetch_equity()
logger.info(f"💵 Starting equity: ${opening_equity:.2f}")
position_active = False
entry_price = 0
entry_time = None
stop_loss = 0
position_type = None
trade_count = 0
trades_today = 0
total_pnl = 0
signal_state.reset()
try:
existing_position = api.get_position(SYMBOL)
qty = float(existing_position.qty)
if qty != 0:
position_active = True
entry_price = float(existing_position.avg_entry_price)
position_type = 'long' if qty > 0 else 'short'
bars_for_atr = get_recent_bars(SYMBOL, 50)
if bars_for_atr is not None and len(bars_for_atr) >= 14:
atr_val = atr(bars_for_atr['high'], bars_for_atr['low'], bars_for_atr['close']).iloc[-1]
if position_type == 'long':
stop_loss = entry_price - atr_val * ATR_STOP_MULTIPLIER
else:
stop_loss = entry_price + atr_val * ATR_STOP_MULTIPLIER
else:
if position_type == 'long':
stop_loss = entry_price * 0.98
else:
stop_loss = entry_price * 1.02
logger.info(f"🔄 Recovered existing {position_type.upper()} position: {abs(qty)} shares @ ${entry_price:.2f}, stop=${stop_loss:.2f}")
debug_print(f"Position recovered from previous session")
entry_time = datetime.now(EASTERN)
unrealized_plpc = float(existing_position.unrealized_plpc) if hasattr(existing_position, 'unrealized_plpc') else 0
if unrealized_plpc > 0.01:
scale_out_profit_taking.target_1_hit = True
debug_print("Assuming target 1 already hit based on positive P&L")
if USE_TRAILING_STOP:
atr_based_trailing_stop.trailing_stop = stop_loss
except Exception as e:
debug_print(f"No existing position found or error during recovery: {e}")
while clock.is_open:
clock = api.get_clock()
current_equity = fetch_equity()
drawdown = (opening_equity - current_equity) / opening_equity if opening_equity > 0 else 0
if drawdown > MAX_DRAWDOWN:
logger.warning(f"⚠️ Max drawdown reached: {drawdown:.2%}")
debug_print(f"Max drawdown triggered: {drawdown:.2%}")
close_all_positions()
logger.info("🛑 Trading halted for the day")
time.sleep(3600)
break
bars = get_recent_bars(SYMBOL, 10)
if bars is None or len(bars) == 0:
debug_print("No bars available, retrying...")
time.sleep(30)
continue
current_price = bars['close'].iloc[-1]
vix_level = get_vix(api, SYMBOL, USE_VIX_FILTER)
if position_active:
debug_print(f"Managing active position: {position_type}, entry=${entry_price:.2f}")
if MAX_HOLD_TIME > 0 and entry_time:
time_in_trade = (datetime.now(EASTERN) - entry_time).total_seconds()
if time_in_trade > MAX_HOLD_TIME:
logger.info(f"⏰ Max hold time ({MAX_HOLD_TIME//60} min)")
debug_print(f"Max hold time exceeded, closing position")
qty = current_position_qty(SYMBOL)
if qty != 0:
if position_type == 'long':
submit_market_sell(SYMBOL, qty)
else:
submit_buy_to_cover(SYMBOL, abs(qty))
position_active = False
trade_count += 1
try:
delattr(scale_out_profit_taking, "target_1_hit")
except AttributeError:
pass
try:
delattr(atr_based_trailing_stop, "trailing_stop")
except AttributeError:
pass
debug_print(f"Sleeping {seconds_to_human_readable(POLL_INTERVAL)} after exit")
time.sleep(POLL_INTERVAL)
continue
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
if position_type == 'long':
trade_pnl = (current_price - entry_price) * 100
else:
trade_pnl = (entry_price - current_price) * 100
total_pnl += trade_pnl
logger.info(f"✅ Position closed (PnL: ${trade_pnl:.2f})")
debug_print(f"Position fully closed, PnL: ${trade_pnl:.2f}")
try:
delattr(scale_out_profit_taking, "target_1_hit")
except AttributeError:
pass
try:
delattr(atr_based_trailing_stop, "trailing_stop")
except AttributeError:
pass
debug_print(f"Sleeping {seconds_to_human_readable(POLL_INTERVAL)} after exit")
time.sleep(POLL_INTERVAL)
continue
if atr_based_trailing_stop(SYMBOL, entry_price, current_price, stop_loss, position_type):
qty = current_position_qty(SYMBOL)
if qty != 0:
if position_type == 'long':
submit_market_sell(SYMBOL, qty)
else:
submit_buy_to_cover(SYMBOL, abs(qty))
position_active = False
trade_count += 1
logger.info("🛑 Stop hit")
debug_print("Stop hit, position closed")
try:
delattr(scale_out_profit_taking, "target_1_hit")
except AttributeError:
pass
try:
delattr(atr_based_trailing_stop, "trailing_stop")
except AttributeError:
pass
debug_print(f"Sleeping {seconds_to_human_readable(POLL_INTERVAL)} after exit")
time.sleep(POLL_INTERVAL)
continue
if trades_today >= MAX_TRADES_PER_DAY:
logger.info(f"📊 Daily limit ({MAX_TRADES_PER_DAY}) - monitoring only")
debug_print(f"Daily trade limit reached ({trades_today}/{MAX_TRADES_PER_DAY})")
time.sleep(POLL_INTERVAL)
continue
signal, strength, signal_stop_loss, signal_position_type = advanced_signal_generator(SYMBOL)
if signal == 'sell' and not ENABLE_SHORT_SELLING:
debug_print("Short selling disabled, ignoring sell signal")
signal = None
bars = get_recent_bars(SYMBOL, 50)
if bars is not None:
regime = detect_market_regime(bars, ADX_THRESHOLD)
else:
regime = 'unknown'
if signal in ['buy', 'sell'] and not position_active:
debug_print(f"Signal detected: {signal}, executing trade...")
buying_power = fetch_buying_power()
position_size = calculate_position_size(current_equity, signal_stop_loss, current_price)
if buying_power >= position_size:
execution_price = None
if signal == 'buy':
if USE_LIMIT_ORDERS:
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)
elif signal == 'sell':
if USE_LIMIT_ORDERS:
bid, ask = get_bid_ask(SYMBOL)
limit_price = ask
execution_price = submit_limit_short_sell(SYMBOL, position_size, limit_price)
else:
execution_price = submit_short_sell(SYMBOL, position_size)
if execution_price:
trade_count += 1
trades_today += 1
entry_price = execution_price
entry_time = datetime.now(EASTERN)
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" 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})")
debug_print(f"Trade executed: entry=${entry_price:.2f}, stop=${stop_loss:.2f}, regime={regime}")
atr_based_trailing_stop.trailing_stop = stop_loss
debug_print(f"Trailing stop initialized: ${stop_loss:.2f}")
else:
logger.warning(f"⚠️ Insufficient buying power: ${buying_power:.2f} < ${position_size:.2f}")
debug_print(f"Insufficient buying power: ${buying_power:.2f} < ${position_size:.2f}")
position_status = f"{position_type.upper()}" if position_active else "FLAT"
try:
ts = clock.timestamp
if ts.tzinfo is None:
ts = EASTERN.localize(ts)
else:
ts = ts.astimezone(EASTERN)
current_time = ts.strftime("%I:%M:%S %p ET")
except Exception:
current_time = datetime.now(EASTERN).strftime("%I:%M:%S %p ET")
hourly_trend = check_multiframe_confluence(SYMBOL, USE_EMA, api)
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)
debug_print(f"Sleeping {seconds_to_human_readable(POLL_INTERVAL)}...")
time.sleep(POLL_INTERVAL)
logger.info("🔚 Session ending...")
debug_print("Session ending, closing all 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"📊 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...")
debug_print(f"Day complete. Trades: {trade_count}, PnL: ${session_pnl:+.2f}")
time.sleep(3600)
except Exception as e:
logger.error(f"💥 Session error: {e}")
debug_print(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")
debug_print("User interrupt detected")
close_all_positions()
except Exception as e:
logger.error(f"💥 Fatal error: {e}")
debug_print(f"Fatal error: {e}")
import traceback
logger.error(traceback.format_exc())
finally:
logger.info("🔚 Shutdown")
debug_print("Script shutdown")
def run():
return main()
if __name__ == "__main__":
main()