2026-01-26 18:13:13 -07:00
|
|
|
|
#!/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
|
2026-01-26 18:44:08 -07:00
|
|
|
|
import pandas as pd
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
from datetime import datetime, timedelta
|
2026-01-29 17:56:44 -07:00
|
|
|
|
import pytz
|
2026-01-26 18:13:13 -07:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
import alpaca_trade_api as tradeapi
|
|
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR = Path(__file__).parent
|
|
|
|
|
|
CONFIG_PATH = SCRIPT_DIR / "daytrader.json"
|
|
|
|
|
|
ENV_PATH = SCRIPT_DIR / ".env"
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_CONFIG = {
|
2026-01-28 11:57:33 -07:00
|
|
|
|
"DEBUG_MODE": True,
|
2026-01-26 18:13:13 -07:00
|
|
|
|
"SYMBOL": "SPY",
|
2026-01-28 14:44:33 -07:00
|
|
|
|
"BAR_TIMEFRAME": "5Min",
|
2026-01-26 19:47:58 -07:00
|
|
|
|
"RISK_PER_TRADE": 0.005,
|
|
|
|
|
|
"SHORT_WINDOW": 20,
|
|
|
|
|
|
"LONG_WINDOW": 50,
|
2026-01-26 18:13:13 -07:00
|
|
|
|
"MIN_NOTIONAL": 1.0,
|
2026-01-29 18:04:10 -07:00
|
|
|
|
"POLL_INTERVAL": 120,
|
2026-01-26 19:47:58 -07:00
|
|
|
|
"MAX_DRAWDOWN": 0.12,
|
2026-01-26 18:44:08 -07:00
|
|
|
|
"PDT_RULE": True,
|
|
|
|
|
|
"USE_TRAILING_STOP": True,
|
2026-01-26 19:47:58 -07:00
|
|
|
|
"PROFIT_TARGET_1": 1.5,
|
|
|
|
|
|
"PROFIT_TARGET_2": 3.0,
|
2026-01-26 18:44:08 -07:00
|
|
|
|
"VOLATILITY_ADJUSTMENT": True,
|
2026-01-29 18:04:10 -07:00
|
|
|
|
"MARKET_HOURS_FILTER": False,
|
2026-01-26 19:47:58 -07:00
|
|
|
|
"ENABLE_SLIPPAGE": True,
|
|
|
|
|
|
"SLIPPAGE_PCT": 0.0005,
|
|
|
|
|
|
"COMMISSION_PCT": 0.0005,
|
2026-01-29 18:04:10 -07:00
|
|
|
|
"MIN_SIGNAL_STRENGTH": 0.50,
|
2026-01-26 19:47:58 -07:00
|
|
|
|
"BACKTEST_DAYS": 90,
|
|
|
|
|
|
"USE_LIMIT_ORDERS": True,
|
|
|
|
|
|
"LIMIT_ORDER_TIMEOUT": 60,
|
2026-01-26 20:30:24 -07:00
|
|
|
|
"ADX_THRESHOLD": 20,
|
2026-01-29 18:04:10 -07:00
|
|
|
|
"VOLUME_MULTIPLIER": 0.5,
|
2026-01-26 19:47:58 -07:00
|
|
|
|
"ATR_STOP_MULTIPLIER": 1.5,
|
|
|
|
|
|
"MAX_HOLD_TIME": 7200,
|
|
|
|
|
|
"REGIME_DETECTION": True,
|
2026-01-29 18:04:10 -07:00
|
|
|
|
"MULTIFRAME_FILTER": False,
|
2026-01-26 19:47:58 -07:00
|
|
|
|
"BB_WINDOW": 20,
|
|
|
|
|
|
"BB_STD": 2.0,
|
2026-01-26 20:30:24 -07:00
|
|
|
|
"USE_EMA": True,
|
2026-01-29 18:04:10 -07:00
|
|
|
|
"REQUIRE_CANDLE_PATTERN": False,
|
|
|
|
|
|
"USE_PIVOT_POINTS": False,
|
2026-01-26 20:30:24 -07:00
|
|
|
|
"VIX_THRESHOLD": 20,
|
|
|
|
|
|
"USE_VIX_FILTER": True,
|
2026-01-29 18:04:10 -07:00
|
|
|
|
"USE_FIBONACCI": False,
|
|
|
|
|
|
"MAX_TRADES_PER_DAY": 1000,
|
|
|
|
|
|
"SKIP_MONDAYS_FRIDAYS": False,
|
|
|
|
|
|
"USE_200_SMA_FILTER": False,
|
|
|
|
|
|
"REQUIRE_MACD_CONFIRMATION": False,
|
|
|
|
|
|
"MIN_RISK_REWARD": 1.5,
|
2026-01-30 09:49:14 -07:00
|
|
|
|
"PULLBACK_PERCENTAGE": 0.382,
|
|
|
|
|
|
"ENABLE_SHORT_SELLING": False
|
2026-01-26 18:13:13 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ENV_PATH.exists():
|
|
|
|
|
|
load_dotenv(ENV_PATH)
|
|
|
|
|
|
else:
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
if CONFIG_PATH.exists():
|
|
|
|
|
|
with open(CONFIG_PATH, "r") as f:
|
|
|
|
|
|
config = json.load(f)
|
|
|
|
|
|
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}")
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
DEBUG_MODE = bool(config.get("DEBUG_MODE", False))
|
2026-01-26 18:13:13 -07:00
|
|
|
|
SYMBOL = config["SYMBOL"]
|
2026-01-28 14:44:33 -07:00
|
|
|
|
BAR_TIMEFRAME = config.get("BAR_TIMEFRAME", "5Min")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
RISK_PER_TRADE = float(config["RISK_PER_TRADE"])
|
2026-01-26 18:13:13 -07:00
|
|
|
|
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"])
|
2026-01-26 18:44:08 -07:00
|
|
|
|
USE_TRAILING_STOP = bool(config["USE_TRAILING_STOP"])
|
2026-01-26 19:47:58 -07:00
|
|
|
|
PROFIT_TARGET_1 = float(config["PROFIT_TARGET_1"])
|
|
|
|
|
|
PROFIT_TARGET_2 = float(config["PROFIT_TARGET_2"])
|
2026-01-26 18:44:08 -07:00
|
|
|
|
VOLATILITY_ADJUSTMENT = bool(config["VOLATILITY_ADJUSTMENT"])
|
|
|
|
|
|
MARKET_HOURS_FILTER = bool(config["MARKET_HOURS_FILTER"])
|
2026-01-26 19:47:58 -07:00
|
|
|
|
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"])
|
2026-01-26 20:30:24 -07:00
|
|
|
|
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"])
|
2026-01-30 09:49:14 -07:00
|
|
|
|
ENABLE_SHORT_SELLING = bool(config.get("ENABLE_SHORT_SELLING", False))
|
2026-01-26 18:13:13 -07:00
|
|
|
|
|
2026-01-29 17:56:44 -07:00
|
|
|
|
EASTERN = pytz.timezone('US/Eastern')
|
|
|
|
|
|
|
2026-01-26 18:13:13 -07:00
|
|
|
|
api = tradeapi.REST(
|
|
|
|
|
|
os.getenv('APCA_API_KEY_ID'),
|
|
|
|
|
|
os.getenv('APCA_API_SECRET_KEY'),
|
|
|
|
|
|
os.getenv('APCA_API_BASE_URL'),
|
|
|
|
|
|
api_version='v2'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
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__)
|
|
|
|
|
|
|
|
|
|
|
|
def debug_print(message):
|
|
|
|
|
|
if DEBUG_MODE:
|
|
|
|
|
|
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f')[:-3]
|
|
|
|
|
|
print(f"{timestamp} - DEBUG - 🔎 {message}", flush=True)
|
2026-01-26 18:44:08 -07:00
|
|
|
|
|
|
|
|
|
|
def calculate_sma(data, window):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Calculating SMA with window={window}")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
return data.rolling(window=window).mean()
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_ema(data, window):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Calculating EMA with window={window}")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
return data.ewm(span=window, adjust=False).mean()
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_rsi(data, window=14):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Calculating RSI with window={window}")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
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):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Calculating ATR with window={window}")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
def calculate_adx(high, low, close, window=14):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Calculating ADX with window={window}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
tr1 = high - low
|
|
|
|
|
|
tr2 = abs(high - close.shift())
|
|
|
|
|
|
tr3 = abs(low - close.shift())
|
|
|
|
|
|
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
|
|
|
|
|
atr = tr.rolling(window=window).mean()
|
|
|
|
|
|
|
|
|
|
|
|
up_move = high - high.shift()
|
|
|
|
|
|
down_move = low.shift() - low
|
|
|
|
|
|
|
|
|
|
|
|
plus_dm = pd.Series(0.0, index=close.index)
|
|
|
|
|
|
minus_dm = pd.Series(0.0, index=close.index)
|
|
|
|
|
|
|
|
|
|
|
|
plus_dm[(up_move > down_move) & (up_move > 0)] = up_move
|
|
|
|
|
|
minus_dm[(down_move > up_move) & (down_move > 0)] = down_move
|
|
|
|
|
|
|
|
|
|
|
|
plus_di = 100 * (plus_dm.rolling(window=window).mean() / atr)
|
|
|
|
|
|
minus_di = 100 * (minus_dm.rolling(window=window).mean() / atr)
|
|
|
|
|
|
|
|
|
|
|
|
dx = 100 * abs(plus_di - minus_di) / (plus_di + minus_di)
|
|
|
|
|
|
adx = dx.rolling(window=window).mean()
|
|
|
|
|
|
|
|
|
|
|
|
return adx, plus_di, minus_di
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
def calculate_macd(close, fast=12, slow=26, signal=9):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Calculating MACD (fast={fast}, slow={slow}, signal={signal})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
ema_fast = calculate_ema(close, fast)
|
|
|
|
|
|
ema_slow = calculate_ema(close, slow)
|
|
|
|
|
|
macd_line = ema_fast - ema_slow
|
|
|
|
|
|
signal_line = calculate_ema(macd_line, signal)
|
|
|
|
|
|
histogram = macd_line - signal_line
|
|
|
|
|
|
|
|
|
|
|
|
return macd_line, signal_line, histogram
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
def calculate_bollinger_bands(close, window=20, num_std=2):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Calculating Bollinger Bands (window={window}, std={num_std})")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
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):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Checking volume confirmation...")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if 'volume' not in bars.columns or len(bars) < 20:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Volume check: insufficient data, returning True")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return True
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
avg_volume = bars['volume'].rolling(window=20).mean().iloc[-1]
|
|
|
|
|
|
current_volume = bars['volume'].iloc[-1]
|
2026-01-28 11:57:33 -07:00
|
|
|
|
ratio = current_volume / avg_volume if avg_volume > 0 else 0
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
result = current_volume >= (avg_volume * VOLUME_MULTIPLIER)
|
|
|
|
|
|
debug_print(f"Volume: current={current_volume:.0f}, avg={avg_volume:.0f}, ratio={ratio:.2f}, pass={result}")
|
|
|
|
|
|
return result
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
def detect_market_regime(bars):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Detecting market regime...")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if len(bars) < 50:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Regime: insufficient data, returning 'unknown'")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return 'unknown'
|
|
|
|
|
|
|
|
|
|
|
|
closes = bars['close']
|
|
|
|
|
|
highs = bars['high']
|
|
|
|
|
|
lows = bars['low']
|
|
|
|
|
|
|
|
|
|
|
|
adx, plus_di, minus_di = calculate_adx(highs, lows, closes, 14)
|
|
|
|
|
|
current_adx = adx.iloc[-1]
|
|
|
|
|
|
|
|
|
|
|
|
atr = calculate_atr(highs, lows, closes, 14)
|
|
|
|
|
|
current_atr = atr.iloc[-1]
|
|
|
|
|
|
atr_percentile = (atr <= current_atr).sum() / len(atr) * 100
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Regime indicators: ADX={current_adx:.2f}, ATR_percentile={atr_percentile:.1f}%")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if atr_percentile > 70:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Regime: HIGH_VOL")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return 'high_vol'
|
|
|
|
|
|
elif atr_percentile < 30:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Regime: LOW_VOL")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return 'low_vol'
|
|
|
|
|
|
elif current_adx > ADX_THRESHOLD:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Regime: TREND")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return 'trend'
|
|
|
|
|
|
else:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Regime: RANGE")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return 'range'
|
|
|
|
|
|
|
|
|
|
|
|
def check_multiframe_confluence(symbol):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Checking multiframe confluence for {symbol}...")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if not MULTIFRAME_FILTER:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Multiframe filter disabled, returning 'neutral'")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return 'neutral'
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Fetching hourly bars...")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
hourly_bars = api.get_bars(symbol, "1Hour", limit=50).df
|
|
|
|
|
|
if len(hourly_bars) < 50:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Insufficient hourly data: {len(hourly_bars)} bars")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return 'neutral'
|
|
|
|
|
|
|
|
|
|
|
|
closes = hourly_bars['close']
|
|
|
|
|
|
|
|
|
|
|
|
if USE_EMA:
|
|
|
|
|
|
ema_short = calculate_ema(closes, 20)
|
|
|
|
|
|
ema_long = calculate_ema(closes, 50)
|
|
|
|
|
|
else:
|
|
|
|
|
|
ema_short = calculate_sma(closes, 20)
|
|
|
|
|
|
ema_long = calculate_sma(closes, 50)
|
|
|
|
|
|
|
|
|
|
|
|
current_short = ema_short.iloc[-1]
|
|
|
|
|
|
current_long = ema_long.iloc[-1]
|
|
|
|
|
|
current_price = closes.iloc[-1]
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Hourly: price={current_price:.2f}, short_MA={current_short:.2f}, long_MA={current_long:.2f}")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if current_short > current_long and current_price > current_short:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Multiframe: BULLISH")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return 'bullish'
|
|
|
|
|
|
elif current_short < current_long and current_price < current_short:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Multiframe: BEARISH")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return 'bearish'
|
|
|
|
|
|
else:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Multiframe: NEUTRAL")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return 'neutral'
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"⚠️ Could not check multiframe confluence: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Multiframe check failed: {e}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return 'neutral'
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
def check_candle_pattern(bars):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Checking candle patterns...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if len(bars) < 2:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Candle pattern: insufficient data")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return False, False
|
|
|
|
|
|
|
|
|
|
|
|
last = bars.iloc[-1]
|
|
|
|
|
|
prev = bars.iloc[-2]
|
|
|
|
|
|
|
|
|
|
|
|
bullish_engulfing = (
|
|
|
|
|
|
last['close'] > last['open'] and
|
|
|
|
|
|
prev['close'] < prev['open'] and
|
|
|
|
|
|
last['close'] > prev['open'] and
|
|
|
|
|
|
last['open'] < prev['close']
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
bearish_engulfing = (
|
|
|
|
|
|
last['close'] < last['open'] and
|
|
|
|
|
|
prev['close'] > prev['open'] and
|
|
|
|
|
|
last['close'] < prev['open'] and
|
|
|
|
|
|
last['open'] > prev['close']
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Candle pattern: bullish_engulfing={bullish_engulfing}, bearish_engulfing={bearish_engulfing}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return bullish_engulfing, bearish_engulfing
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_pivot_points(symbol):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Calculating pivot points for {symbol}...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if not USE_PIVOT_POINTS:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Pivot points disabled")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, None, None, None, None
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Fetching yesterday's daily bars...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
yesterday_bars = api.get_bars(symbol, "1Day", limit=2).df
|
|
|
|
|
|
if len(yesterday_bars) < 2:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Insufficient daily data: {len(yesterday_bars)} bars")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, None, None, None, None
|
|
|
|
|
|
|
|
|
|
|
|
h = yesterday_bars['high'].iloc[-2]
|
|
|
|
|
|
l = yesterday_bars['low'].iloc[-2]
|
|
|
|
|
|
c = yesterday_bars['close'].iloc[-2]
|
|
|
|
|
|
|
|
|
|
|
|
pivot = (h + l + c) / 3
|
|
|
|
|
|
r1 = 2 * pivot - l
|
|
|
|
|
|
r2 = pivot + (h - l)
|
|
|
|
|
|
s1 = 2 * pivot - h
|
|
|
|
|
|
s2 = pivot - (h - l)
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Pivots: S2={s2:.2f}, S1={s1:.2f}, P={pivot:.2f}, R1={r1:.2f}, R2={r2:.2f}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return pivot, r1, r2, s1, s2
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"⚠️ Could not calculate pivot points: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Pivot calculation failed: {e}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, None, None, None, None
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_fibonacci_levels(bars, lookback=20):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Calculating Fibonacci levels (lookback={lookback})...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if not USE_FIBONACCI or len(bars) < lookback:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Fibonacci disabled or insufficient data")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, None, None, None, None
|
|
|
|
|
|
|
|
|
|
|
|
recent_bars = bars.tail(lookback)
|
|
|
|
|
|
swing_high = recent_bars['high'].max()
|
|
|
|
|
|
swing_low = recent_bars['low'].min()
|
|
|
|
|
|
|
|
|
|
|
|
diff = swing_high - swing_low
|
|
|
|
|
|
|
|
|
|
|
|
fib_382 = swing_high - (diff * 0.382)
|
|
|
|
|
|
fib_500 = swing_high - (diff * 0.500)
|
|
|
|
|
|
fib_618 = swing_high - (diff * 0.618)
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Fibonacci: high={swing_high:.2f}, low={swing_low:.2f}, 38.2%={fib_382:.2f}, 50%={fib_500:.2f}, 61.8%={fib_618:.2f}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return fib_382, fib_500, fib_618, swing_high, swing_low
|
|
|
|
|
|
|
|
|
|
|
|
def get_vix_level():
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Getting VIX level...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if not USE_VIX_FILTER:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("VIX filter disabled, returning 0")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Fetching VIX data...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
vix_bars = api.get_bars("VIX", "1Day", limit=5).df
|
|
|
|
|
|
if len(vix_bars) > 0:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
vix = vix_bars['close'].iloc[-1]
|
|
|
|
|
|
debug_print(f"VIX from data: {vix:.2f}")
|
|
|
|
|
|
return vix
|
2026-01-26 20:30:24 -07:00
|
|
|
|
else:
|
2026-01-29 12:37:20 -07:00
|
|
|
|
debug_print(f"No VIX data, estimating from {SYMBOL} volatility...")
|
|
|
|
|
|
spy_bars = api.get_bars(SYMBOL, "1Day", limit=20).df
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if len(spy_bars) >= 20:
|
|
|
|
|
|
spy_returns = spy_bars['close'].pct_change()
|
|
|
|
|
|
volatility = spy_returns.std() * np.sqrt(252) * 100
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"VIX estimated: {volatility:.2f}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return volatility
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Returning default VIX: 15")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 15
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"⚠️ Could not get VIX level: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"VIX fetch failed: {e}, returning 15")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 15
|
|
|
|
|
|
|
|
|
|
|
|
def check_200_sma_filter(symbol):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Checking 200 SMA filter for {symbol}...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if not USE_200_SMA_FILTER:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("200 SMA filter disabled")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 'neutral'
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Fetching 210 days of daily bars...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
daily_bars = api.get_bars(symbol, "1Day", limit=210).df
|
|
|
|
|
|
if len(daily_bars) < 200:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Insufficient data for 200 SMA: {len(daily_bars)} bars")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 'neutral'
|
|
|
|
|
|
|
|
|
|
|
|
closes = daily_bars['close']
|
|
|
|
|
|
sma_200 = calculate_sma(closes, 200).iloc[-1]
|
|
|
|
|
|
current_price = closes.iloc[-1]
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"200 SMA: price={current_price:.2f}, SMA={sma_200:.2f}, ratio={current_price/sma_200:.4f}")
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if current_price > sma_200 * 1.01:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("200 SMA: BULLISH")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 'bullish'
|
|
|
|
|
|
elif current_price < sma_200 * 0.99:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("200 SMA: BEARISH")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 'bearish'
|
|
|
|
|
|
else:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("200 SMA: NEUTRAL")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 'neutral'
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"⚠️ Could not check 200 SMA: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"200 SMA check failed: {e}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 'neutral'
|
|
|
|
|
|
|
|
|
|
|
|
def check_macd_confirmation(bars):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Checking MACD confirmation...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if not REQUIRE_MACD_CONFIRMATION or len(bars) < 35:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("MACD confirmation disabled or insufficient data")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 'neutral'
|
|
|
|
|
|
|
|
|
|
|
|
closes = bars['close']
|
|
|
|
|
|
macd_line, signal_line, histogram = calculate_macd(closes)
|
|
|
|
|
|
|
|
|
|
|
|
current_macd = macd_line.iloc[-1]
|
|
|
|
|
|
current_signal = signal_line.iloc[-1]
|
|
|
|
|
|
prev_macd = macd_line.iloc[-2]
|
|
|
|
|
|
prev_signal = signal_line.iloc[-2]
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"MACD: current={current_macd:.4f}, signal={current_signal:.4f}, prev_macd={prev_macd:.4f}, prev_signal={prev_signal:.4f}")
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if prev_macd <= prev_signal and current_macd > current_signal:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("MACD: BULLISH crossover")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 'bullish'
|
|
|
|
|
|
elif prev_macd >= prev_signal and current_macd < current_signal:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("MACD: BEARISH crossover")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 'bearish'
|
|
|
|
|
|
elif current_macd > current_signal:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("MACD: BULLISH continuation")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 'bullish'
|
|
|
|
|
|
elif current_macd < current_signal:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("MACD: BEARISH continuation")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 'bearish'
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("MACD: NEUTRAL")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return 'neutral'
|
|
|
|
|
|
|
|
|
|
|
|
def should_skip_trading_day():
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Checking if should skip trading day...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if not SKIP_MONDAYS_FRIDAYS:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Skip Monday/Friday disabled")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
today = datetime.now().weekday()
|
2026-01-29 17:56:44 -07:00
|
|
|
|
day_name = datetime.now(EASTERN).strftime("%A")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if today == 0 or today == 4:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Skipping {day_name} (skip_mondays_fridays enabled)")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return True
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Not skipping {day_name}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-26 18:13:13 -07:00
|
|
|
|
def seconds_to_human_readable(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 ''}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if secs > 0 and hours == 0:
|
2026-01-26 18:13:13 -07:00
|
|
|
|
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):
|
2026-01-28 15:03:56 -07:00
|
|
|
|
if hasattr(dt_obj, 'to_pydatetime'):
|
|
|
|
|
|
dt_obj = dt_obj.to_pydatetime()
|
2026-01-29 17:56:44 -07:00
|
|
|
|
|
|
|
|
|
|
if dt_obj.tzinfo is None:
|
|
|
|
|
|
dt_obj = EASTERN.localize(dt_obj)
|
|
|
|
|
|
elif dt_obj.tzinfo != EASTERN:
|
|
|
|
|
|
dt_obj = dt_obj.astimezone(EASTERN)
|
|
|
|
|
|
|
|
|
|
|
|
eastern_time = dt_obj.strftime("%Y-%m-%d %I:%M:%S %p %Z")
|
2026-01-28 15:03:56 -07:00
|
|
|
|
local_time = dt_obj.astimezone().strftime("%I:%M%p").lstrip('0')
|
|
|
|
|
|
return f"{eastern_time} ({local_time} local)"
|
2026-01-26 18:13:13 -07:00
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
def apply_slippage(price, is_buy=True):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Applying slippage to price={price:.2f}, is_buy={is_buy}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if not ENABLE_SLIPPAGE:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Slippage disabled")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Adjusted price: {adjusted_price:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return adjusted_price
|
2026-01-28 14:44:33 -07:00
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
def advanced_backtest_strategy():
|
|
|
|
|
|
logger.info("📊 Running advanced backtest with all filters...")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("=== STARTING BACKTEST ===")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
end_date = datetime.now()
|
|
|
|
|
|
start_date = end_date - timedelta(days=BACKTEST_DAYS)
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Backtest period: {start_date.date()} to {end_date.date()}")
|
2026-01-28 14:44:33 -07:00
|
|
|
|
debug_print(f"Fetching {BACKTEST_DAYS} days of {BAR_TIMEFRAME} bars for {SYMBOL}...")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
|
2026-01-28 14:44:33 -07:00
|
|
|
|
bars = api.get_bars(SYMBOL, BAR_TIMEFRAME, start=start_date.strftime('%Y-%m-%d'),
|
2026-01-27 17:28:46 -07:00
|
|
|
|
end=end_date.strftime('%Y-%m-%d')).df
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Received {len(bars)} bars")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if len(bars) < 100:
|
|
|
|
|
|
logger.warning("⚠️ Insufficient data for backtest")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Insufficient data for backtest, aborting")
|
2026-01-27 17:37:01 -07:00
|
|
|
|
return
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Calculating indicators for backtest...")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
closes = bars['close']
|
|
|
|
|
|
highs = bars['high']
|
|
|
|
|
|
lows = bars['low']
|
|
|
|
|
|
|
|
|
|
|
|
if USE_EMA:
|
|
|
|
|
|
short_ma = calculate_ema(closes, SHORT_WINDOW)
|
|
|
|
|
|
long_ma = calculate_ema(closes, LONG_WINDOW)
|
|
|
|
|
|
else:
|
|
|
|
|
|
short_ma = calculate_sma(closes, SHORT_WINDOW)
|
|
|
|
|
|
long_ma = calculate_sma(closes, LONG_WINDOW)
|
|
|
|
|
|
|
|
|
|
|
|
rsi = calculate_rsi(closes, 14)
|
|
|
|
|
|
adx, plus_di, minus_di = calculate_adx(highs, lows, closes, 14)
|
|
|
|
|
|
atr = calculate_atr(highs, lows, closes, 14)
|
|
|
|
|
|
upper_bb, middle_bb, lower_bb = calculate_bollinger_bands(closes, BB_WINDOW, BB_STD)
|
2026-01-26 20:30:24 -07:00
|
|
|
|
macd_line, signal_line, histogram = calculate_macd(closes)
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Indicators calculated, starting backtest simulation...")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
initial_balance = 10000
|
|
|
|
|
|
balance = initial_balance
|
|
|
|
|
|
position = 0
|
|
|
|
|
|
entry_price = 0
|
|
|
|
|
|
entry_time = None
|
|
|
|
|
|
stop_loss = 0
|
|
|
|
|
|
trades = []
|
|
|
|
|
|
winning_trades = 0
|
2026-01-26 20:30:24 -07:00
|
|
|
|
daily_trades = {}
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Initial balance: ${initial_balance}")
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
for i in range(max(SHORT_WINDOW, LONG_WINDOW, BB_WINDOW, 35), len(bars)):
|
2026-01-26 19:47:58 -07:00
|
|
|
|
current_price = closes.iloc[i]
|
|
|
|
|
|
current_time = bars.index[i]
|
2026-01-26 20:30:24 -07:00
|
|
|
|
current_date = current_time.date()
|
2026-01-26 19:47:58 -07:00
|
|
|
|
current_adx = adx.iloc[i]
|
|
|
|
|
|
current_rsi = rsi.iloc[i]
|
|
|
|
|
|
current_atr = atr.iloc[i]
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if current_date not in daily_trades:
|
|
|
|
|
|
daily_trades[current_date] = 0
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
regime = 'trend' if current_adx > ADX_THRESHOLD else 'range'
|
|
|
|
|
|
macd_signal = 'bullish' if macd_line.iloc[i] > signal_line.iloc[i] else 'bearish'
|
|
|
|
|
|
|
|
|
|
|
|
recent_bars = bars.iloc[max(0, i-1):i+1]
|
|
|
|
|
|
bullish_eng, bearish_eng = check_candle_pattern(recent_bars)
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if regime == 'trend':
|
|
|
|
|
|
ma_signal = 1 if short_ma.iloc[i] > long_ma.iloc[i] else -1
|
|
|
|
|
|
rsi_signal = 1 if current_rsi < 65 else (-1 if current_rsi > 35 else 0)
|
|
|
|
|
|
combined_signal = ma_signal + (rsi_signal * 0.3)
|
|
|
|
|
|
else:
|
|
|
|
|
|
if current_price <= lower_bb.iloc[i] and current_rsi < 30:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
combined_signal = 1.5
|
2026-01-26 19:47:58 -07:00
|
|
|
|
elif current_price >= upper_bb.iloc[i] and current_rsi > 70:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
combined_signal = -1.5
|
2026-01-26 19:47:58 -07:00
|
|
|
|
else:
|
|
|
|
|
|
combined_signal = 0
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if position == 0 and abs(combined_signal) >= 1.2:
|
|
|
|
|
|
if daily_trades[current_date] >= MAX_TRADES_PER_DAY:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if REQUIRE_CANDLE_PATTERN:
|
|
|
|
|
|
if combined_signal > 0 and not bullish_eng:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if combined_signal < 0 and not bearish_eng:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if REQUIRE_MACD_CONFIRMATION:
|
|
|
|
|
|
if combined_signal > 0 and macd_signal != 'bullish':
|
|
|
|
|
|
continue
|
|
|
|
|
|
if combined_signal < 0 and macd_signal != 'bearish':
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
position = 1 if combined_signal > 0 else -1
|
|
|
|
|
|
entry_price = apply_slippage(current_price, combined_signal > 0)
|
|
|
|
|
|
entry_time = current_time
|
|
|
|
|
|
|
|
|
|
|
|
stop_distance = current_atr * ATR_STOP_MULTIPLIER
|
|
|
|
|
|
if position > 0:
|
|
|
|
|
|
stop_loss = entry_price - stop_distance
|
|
|
|
|
|
else:
|
|
|
|
|
|
stop_loss = entry_price + stop_distance
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
daily_trades[current_date] += 1
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
trades.append({
|
|
|
|
|
|
'entry_price': entry_price,
|
|
|
|
|
|
'position': position,
|
|
|
|
|
|
'entry_time': entry_time,
|
|
|
|
|
|
'stop_loss': stop_loss,
|
|
|
|
|
|
'regime': regime
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
elif position != 0:
|
|
|
|
|
|
exit_triggered = False
|
|
|
|
|
|
exit_price = None
|
|
|
|
|
|
exit_reason = None
|
|
|
|
|
|
|
|
|
|
|
|
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_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'
|
|
|
|
|
|
|
|
|
|
|
|
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'
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Backtest simulation complete, calculating statistics...")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
total_trades = len([t for t in trades if 'exit_price' in t])
|
|
|
|
|
|
win_rate = winning_trades / total_trades if total_trades > 0 else 0
|
|
|
|
|
|
total_return = (balance - initial_balance) / initial_balance
|
|
|
|
|
|
|
|
|
|
|
|
winning_pnl = sum([t['pnl'] for t in trades if 'pnl' in t and t['pnl'] > 0])
|
|
|
|
|
|
losing_pnl = sum([abs(t['pnl']) for t in trades if 'pnl' in t and t['pnl'] < 0])
|
|
|
|
|
|
profit_factor = winning_pnl / losing_pnl if losing_pnl > 0 else 0
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
avg_win = winning_pnl / winning_trades if winning_trades > 0 else 0
|
|
|
|
|
|
avg_loss = losing_pnl / (total_trades - winning_trades) if (total_trades - winning_trades) > 0 else 0
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"📈 Advanced Backtest Results:")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
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}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f" Avg win: ${avg_win:.2f}")
|
|
|
|
|
|
logger.info(f" Avg loss: ${avg_loss:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
logger.info(f" Final balance: ${balance:.2f}")
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Backtest results: trades={total_trades}, winrate={win_rate:.1%}, return={total_return:.1%}, PF={profit_factor:.2f}")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if total_trades < 5:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.warning("⚠️ Very few trades - filters may be too strict")
|
|
|
|
|
|
if win_rate < 0.45:
|
|
|
|
|
|
logger.warning("⚠️ Win rate below target")
|
|
|
|
|
|
if profit_factor < 1.3:
|
|
|
|
|
|
logger.warning("⚠️ Profit factor < 1.3")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-01-27 17:37:01 -07:00
|
|
|
|
error_msg = str(e).lower()
|
|
|
|
|
|
if 'subscription' in error_msg or 'permit' in error_msg:
|
|
|
|
|
|
logger.warning(f"⚠️ Backtest unavailable: Your subscription doesn't permit historical data access")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Backtest failed: subscription issue - {e}")
|
2026-01-27 17:37:01 -07:00
|
|
|
|
else:
|
|
|
|
|
|
logger.warning(f"⚠️ Backtest failed: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Backtest failed: {e}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
def advanced_signal_generator(symbol):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"=== GENERATING SIGNAL FOR {symbol} ===")
|
|
|
|
|
|
|
|
|
|
|
|
debug_print("Fetching recent bars...")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
bars = get_recent_bars(symbol, 100)
|
|
|
|
|
|
if bars is None or len(bars) < 50:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Insufficient bars for signal generation")
|
2026-01-30 11:36:16 -07:00
|
|
|
|
return None, 0, 0, None
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Received {len(bars)} bars")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
closes = bars['close']
|
|
|
|
|
|
highs = bars['high']
|
|
|
|
|
|
lows = bars['low']
|
|
|
|
|
|
current_price = closes.iloc[-1]
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Current price: ${current_price:.2f}")
|
|
|
|
|
|
|
|
|
|
|
|
debug_print("Calculating indicators for signal...")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
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]
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Moving averages: short={short_ma:.2f}, long={long_ma:.2f}")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
rsi = calculate_rsi(closes, 14).iloc[-1]
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"RSI: {rsi:.2f}")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
adx, plus_di, minus_di = calculate_adx(highs, lows, closes, 14)
|
|
|
|
|
|
current_adx = adx.iloc[-1]
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"ADX: {current_adx:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
atr = calculate_atr(highs, lows, closes, 14).iloc[-1]
|
|
|
|
|
|
debug_print(f"ATR: {atr:.4f}")
|
|
|
|
|
|
|
|
|
|
|
|
upper_bb, middle_bb, lower_bb = calculate_bollinger_bands(closes, BB_WINDOW, BB_STD)
|
|
|
|
|
|
debug_print(f"Bollinger Bands: upper={upper_bb.iloc[-1]:.2f}, middle={middle_bb.iloc[-1]:.2f}, lower={lower_bb.iloc[-1]:.2f}")
|
|
|
|
|
|
|
|
|
|
|
|
debug_print("Applying filters...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
vix_level = get_vix_level()
|
|
|
|
|
|
if USE_VIX_FILTER and vix_level > VIX_THRESHOLD:
|
|
|
|
|
|
logger.info(f"📉 VIX too high: {vix_level:.1f} > {VIX_THRESHOLD}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"FILTER FAILED: VIX too high ({vix_level:.1f} > {VIX_THRESHOLD})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
|
|
|
|
|
sma_200_trend = check_200_sma_filter(symbol)
|
|
|
|
|
|
if USE_200_SMA_FILTER and sma_200_trend == 'bearish':
|
|
|
|
|
|
logger.info(f"📉 Below 200 SMA - avoiding longs")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("WARNING: Below 200 SMA - will avoid longs")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
volume_ok = check_volume_confirmation(bars)
|
|
|
|
|
|
if not volume_ok:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"📊 Insufficient volume")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("FILTER FAILED: Insufficient volume")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
bullish_eng, bearish_eng = check_candle_pattern(bars)
|
|
|
|
|
|
macd_signal = check_macd_confirmation(bars)
|
2026-01-26 19:47:58 -07:00
|
|
|
|
hourly_trend = check_multiframe_confluence(symbol)
|
2026-01-26 20:30:24 -07:00
|
|
|
|
pivot, r1, r2, s1, s2 = calculate_pivot_points(symbol)
|
|
|
|
|
|
fib_382, fib_500, fib_618, swing_high, swing_low = calculate_fibonacci_levels(bars, 20)
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
regime = detect_market_regime(bars)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Market regime: {regime}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
if regime == 'low_vol':
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info("📉 Low volatility regime")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("FILTER FAILED: Low volatility regime")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
|
|
|
|
|
signal = None
|
|
|
|
|
|
signal_strength = 0
|
|
|
|
|
|
stop_loss = 0
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Evaluating trading signals...")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if regime == 'trend':
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Processing TREND regime logic...")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if current_adx > ADX_THRESHOLD:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if short_ma > long_ma:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Bullish trend detected (short_ma > long_ma)")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
pullback_ok = False
|
|
|
|
|
|
if USE_FIBONACCI and fib_382 is not None:
|
|
|
|
|
|
if abs(current_price - fib_382) / current_price < 0.01:
|
|
|
|
|
|
pullback_ok = True
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Pullback OK: near fib 38.2% ({fib_382:.2f})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
elif current_price < short_ma * 1.005:
|
|
|
|
|
|
pullback_ok = True
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Pullback OK: price near short MA")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
|
|
|
|
|
if pullback_ok and rsi < 55:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Pullback and RSI conditions met (RSI={rsi:.2f})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if hourly_trend in ['bullish', 'neutral']:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Hourly trend favorable: {hourly_trend}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if REQUIRE_CANDLE_PATTERN and not bullish_eng:
|
|
|
|
|
|
logger.info("❌ No bullish engulfing")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("REJECTED: No bullish engulfing pattern")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
|
|
|
|
|
if REQUIRE_MACD_CONFIRMATION and macd_signal != 'bullish':
|
|
|
|
|
|
logger.info("❌ MACD not bullish")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"REJECTED: MACD not bullish ({macd_signal})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
|
|
|
|
|
if USE_200_SMA_FILTER and sma_200_trend == 'bearish':
|
|
|
|
|
|
logger.info("❌ Below 200 SMA - no longs")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("REJECTED: Below 200 SMA")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
|
|
|
|
|
if USE_PIVOT_POINTS and s1 is not None:
|
|
|
|
|
|
if current_price < s1 * 1.02:
|
|
|
|
|
|
signal = 'buy'
|
|
|
|
|
|
signal_strength = min(1.0, (current_adx / 40) * 0.8 + 0.2)
|
|
|
|
|
|
stop_loss = current_price - (atr * ATR_STOP_MULTIPLIER)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"SIGNAL: BUY (trend with pivot, strength={signal_strength:.2f}, stop={stop_loss:.2f})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
else:
|
|
|
|
|
|
signal = 'buy'
|
|
|
|
|
|
signal_strength = min(1.0, (current_adx / 40) * 0.7 + 0.3)
|
|
|
|
|
|
stop_loss = current_price - (atr * ATR_STOP_MULTIPLIER)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"SIGNAL: BUY (trend, strength={signal_strength:.2f}, stop={stop_loss:.2f})")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
elif short_ma < long_ma:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Bearish trend detected (short_ma < long_ma)")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
pullback_ok = False
|
|
|
|
|
|
if USE_FIBONACCI and fib_618 is not None:
|
|
|
|
|
|
if abs(current_price - fib_618) / current_price < 0.01:
|
|
|
|
|
|
pullback_ok = True
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Pullback OK: near fib 61.8% ({fib_618:.2f})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
elif current_price > short_ma * 0.995:
|
|
|
|
|
|
pullback_ok = True
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Pullback OK: price near short MA")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
2026-01-30 10:30:16 -07:00
|
|
|
|
if pullback_ok and rsi < 55:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Pullback and RSI conditions met (RSI={rsi:.2f})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if hourly_trend in ['bearish', 'neutral']:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Hourly trend favorable: {hourly_trend}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if REQUIRE_CANDLE_PATTERN and not bearish_eng:
|
|
|
|
|
|
logger.info("❌ No bearish engulfing")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("REJECTED: No bearish engulfing pattern")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
|
|
|
|
|
if REQUIRE_MACD_CONFIRMATION and macd_signal != 'bearish':
|
|
|
|
|
|
logger.info("❌ MACD not bearish")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"REJECTED: MACD not bearish ({macd_signal})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
|
|
|
|
|
if USE_PIVOT_POINTS and r1 is not None:
|
|
|
|
|
|
if current_price > r1 * 0.98:
|
|
|
|
|
|
signal = 'sell'
|
|
|
|
|
|
signal_strength = min(1.0, (current_adx / 40) * 0.8 + 0.2)
|
|
|
|
|
|
stop_loss = current_price + (atr * ATR_STOP_MULTIPLIER)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"SIGNAL: SELL (trend with pivot, strength={signal_strength:.2f}, stop={stop_loss:.2f})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
else:
|
|
|
|
|
|
signal = 'sell'
|
|
|
|
|
|
signal_strength = min(1.0, (current_adx / 40) * 0.7 + 0.3)
|
|
|
|
|
|
stop_loss = current_price + (atr * ATR_STOP_MULTIPLIER)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"SIGNAL: SELL (trend, strength={signal_strength:.2f}, stop={stop_loss:.2f})")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
elif regime == 'range':
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Processing RANGE regime logic...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if current_price <= lower_bb.iloc[-1] and rsi < 30:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Oversold condition: price at/below lower BB and RSI < 30")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if hourly_trend != 'bearish':
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Hourly trend not bearish: {hourly_trend}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if REQUIRE_CANDLE_PATTERN and not bullish_eng:
|
|
|
|
|
|
logger.info("❌ No bullish engulfing in range")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("REJECTED: No bullish engulfing in range")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
|
|
|
|
|
if USE_200_SMA_FILTER and sma_200_trend == 'bearish':
|
|
|
|
|
|
logger.info("❌ Below 200 SMA - no mean reversion longs")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("REJECTED: Below 200 SMA for mean reversion")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
signal = 'buy'
|
2026-01-26 20:30:24 -07:00
|
|
|
|
signal_strength = 0.85
|
2026-01-26 19:47:58 -07:00
|
|
|
|
stop_loss = current_price - (atr * ATR_STOP_MULTIPLIER)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"SIGNAL: BUY (range oversold, strength={signal_strength:.2f}, stop={stop_loss:.2f})")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
elif current_price >= upper_bb.iloc[-1] and rsi > 70:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Overbought condition: price at/above upper BB and RSI > 70")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if hourly_trend != 'bullish':
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Hourly trend not bullish: {hourly_trend}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if REQUIRE_CANDLE_PATTERN and not bearish_eng:
|
|
|
|
|
|
logger.info("❌ No bearish engulfing in range")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("REJECTED: No bearish engulfing in range")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
signal = 'sell'
|
2026-01-26 20:30:24 -07:00
|
|
|
|
signal_strength = 0.85
|
2026-01-26 19:47:58 -07:00
|
|
|
|
stop_loss = current_price + (atr * ATR_STOP_MULTIPLIER)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"SIGNAL: SELL (range overbought, strength={signal_strength:.2f}, stop={stop_loss:.2f})")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
elif regime == 'high_vol':
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Processing HIGH_VOL regime logic...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if short_ma > long_ma and rsi < 35:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"High vol bullish setup: short_ma > long_ma and RSI < 35")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if hourly_trend == 'bullish':
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Hourly trend bullish")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if REQUIRE_CANDLE_PATTERN and not bullish_eng:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("REJECTED: No bullish engulfing in high vol")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
|
|
|
|
|
if REQUIRE_MACD_CONFIRMATION and macd_signal != 'bullish':
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"REJECTED: MACD not bullish in high vol ({macd_signal})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
signal = 'buy'
|
|
|
|
|
|
signal_strength = 0.6
|
|
|
|
|
|
stop_loss = current_price - (atr * ATR_STOP_MULTIPLIER * 1.5)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"SIGNAL: BUY (high vol, strength={signal_strength:.2f}, stop={stop_loss:.2f})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
|
|
|
|
|
elif short_ma < long_ma and rsi > 65:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"High vol bearish setup: short_ma < long_ma and RSI > 65")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if hourly_trend == 'bearish':
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Hourly trend bearish")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if REQUIRE_CANDLE_PATTERN and not bearish_eng:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("REJECTED: No bearish engulfing in high vol")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
|
|
|
|
|
if REQUIRE_MACD_CONFIRMATION and macd_signal != 'bearish':
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"REJECTED: MACD not bearish in high vol ({macd_signal})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, 0, 0
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
signal = 'sell'
|
|
|
|
|
|
signal_strength = 0.6
|
|
|
|
|
|
stop_loss = current_price + (atr * ATR_STOP_MULTIPLIER * 1.5)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"SIGNAL: SELL (high vol, strength={signal_strength:.2f}, stop={stop_loss:.2f})")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
if signal_strength < MIN_SIGNAL_STRENGTH:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"❌ Signal strength {signal_strength:.2f} < {MIN_SIGNAL_STRENGTH:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"REJECTED: Signal strength {signal_strength:.2f} < threshold {MIN_SIGNAL_STRENGTH:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return None, signal_strength, 0
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if signal and stop_loss != 0:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Performing risk/reward check...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
potential_reward = abs(current_price - stop_loss) * MIN_RISK_REWARD
|
|
|
|
|
|
if USE_PIVOT_POINTS:
|
|
|
|
|
|
if signal == 'buy' and r1 is not None:
|
|
|
|
|
|
actual_reward = r1 - current_price
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"R:R check (buy): actual_reward={actual_reward:.2f}, potential_reward={potential_reward:.2f}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if actual_reward < potential_reward:
|
|
|
|
|
|
logger.info(f"❌ R:R too low: {actual_reward:.2f} < {potential_reward:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"REJECTED: R:R too low")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, signal_strength, 0
|
|
|
|
|
|
elif signal == 'sell' and s1 is not None:
|
|
|
|
|
|
actual_reward = current_price - s1
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"R:R check (sell): actual_reward={actual_reward:.2f}, potential_reward={potential_reward:.2f}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if actual_reward < potential_reward:
|
|
|
|
|
|
logger.info(f"❌ R:R too low: {actual_reward:.2f} < {potential_reward:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"REJECTED: R:R too low")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
return None, signal_strength, 0
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
if signal:
|
|
|
|
|
|
debug_print(f"=== FINAL SIGNAL: {signal.upper()}, strength={signal_strength:.2f}, stop=${stop_loss:.2f} ===")
|
|
|
|
|
|
else:
|
|
|
|
|
|
debug_print("=== NO SIGNAL GENERATED ===")
|
|
|
|
|
|
|
2026-01-30 11:36:16 -07:00
|
|
|
|
position_type = 'long' if signal == 'buy' else ('short' if signal == 'sell' else None)
|
|
|
|
|
|
return signal, signal_strength, stop_loss, position_type
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-26 18:13:13 -07:00
|
|
|
|
def wait_until_market_open():
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Checking if market is open...")
|
2026-01-27 17:09:01 -07:00
|
|
|
|
try:
|
|
|
|
|
|
clock = api.get_clock()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"⚠️ Failed to get clock: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Failed to get clock: {e}")
|
2026-01-27 17:09:01 -07:00
|
|
|
|
time.sleep(60)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-01-26 18:13:13 -07:00
|
|
|
|
now = clock.timestamp
|
2026-01-29 17:56:44 -07:00
|
|
|
|
if now.tzinfo is None:
|
|
|
|
|
|
now = EASTERN.localize(now)
|
|
|
|
|
|
else:
|
|
|
|
|
|
now = now.astimezone(EASTERN)
|
|
|
|
|
|
|
2026-01-26 18:13:13 -07:00
|
|
|
|
next_open = clock.next_open
|
2026-01-29 17:56:44 -07:00
|
|
|
|
if next_open.tzinfo is None:
|
|
|
|
|
|
next_open = EASTERN.localize(next_open)
|
|
|
|
|
|
else:
|
|
|
|
|
|
next_open = next_open.astimezone(EASTERN)
|
2026-01-26 18:13:13 -07:00
|
|
|
|
|
|
|
|
|
|
if not clock.is_open:
|
|
|
|
|
|
seconds_until_open = (next_open - now).total_seconds()
|
2026-01-29 17:56:44 -07:00
|
|
|
|
readable_time = seconds_to_human_readable(seconds_until_open)
|
|
|
|
|
|
debug_print(f"Market closed, {readable_time} until open")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
if seconds_until_open > 0:
|
|
|
|
|
|
readable_time = seconds_to_human_readable(seconds_until_open)
|
2026-01-26 18:44:08 -07:00
|
|
|
|
logger.info(f"🕒 Market opens at {format_market_time(next_open)}")
|
|
|
|
|
|
logger.info(f"⏱️ Waiting {readable_time}...")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
|
|
|
|
|
|
while seconds_until_open > 0:
|
2026-01-26 19:47:58 -07:00
|
|
|
|
sleep_time = min(60, seconds_until_open)
|
2026-01-26 18:13:13 -07:00
|
|
|
|
time.sleep(sleep_time)
|
|
|
|
|
|
seconds_until_open -= sleep_time
|
|
|
|
|
|
|
2026-01-29 17:56:44 -07:00
|
|
|
|
if sleep_time >= 60 and (seconds_until_open % 3600 < 60 or seconds_until_open < 3600):
|
2026-01-26 18:13:13 -07:00
|
|
|
|
remaining_readable = seconds_to_human_readable(seconds_until_open)
|
2026-01-26 18:44:08 -07:00
|
|
|
|
logger.info(f"⏱️ {remaining_readable} remaining...")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Waiting... {remaining_readable} remaining")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
else:
|
2026-01-26 18:44:08 -07:00
|
|
|
|
logger.info("✅ Market is open!")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Market is open")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
else:
|
2026-01-26 18:44:08 -07:00
|
|
|
|
logger.info("✅ Market is open!")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Market is open")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
|
|
|
|
|
|
def fetch_equity():
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Fetching account equity...")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
try:
|
|
|
|
|
|
account = api.get_account()
|
2026-01-28 11:57:33 -07:00
|
|
|
|
equity = float(account.equity)
|
|
|
|
|
|
debug_print(f"Account equity: ${equity:.2f}")
|
|
|
|
|
|
return equity
|
2026-01-26 18:13:13 -07:00
|
|
|
|
except Exception as e:
|
2026-01-26 18:44:08 -07:00
|
|
|
|
logger.error(f"❌ Failed to fetch equity: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Failed to fetch equity: {e}")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return 0.0
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_buying_power():
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Fetching buying power...")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
try:
|
|
|
|
|
|
account = api.get_account()
|
2026-01-28 11:57:33 -07:00
|
|
|
|
bp = float(account.buying_power)
|
|
|
|
|
|
debug_print(f"Buying power: ${bp:.2f}")
|
|
|
|
|
|
return bp
|
2026-01-26 18:13:13 -07:00
|
|
|
|
except Exception as e:
|
2026-01-26 18:44:08 -07:00
|
|
|
|
logger.error(f"❌ Failed to fetch buying power: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Failed to fetch buying power: {e}")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return 0.0
|
|
|
|
|
|
|
|
|
|
|
|
def get_day_trade_count():
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Getting day trade count...")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
try:
|
|
|
|
|
|
account = api.get_account()
|
2026-01-28 11:57:33 -07:00
|
|
|
|
count = int(account.daytrade_count)
|
|
|
|
|
|
debug_print(f"Day trade count: {count}")
|
|
|
|
|
|
return count
|
2026-01-26 18:13:13 -07:00
|
|
|
|
except Exception as e:
|
2026-01-26 18:44:08 -07:00
|
|
|
|
logger.error(f"❌ Failed to fetch day trade count: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Failed to fetch day trade count: {e}")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return 0
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
def submit_limit_buy(symbol, notional, limit_price):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"=== SUBMITTING LIMIT BUY ORDER ===")
|
|
|
|
|
|
debug_print(f"Symbol: {symbol}, Notional: ${notional:.2f}, Limit: ${limit_price:.2f}")
|
|
|
|
|
|
|
2026-01-26 18:13:13 -07:00
|
|
|
|
if notional < MIN_NOTIONAL:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.warning(f"⚠️ Notional ${notional:.2f} < minimum ${MIN_NOTIONAL}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Order rejected: notional too small")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-01-26 19:47:58 -07:00
|
|
|
|
shares = int(notional / limit_price)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Calculated shares: {shares}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
if shares == 0:
|
|
|
|
|
|
logger.warning(f"⚠️ Cannot buy fractional shares with ${notional:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Order rejected: shares = 0")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Submitting limit buy order to API...")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
order = api.submit_order(
|
|
|
|
|
|
symbol=symbol,
|
|
|
|
|
|
qty=shares,
|
|
|
|
|
|
side="buy",
|
|
|
|
|
|
type="limit",
|
|
|
|
|
|
limit_price=round(limit_price, 2),
|
|
|
|
|
|
time_in_force="gtc"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Order submitted, ID: {order.id}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"🟢 LIMIT BUY: {shares} shares @ ${limit_price:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
start_time = time.time()
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Waiting for fill (timeout: {LIMIT_ORDER_TIMEOUT}s)...")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
while (time.time() - start_time) < LIMIT_ORDER_TIMEOUT:
|
|
|
|
|
|
order_status = api.get_order(order.id)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Order status: {order_status.status}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if order_status.status == 'filled':
|
|
|
|
|
|
filled_price = float(order_status.filled_avg_price)
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"✅ FILLED @ ${filled_price:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Order filled at ${filled_price:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return filled_price
|
|
|
|
|
|
elif order_status.status in ['cancelled', 'expired', 'rejected']:
|
|
|
|
|
|
logger.warning(f"⚠️ Limit order {order_status.status}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Order {order_status.status}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return False
|
|
|
|
|
|
time.sleep(2)
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.warning("⏱️ Timeout - switching to market")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Timeout reached, canceling order and switching to market")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
api.cancel_order(order.id)
|
|
|
|
|
|
return submit_market_buy(symbol, notional)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.error(f"❌ Failed limit buy: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Limit buy failed: {e}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def submit_market_buy(symbol, notional):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"=== SUBMITTING MARKET BUY ORDER ===")
|
|
|
|
|
|
debug_print(f"Symbol: {symbol}, Notional: ${notional:.2f}")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
try:
|
|
|
|
|
|
current_price = get_current_price(symbol)
|
|
|
|
|
|
if current_price == 0:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Market buy failed: could not get current price")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
execution_price = apply_slippage(current_price, True)
|
|
|
|
|
|
shares = int(notional / execution_price)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Shares: {shares}, Expected execution: ${execution_price:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
if shares == 0:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Market buy failed: shares = 0")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Submitting market buy order to API...")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
api.submit_order(
|
|
|
|
|
|
symbol=symbol,
|
2026-01-26 19:47:58 -07:00
|
|
|
|
qty=shares,
|
2026-01-26 18:13:13 -07:00
|
|
|
|
side="buy",
|
|
|
|
|
|
type="market",
|
|
|
|
|
|
time_in_force="day"
|
|
|
|
|
|
)
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"🟢 MARKET BUY: {shares} shares @ ~${execution_price:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Market buy order submitted")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return execution_price
|
2026-01-26 18:13:13 -07:00
|
|
|
|
except Exception as e:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.error(f"❌ Failed buy: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Market buy failed: {e}")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-30 09:49:14 -07:00
|
|
|
|
def submit_short_sell(symbol, notional):
|
|
|
|
|
|
"""Open a short position by selling shares we don't own"""
|
|
|
|
|
|
debug_print(f"=== SUBMITTING SHORT SELL (OPENING SHORT POSITION) ===")
|
|
|
|
|
|
debug_print(f"Symbol: {symbol}, Notional: ${notional:.2f}")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
current_price = get_current_price(symbol)
|
|
|
|
|
|
if current_price == 0:
|
|
|
|
|
|
debug_print("Short sell failed: could not get current price")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
execution_price = apply_slippage(current_price, False)
|
|
|
|
|
|
shares = int(notional / execution_price)
|
|
|
|
|
|
debug_print(f"Shares to short: {shares}, Expected execution: ${execution_price:.2f}")
|
|
|
|
|
|
|
|
|
|
|
|
if shares == 0:
|
|
|
|
|
|
debug_print("Short sell failed: shares = 0")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
debug_print("Submitting short sell order to API...")
|
|
|
|
|
|
api.submit_order(
|
|
|
|
|
|
symbol=symbol,
|
|
|
|
|
|
qty=shares,
|
|
|
|
|
|
side="sell",
|
|
|
|
|
|
type="market",
|
|
|
|
|
|
time_in_force="day"
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info(f"🔴 SHORT SELL: {shares} shares @ ~${execution_price:.2f}")
|
|
|
|
|
|
debug_print(f"Short sell order submitted (opened short position)")
|
|
|
|
|
|
return execution_price
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"❌ Failed short sell: {e}")
|
|
|
|
|
|
debug_print(f"Short sell failed: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def submit_limit_short_sell(symbol, notional, limit_price):
|
|
|
|
|
|
debug_print(f"=== SUBMITTING LIMIT SHORT SELL (OPENING SHORT POSITION) ===")
|
|
|
|
|
|
debug_print(f"Symbol: {symbol}, Notional: ${notional:.2f}, Limit: ${limit_price:.2f}")
|
|
|
|
|
|
|
|
|
|
|
|
if notional < MIN_NOTIONAL:
|
|
|
|
|
|
logger.warning(f"⚠️ Notional ${notional:.2f} < minimum ${MIN_NOTIONAL}")
|
|
|
|
|
|
debug_print(f"Order rejected: notional too small")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
shares = int(notional / limit_price)
|
|
|
|
|
|
debug_print(f"Calculated shares to short: {shares}")
|
|
|
|
|
|
|
|
|
|
|
|
if shares == 0:
|
|
|
|
|
|
logger.warning(f"⚠️ Cannot short fractional shares with ${notional:.2f}")
|
|
|
|
|
|
debug_print(f"Order rejected: shares = 0")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
debug_print(f"Submitting limit short sell order to API...")
|
|
|
|
|
|
order = api.submit_order(
|
|
|
|
|
|
symbol=symbol,
|
|
|
|
|
|
qty=shares,
|
|
|
|
|
|
side="sell",
|
|
|
|
|
|
type="limit",
|
|
|
|
|
|
limit_price=round(limit_price, 2),
|
|
|
|
|
|
time_in_force="gtc"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
debug_print(f"Order submitted, ID: {order.id}")
|
|
|
|
|
|
logger.info(f"🔴 LIMIT SHORT SELL: {shares} shares @ ${limit_price:.2f}")
|
|
|
|
|
|
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
debug_print(f"Waiting for fill (timeout: {LIMIT_ORDER_TIMEOUT}s)...")
|
|
|
|
|
|
while (time.time() - start_time) < LIMIT_ORDER_TIMEOUT:
|
|
|
|
|
|
order_status = api.get_order(order.id)
|
|
|
|
|
|
debug_print(f"Order status: {order_status.status}")
|
|
|
|
|
|
if order_status.status == 'filled':
|
|
|
|
|
|
filled_price = float(order_status.filled_avg_price)
|
2026-01-30 11:39:44 -07:00
|
|
|
|
stop_distance = abs(filled_price - limit_price) * ATR_STOP_MULTIPLIER
|
|
|
|
|
|
suggested_stop = filled_price + stop_distance
|
2026-01-30 09:49:14 -07:00
|
|
|
|
logger.info(f"✅ FILLED @ ${filled_price:.2f}")
|
2026-01-30 11:39:44 -07:00
|
|
|
|
debug_print(f"Order filled at ${filled_price:.2f}, suggested stop: ${suggested_stop:.2f}")
|
2026-01-30 09:49:14 -07:00
|
|
|
|
return filled_price
|
|
|
|
|
|
elif order_status.status in ['cancelled', 'expired', 'rejected']:
|
|
|
|
|
|
logger.warning(f"⚠️ Limit order {order_status.status}")
|
|
|
|
|
|
debug_print(f"Order {order_status.status}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
time.sleep(2)
|
|
|
|
|
|
|
|
|
|
|
|
logger.warning("⏱️ Timeout - switching to market")
|
|
|
|
|
|
debug_print("Timeout reached, canceling order and switching to market")
|
|
|
|
|
|
api.cancel_order(order.id)
|
|
|
|
|
|
return submit_short_sell(symbol, notional)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"❌ Failed limit short sell: {e}")
|
|
|
|
|
|
debug_print(f"Limit short sell failed: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-30 11:39:44 -07:00
|
|
|
|
|
2026-01-30 09:49:14 -07:00
|
|
|
|
def submit_buy_to_cover(symbol, qty):
|
|
|
|
|
|
"""Close a short position by buying back shares"""
|
|
|
|
|
|
debug_print(f"=== SUBMITTING BUY TO COVER (CLOSING SHORT POSITION) ===")
|
|
|
|
|
|
debug_print(f"Symbol: {symbol}, Qty: {qty}")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
current_price = get_current_price(symbol)
|
|
|
|
|
|
if current_price == 0:
|
|
|
|
|
|
debug_print("Buy to cover failed: could not get current price")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
execution_price = apply_slippage(current_price, True)
|
|
|
|
|
|
debug_print(f"Expected execution: ${execution_price:.2f}")
|
|
|
|
|
|
|
|
|
|
|
|
debug_print("Submitting buy to cover order to API...")
|
|
|
|
|
|
api.submit_order(
|
|
|
|
|
|
symbol=symbol,
|
|
|
|
|
|
qty=qty,
|
|
|
|
|
|
side="buy",
|
|
|
|
|
|
type="market",
|
|
|
|
|
|
time_in_force="day"
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info(f"🟢 BUY TO COVER: {qty} shares @ ~${execution_price:.2f}")
|
|
|
|
|
|
debug_print(f"Buy to cover order submitted (closed short position)")
|
|
|
|
|
|
return execution_price
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"❌ Failed buy to cover: {e}")
|
|
|
|
|
|
debug_print(f"Buy to cover failed: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
def submit_limit_sell(symbol, qty, limit_price):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"=== SUBMITTING LIMIT SELL ORDER ===")
|
|
|
|
|
|
debug_print(f"Symbol: {symbol}, Qty: {qty}, Limit: ${limit_price:.2f}")
|
|
|
|
|
|
|
2026-01-30 11:39:44 -07:00
|
|
|
|
qty = abs(qty)
|
|
|
|
|
|
|
2026-01-26 18:13:13 -07:00
|
|
|
|
try:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Submitting limit sell order to API...")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
order = api.submit_order(
|
|
|
|
|
|
symbol=symbol,
|
|
|
|
|
|
qty=qty,
|
|
|
|
|
|
side="sell",
|
|
|
|
|
|
type="limit",
|
|
|
|
|
|
limit_price=round(limit_price, 2),
|
|
|
|
|
|
time_in_force="gtc"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Order submitted, ID: {order.id}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"🔴 LIMIT SELL: {qty} shares @ ${limit_price:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
start_time = time.time()
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Waiting for fill (timeout: {LIMIT_ORDER_TIMEOUT}s)...")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
while (time.time() - start_time) < LIMIT_ORDER_TIMEOUT:
|
|
|
|
|
|
order_status = api.get_order(order.id)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Order status: {order_status.status}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if order_status.status == 'filled':
|
|
|
|
|
|
filled_price = float(order_status.filled_avg_price)
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"✅ FILLED @ ${filled_price:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Order filled at ${filled_price:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return filled_price
|
|
|
|
|
|
elif order_status.status in ['cancelled', 'expired', 'rejected']:
|
|
|
|
|
|
logger.warning(f"⚠️ Limit order {order_status.status}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Order {order_status.status}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return False
|
|
|
|
|
|
time.sleep(2)
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.warning("⏱️ Timeout - switching to market")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Timeout reached, canceling order and switching to market")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
api.cancel_order(order.id)
|
|
|
|
|
|
return submit_market_sell(symbol, qty)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.error(f"❌ Failed limit sell: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Limit sell failed: {e}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-30 11:39:44 -07:00
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
def submit_market_sell(symbol, qty):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"=== SUBMITTING MARKET SELL ORDER ===")
|
|
|
|
|
|
debug_print(f"Symbol: {symbol}, Qty: {qty}")
|
|
|
|
|
|
|
2026-01-30 11:39:44 -07:00
|
|
|
|
qty = abs(qty)
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
try:
|
|
|
|
|
|
current_price = get_current_price(symbol)
|
|
|
|
|
|
if current_price == 0:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Market sell failed: could not get current price")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
execution_price = apply_slippage(current_price, False)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Expected execution: ${execution_price:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Submitting market sell order to API...")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
api.submit_order(
|
|
|
|
|
|
symbol=symbol,
|
|
|
|
|
|
qty=qty,
|
|
|
|
|
|
side="sell",
|
|
|
|
|
|
type="market",
|
|
|
|
|
|
time_in_force="day"
|
|
|
|
|
|
)
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"🔴 MARKET SELL: {qty} shares @ ~${execution_price:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Market sell order submitted")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return execution_price
|
2026-01-26 18:13:13 -07:00
|
|
|
|
except Exception as e:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.error(f"❌ Failed sell: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Market sell failed: {e}")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-30 11:39:44 -07:00
|
|
|
|
|
2026-01-26 18:13:13 -07:00
|
|
|
|
def close_all_positions():
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Closing all positions...")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
try:
|
|
|
|
|
|
positions = api.list_positions()
|
|
|
|
|
|
if not positions:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info("✅ No open positions")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("No open positions to close")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Found {len(positions)} positions to close")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.warning("⚠️ Closing all positions...")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
for pos in positions:
|
2026-01-30 11:31:03 -07:00
|
|
|
|
qty = int(float(pos.qty))
|
|
|
|
|
|
debug_print(f"Closing position: {pos.symbol}, qty={qty}")
|
|
|
|
|
|
if qty > 0:
|
|
|
|
|
|
submit_market_sell(pos.symbol, qty)
|
|
|
|
|
|
elif qty < 0:
|
|
|
|
|
|
submit_buy_to_cover(pos.symbol, abs(qty))
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info("✅ All positions closed")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("All positions closed successfully")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
except Exception as e:
|
2026-01-26 18:44:08 -07:00
|
|
|
|
logger.error(f"❌ Failed to close positions: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Failed to close positions: {e}")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
|
2026-01-26 18:44:08 -07:00
|
|
|
|
def get_recent_bars(symbol, limit=100):
|
2026-01-28 14:44:33 -07:00
|
|
|
|
debug_print(f"Fetching {limit} recent {BAR_TIMEFRAME} bars for {symbol}...")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
try:
|
2026-01-28 14:44:33 -07:00
|
|
|
|
bars = api.get_bars(symbol, BAR_TIMEFRAME, limit=limit).df
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Received {len(bars)} bars")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return bars
|
|
|
|
|
|
except Exception as e:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.error(f"❌ Failed to fetch bars: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Failed to fetch bars: {e}")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def current_position_qty(symbol):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Checking position quantity for {symbol}...")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
try:
|
|
|
|
|
|
positions = api.list_positions()
|
|
|
|
|
|
for pos in positions:
|
|
|
|
|
|
if pos.symbol == symbol:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
qty = int(float(pos.qty))
|
2026-01-30 09:49:14 -07:00
|
|
|
|
debug_print(f"Position qty: {qty} ({'SHORT' if qty < 0 else 'LONG'})")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
return qty
|
|
|
|
|
|
debug_print("No position found")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return 0
|
|
|
|
|
|
except Exception as e:
|
2026-01-26 18:44:08 -07:00
|
|
|
|
logger.error(f"❌ Failed to fetch positions: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Failed to fetch positions: {e}")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
def pdt_allows_new_trade():
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Checking PDT rules...")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
if not PDT_RULE:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("PDT rule disabled, allowing trade")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
equity = fetch_equity()
|
|
|
|
|
|
day_trade_count = get_day_trade_count()
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"PDT check: equity=${equity:.2f}, day_trades={day_trade_count}")
|
|
|
|
|
|
|
2026-01-26 18:13:13 -07:00
|
|
|
|
if equity < 25000:
|
|
|
|
|
|
if day_trade_count >= 3:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.error(f"🛑 PDT rule: {day_trade_count} trades in 5-day window")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"PDT violation: {day_trade_count} >= 3 with equity < $25k")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("PDT check passed")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def get_market_status():
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Getting market status...")
|
2026-01-27 17:09:01 -07:00
|
|
|
|
try:
|
|
|
|
|
|
clock = api.get_clock()
|
|
|
|
|
|
status = "open" if clock.is_open else "closed"
|
|
|
|
|
|
next_event = clock.next_open if not clock.is_open else clock.next_close
|
|
|
|
|
|
event_type = "open" if not clock.is_open else "close"
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Market status: {status}, next {event_type} at {next_event}")
|
|
|
|
|
|
|
2026-01-27 17:09:01 -07:00
|
|
|
|
return {
|
|
|
|
|
|
"status": status,
|
|
|
|
|
|
"next_event": next_event,
|
|
|
|
|
|
"event_type": event_type,
|
|
|
|
|
|
"timestamp": clock.timestamp
|
|
|
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"⚠️ Failed to get market status: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Failed to get market status: {e}")
|
2026-01-27 17:09:01 -07:00
|
|
|
|
return {
|
|
|
|
|
|
"status": "unknown",
|
|
|
|
|
|
"next_event": None,
|
|
|
|
|
|
"event_type": "unknown",
|
|
|
|
|
|
"timestamp": datetime.now()
|
|
|
|
|
|
}
|
2026-01-26 18:13:13 -07:00
|
|
|
|
|
2026-01-30 11:36:16 -07:00
|
|
|
|
def calculate_position_size(equity, stop_loss, entry_price, regime='normal', max_position_pct=0.95):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Calculating position size: equity=${equity:.2f}, entry=${entry_price:.2f}, stop=${stop_loss:.2f}, regime={regime}")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
risk_amount = equity * RISK_PER_TRADE
|
2026-01-26 18:44:08 -07:00
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if regime == 'high_vol':
|
|
|
|
|
|
risk_amount *= 0.5
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"📊 High vol - reducing position 50%")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("High vol: reducing risk by 50%")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
stop_distance = abs(entry_price - stop_loss)
|
|
|
|
|
|
if stop_distance == 0:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Stop distance is 0, returning MIN_NOTIONAL")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return MIN_NOTIONAL
|
2026-01-26 18:44:08 -07:00
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
position_size = risk_amount / stop_distance * entry_price
|
|
|
|
|
|
position_size = max(MIN_NOTIONAL, position_size)
|
2026-01-26 18:44:08 -07:00
|
|
|
|
|
2026-01-30 11:36:16 -07:00
|
|
|
|
max_position = equity * max_position_pct
|
|
|
|
|
|
if position_size > max_position:
|
|
|
|
|
|
position_size = max_position
|
|
|
|
|
|
debug_print(f"Position capped at {max_position_pct:.0%} of equity: ${position_size:.2f}")
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"💰 Position: Risk=${risk_amount:.2f}, Stop=${stop_distance:.2f}, Size=${position_size:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Position size: ${position_size:.2f}")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return position_size
|
2026-01-26 18:44:08 -07:00
|
|
|
|
|
|
|
|
|
|
def should_trade_based_on_market_hours():
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Checking if in tradeable market hours...")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
if not MARKET_HOURS_FILTER:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Market hours filter disabled")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
return True
|
2026-01-29 17:56:44 -07:00
|
|
|
|
|
|
|
|
|
|
now_eastern = datetime.now(EASTERN).time()
|
2026-01-26 18:44:08 -07:00
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
open_buffer_end = datetime.strptime("10:00", "%H:%M").time()
|
|
|
|
|
|
close_buffer_start = datetime.strptime("15:30", "%H:%M").time()
|
2026-01-26 18:44:08 -07:00
|
|
|
|
|
2026-01-29 17:56:44 -07:00
|
|
|
|
debug_print(f"Current time (ET): {now_eastern}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
|
2026-01-29 17:56:44 -07:00
|
|
|
|
if now_eastern < open_buffer_end:
|
|
|
|
|
|
debug_print("Before 10:00 AM ET, outside trading hours")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return False
|
2026-01-26 18:44:08 -07:00
|
|
|
|
|
2026-01-29 17:56:44 -07:00
|
|
|
|
if now_eastern >= close_buffer_start:
|
|
|
|
|
|
debug_print("After 3:30 PM ET, outside trading hours")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Within trading hours")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
return True
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
def atr_based_trailing_stop(symbol, entry_price, current_price, stop_loss, position_type='long'):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Checking trailing stop: entry=${entry_price:.2f}, current=${current_price:.2f}, stop=${stop_loss:.2f}, type={position_type}")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if not USE_TRAILING_STOP:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Trailing stop disabled, checking fixed stop")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if position_type == 'long' and current_price <= stop_loss:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Fixed stop hit (long)")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return True
|
|
|
|
|
|
elif position_type == 'short' and current_price >= stop_loss:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Fixed stop hit (short)")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-26 18:44:08 -07:00
|
|
|
|
position_qty = current_position_qty(symbol)
|
|
|
|
|
|
if position_qty == 0:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("No position, skipping stop check")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
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
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"ATR trail distance: {trail_distance:.4f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
else:
|
|
|
|
|
|
trail_distance = abs(entry_price - stop_loss)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Using fixed trail distance: {trail_distance:.4f}")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
if not hasattr(atr_based_trailing_stop, 'trailing_stop'):
|
|
|
|
|
|
atr_based_trailing_stop.trailing_stop = stop_loss
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Initialized trailing stop: ${stop_loss:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
if position_type == 'long':
|
|
|
|
|
|
new_stop = current_price - trail_distance
|
|
|
|
|
|
if new_stop > atr_based_trailing_stop.trailing_stop:
|
|
|
|
|
|
atr_based_trailing_stop.trailing_stop = new_stop
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"📈 Trailing stop → ${new_stop:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Trailing stop updated (long): ${new_stop:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
if current_price <= atr_based_trailing_stop.trailing_stop:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"🛑 Trailing stop hit @ ${current_price:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Trailing stop hit (long): price=${current_price:.2f} <= stop=${atr_based_trailing_stop.trailing_stop:.2f}")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
return True
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
elif position_type == 'short':
|
|
|
|
|
|
new_stop = current_price + trail_distance
|
|
|
|
|
|
if new_stop < atr_based_trailing_stop.trailing_stop:
|
|
|
|
|
|
atr_based_trailing_stop.trailing_stop = new_stop
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"📉 Trailing stop → ${new_stop:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Trailing stop updated (short): ${new_stop:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
if current_price >= atr_based_trailing_stop.trailing_stop:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"🛑 Trailing stop hit @ ${current_price:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Trailing stop hit (short): price=${current_price:.2f} >= stop=${atr_based_trailing_stop.trailing_stop:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return True
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Trailing stop not hit")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def scale_out_profit_taking(symbol, entry_price, current_price, stop_loss, position_type='long'):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Checking profit targets: entry=${entry_price:.2f}, current=${current_price:.2f}, stop=${stop_loss:.2f}")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
position_qty = current_position_qty(symbol)
|
|
|
|
|
|
if position_qty == 0:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("No position, skipping profit targets")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
risk_distance = abs(entry_price - stop_loss)
|
|
|
|
|
|
|
|
|
|
|
|
if position_type == 'long':
|
|
|
|
|
|
profit_pct = (current_price - entry_price) / entry_price
|
|
|
|
|
|
profit_in_r = (current_price - entry_price) / risk_distance if risk_distance > 0 else 0
|
|
|
|
|
|
else:
|
|
|
|
|
|
profit_pct = (entry_price - current_price) / entry_price
|
|
|
|
|
|
profit_in_r = (entry_price - current_price) / risk_distance if risk_distance > 0 else 0
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Profit: {profit_pct:.2%}, {profit_in_r:.2f}R")
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Target 1 ({PROFIT_TARGET_1}R) hit, scaling out {partial_qty} shares")
|
|
|
|
|
|
|
2026-01-30 11:31:03 -07:00
|
|
|
|
if partial_qty != 0:
|
2026-01-30 09:49:14 -07:00
|
|
|
|
if position_type == 'long':
|
|
|
|
|
|
if USE_LIMIT_ORDERS:
|
|
|
|
|
|
limit_price = current_price
|
|
|
|
|
|
submit_limit_sell(symbol, partial_qty, limit_price)
|
|
|
|
|
|
else:
|
|
|
|
|
|
submit_market_sell(symbol, partial_qty)
|
2026-01-26 19:47:58 -07:00
|
|
|
|
else:
|
2026-01-30 09:49:14 -07:00
|
|
|
|
submit_buy_to_cover(symbol, partial_qty)
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"🎯 Target 1 ({PROFIT_TARGET_1}R) - 50% out @ ${current_price:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
|
|
|
|
|
atr_based_trailing_stop.trailing_stop = entry_price
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"🔒 Stop → breakeven: ${entry_price:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Stop moved to breakeven: ${entry_price:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
if profit_in_r >= PROFIT_TARGET_2:
|
|
|
|
|
|
remaining_qty = current_position_qty(symbol)
|
2026-01-30 09:49:14 -07:00
|
|
|
|
debug_print(f"Target 2 ({PROFIT_TARGET_2}R) hit, exiting {abs(remaining_qty)} shares")
|
|
|
|
|
|
if remaining_qty != 0:
|
|
|
|
|
|
if position_type == 'long':
|
|
|
|
|
|
if USE_LIMIT_ORDERS:
|
|
|
|
|
|
limit_price = current_price
|
|
|
|
|
|
submit_limit_sell(symbol, remaining_qty, limit_price)
|
|
|
|
|
|
else:
|
|
|
|
|
|
submit_market_sell(symbol, remaining_qty)
|
2026-01-26 19:47:58 -07:00
|
|
|
|
else:
|
2026-01-30 09:49:14 -07:00
|
|
|
|
submit_buy_to_cover(symbol, abs(remaining_qty))
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"🎯🎯 Target 2 ({PROFIT_TARGET_2}R) - Full exit @ ${current_price:.2f}")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
return True
|
2026-01-26 18:44:08 -07:00
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("No profit targets hit")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def get_current_price(symbol):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Getting current price for {symbol}...")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
try:
|
2026-01-26 19:47:58 -07:00
|
|
|
|
bars = api.get_bars(symbol, "1Min", limit=5).df
|
|
|
|
|
|
if len(bars) > 0:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
price = bars['close'].iloc[-1]
|
|
|
|
|
|
debug_print(f"Current price: ${price:.2f}")
|
|
|
|
|
|
return price
|
2026-01-26 18:44:08 -07:00
|
|
|
|
else:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("No bars returned")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
return 0
|
|
|
|
|
|
except Exception as e:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.error(f"❌ Failed to get price: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Failed to get price: {e}")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
return 0
|
|
|
|
|
|
|
2026-01-26 19:47:58 -07:00
|
|
|
|
def get_bid_ask(symbol):
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Getting bid/ask for {symbol}...")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
try:
|
|
|
|
|
|
quote = api.get_latest_quote(symbol)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
bid = float(quote.bid_price)
|
|
|
|
|
|
ask = float(quote.ask_price)
|
|
|
|
|
|
debug_print(f"Bid: ${bid:.2f}, Ask: ${ask:.2f}")
|
|
|
|
|
|
return bid, ask
|
2026-01-26 19:47:58 -07:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"⚠️ Could not get bid/ask: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Failed to get bid/ask: {e}, using current price")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
current_price = get_current_price(symbol)
|
|
|
|
|
|
return current_price, current_price
|
2026-01-28 14:44:33 -07:00
|
|
|
|
|
2026-01-26 18:13:13 -07:00
|
|
|
|
def main():
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info("🚀 Starting daytrader.py - continuous operation")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
if DEBUG_MODE:
|
|
|
|
|
|
print("\n" + "="*70)
|
|
|
|
|
|
print("DEBUG MODE ENABLED - Verbose output active")
|
|
|
|
|
|
print("="*70 + "\n")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-27 17:21:35 -07:00
|
|
|
|
logger.info("🔍 Validating API connectivity...")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Starting API validation...")
|
2026-01-27 17:21:35 -07:00
|
|
|
|
try:
|
|
|
|
|
|
account = api.get_account()
|
|
|
|
|
|
logger.info(f"✅ API connected successfully")
|
|
|
|
|
|
logger.info(f"✅ Account ID: {account.id}")
|
|
|
|
|
|
logger.info(f"✅ Equity: ${float(account.equity):.2f}")
|
|
|
|
|
|
logger.info(f"✅ Buying Power: ${float(account.buying_power):.2f}")
|
|
|
|
|
|
logger.info(f"✅ Day Trade Count: {int(account.daytrade_count)}")
|
|
|
|
|
|
logger.info(f"✅ Pattern Day Trader: {account.pattern_day_trader}")
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"API validation successful")
|
|
|
|
|
|
|
|
|
|
|
|
debug_print(f"Testing market data access for {SYMBOL}...")
|
2026-01-27 17:21:35 -07:00
|
|
|
|
test_bars = api.get_bars(SYMBOL, "1Day", limit=1).df
|
|
|
|
|
|
if len(test_bars) > 0:
|
|
|
|
|
|
logger.info(f"✅ Market data access verified for {SYMBOL}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Market data access verified")
|
2026-01-27 17:21:35 -07:00
|
|
|
|
else:
|
|
|
|
|
|
logger.warning(f"⚠️ No market data returned for {SYMBOL}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("WARNING: No market data returned")
|
2026-01-27 17:21:35 -07:00
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Testing clock access...")
|
2026-01-27 17:21:35 -07:00
|
|
|
|
clock = api.get_clock()
|
|
|
|
|
|
logger.info(f"✅ Clock access verified - Market is {'OPEN' if clock.is_open else 'CLOSED'}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Clock access verified, market is {'OPEN' if clock.is_open else 'CLOSED'}")
|
2026-01-27 17:21:35 -07:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-01-27 17:32:34 -07:00
|
|
|
|
error_msg = str(e).lower()
|
|
|
|
|
|
logger.error(f"❌ API validation failed")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"API validation failed: {e}")
|
2026-01-27 17:32:34 -07:00
|
|
|
|
|
|
|
|
|
|
if 'unauthorized' in error_msg or 'forbidden' in error_msg:
|
|
|
|
|
|
logger.error(f"🔑 Invalid API credentials detected")
|
|
|
|
|
|
logger.error(f"Please update your .env file with valid API keys")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Invalid API credentials detected")
|
2026-01-27 17:32:34 -07:00
|
|
|
|
else:
|
|
|
|
|
|
logger.error(f"Error: {e}")
|
|
|
|
|
|
logger.error(f"Please check your .env file and network connection")
|
|
|
|
|
|
|
2026-01-27 17:21:35 -07:00
|
|
|
|
return
|
|
|
|
|
|
|
2026-01-27 17:37:01 -07:00
|
|
|
|
try:
|
|
|
|
|
|
advanced_backtest_strategy()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"⚠️ Backtest skipped: {e}")
|
|
|
|
|
|
logger.info(f"ℹ️ Continuing without backtest - this is optional")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Backtest skipped: {e}")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
last_reset_date = None
|
|
|
|
|
|
trades_today = 0
|
2026-01-26 18:44:08 -07:00
|
|
|
|
|
2026-01-26 18:13:13 -07:00
|
|
|
|
try:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
while True:
|
|
|
|
|
|
debug_print("=== NEW MAIN LOOP ITERATION ===")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
try:
|
2026-01-29 17:56:44 -07:00
|
|
|
|
current_date = datetime.now(EASTERN).date()
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if last_reset_date != current_date:
|
|
|
|
|
|
trades_today = 0
|
|
|
|
|
|
last_reset_date = current_date
|
|
|
|
|
|
logger.info(f"📅 New day: {current_date}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"New day: {current_date}, resetting counters")
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-30 11:39:44 -07:00
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
delattr(scale_out_profit_taking, "target_1_hit")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
except AttributeError:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pass
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Reset target_1_hit attribute")
|
2026-01-30 11:39:44 -07:00
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
delattr(atr_based_trailing_stop, "trailing_stop")
|
|
|
|
|
|
|
|
|
|
|
|
except AttributeError:
|
|
|
|
|
|
|
|
|
|
|
|
pass
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Reset trailing_stop attribute")
|
2026-01-30 11:36:16 -07:00
|
|
|
|
if hasattr(main, 'peak_equity'):
|
|
|
|
|
|
delattr(main, 'peak_equity')
|
|
|
|
|
|
debug_print("Reset peak_equity attribute")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
|
|
|
|
|
if should_skip_trading_day():
|
2026-01-29 17:56:44 -07:00
|
|
|
|
day_name = datetime.now(EASTERN).strftime("%A")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info(f"📅 Skipping {day_name} - monitoring mode")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Skipping trading today ({day_name})")
|
|
|
|
|
|
time.sleep(3600)
|
2026-01-26 20:30:24 -07:00
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
market_info = get_market_status()
|
|
|
|
|
|
|
|
|
|
|
|
if market_info['status'] == 'closed':
|
|
|
|
|
|
logger.info(f"🏛️ Market closed")
|
|
|
|
|
|
logger.info(f"📅 Next open: {format_market_time(market_info['next_event'])}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Market closed, waiting for open...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
wait_until_market_open()
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"🏛️ Market OPEN - starting session")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("=== MARKET OPEN - STARTING SESSION ===")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
|
|
|
|
|
opening_equity = fetch_equity()
|
|
|
|
|
|
if opening_equity == 0:
|
|
|
|
|
|
logger.error("💥 No equity. Waiting 5 min...")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("No equity detected, waiting 5 minutes...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
time.sleep(300)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"💰 Opening equity: ${opening_equity:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Opening equity: ${opening_equity:.2f}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
|
|
|
|
|
vix_level = get_vix_level()
|
|
|
|
|
|
logger.info(f"📊 VIX: {vix_level:.1f}")
|
|
|
|
|
|
|
|
|
|
|
|
sma_200_trend = check_200_sma_filter(SYMBOL)
|
|
|
|
|
|
logger.info(f"📈 200 SMA: {sma_200_trend.upper()}")
|
|
|
|
|
|
|
2026-01-30 09:49:14 -07:00
|
|
|
|
short_status = "ON" if ENABLE_SHORT_SELLING else "OFF"
|
|
|
|
|
|
logger.info(f"⚙️ Config: {SYMBOL}, Risk={RISK_PER_TRADE:.2%}, Trades={trades_today}/{MAX_TRADES_PER_DAY}, Shorts={short_status}")
|
|
|
|
|
|
debug_print(f"Config: SYMBOL={SYMBOL}, RISK={RISK_PER_TRADE:.2%}, TRADES={trades_today}/{MAX_TRADES_PER_DAY}, SHORT_SELLING={ENABLE_SHORT_SELLING}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
|
|
|
|
|
trade_count = 0
|
|
|
|
|
|
entry_price = 0
|
|
|
|
|
|
entry_time = None
|
|
|
|
|
|
stop_loss = 0
|
|
|
|
|
|
position_active = False
|
|
|
|
|
|
position_type = None
|
|
|
|
|
|
total_pnl = 0
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("--- Session loop iteration ---")
|
|
|
|
|
|
|
2026-01-27 17:09:01 -07:00
|
|
|
|
try:
|
|
|
|
|
|
clock = api.get_clock()
|
|
|
|
|
|
if not clock.is_open:
|
|
|
|
|
|
logger.info("❌ Market closed")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Market closed, exiting session loop")
|
2026-01-27 17:09:01 -07:00
|
|
|
|
break
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"⚠️ Clock check failed: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Clock check failed: {e}, waiting 1 minute")
|
2026-01-27 17:09:01 -07:00
|
|
|
|
time.sleep(60)
|
|
|
|
|
|
continue
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
2026-01-29 17:56:44 -07:00
|
|
|
|
if datetime.now(EASTERN).date() != current_date:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info("📅 Day changed - resetting")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Day changed, exiting session loop")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
current_equity = fetch_equity()
|
2026-01-30 11:36:16 -07:00
|
|
|
|
|
|
|
|
|
|
if not hasattr(main, 'peak_equity'):
|
|
|
|
|
|
main.peak_equity = opening_equity
|
|
|
|
|
|
if current_equity > main.peak_equity:
|
|
|
|
|
|
main.peak_equity = current_equity
|
|
|
|
|
|
|
|
|
|
|
|
drawdown = (main.peak_equity - current_equity) / main.peak_equity
|
|
|
|
|
|
debug_print(f"Drawdown check: peak=${main.peak_equity:.2f}, current=${current_equity:.2f}, drawdown={drawdown:.2%}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
|
|
|
|
|
if drawdown > MAX_DRAWDOWN:
|
|
|
|
|
|
logger.error(f"💸 Max drawdown: {drawdown:.2%}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Max drawdown exceeded: {drawdown:.2%} > {MAX_DRAWDOWN:.2%}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if not should_trade_based_on_market_hours():
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Outside trading hours, sleeping 5 minutes")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
time.sleep(300)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if not pdt_allows_new_trade():
|
|
|
|
|
|
logger.error("🛑 PDT violation")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("PDT violation detected, breaking")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
current_price = get_current_price(SYMBOL)
|
|
|
|
|
|
if current_price == 0:
|
|
|
|
|
|
logger.warning("⚠️ No price, retrying...")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("No price data, waiting 1 minute")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
time.sleep(60)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if position_active:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Managing active position: type={position_type}, entry=${entry_price:.2f}")
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if entry_time:
|
2026-01-29 17:56:44 -07:00
|
|
|
|
time_in_trade = (datetime.now(EASTERN) - entry_time).total_seconds()
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Time in trade: {time_in_trade:.0f}s (max: {MAX_HOLD_TIME}s)")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if time_in_trade > MAX_HOLD_TIME:
|
|
|
|
|
|
logger.info(f"⏰ Max hold time ({MAX_HOLD_TIME//60} min)")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Max hold time exceeded, closing position")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
qty = current_position_qty(SYMBOL)
|
2026-01-30 09:49:14 -07:00
|
|
|
|
if qty != 0:
|
|
|
|
|
|
if position_type == 'long':
|
|
|
|
|
|
submit_market_sell(SYMBOL, qty)
|
|
|
|
|
|
else:
|
|
|
|
|
|
submit_buy_to_cover(SYMBOL, abs(qty))
|
2026-01-26 20:30:24 -07:00
|
|
|
|
position_active = False
|
|
|
|
|
|
trade_count += 1
|
2026-01-30 11:39:44 -07:00
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
delattr(scale_out_profit_taking, "target_1_hit")
|
|
|
|
|
|
|
|
|
|
|
|
except AttributeError:
|
|
|
|
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
delattr(atr_based_trailing_stop, "trailing_stop")
|
|
|
|
|
|
|
|
|
|
|
|
except AttributeError:
|
|
|
|
|
|
|
|
|
|
|
|
pass
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Sleeping {seconds_to_human_readable(POLL_INTERVAL)} after exit")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
time.sleep(POLL_INTERVAL)
|
|
|
|
|
|
continue
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
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
|
2026-01-30 09:49:14 -07:00
|
|
|
|
if position_type == 'long':
|
|
|
|
|
|
trade_pnl = (current_price - entry_price) * 100
|
|
|
|
|
|
else:
|
|
|
|
|
|
trade_pnl = (entry_price - current_price) * 100
|
2026-01-26 20:30:24 -07:00
|
|
|
|
total_pnl += trade_pnl
|
|
|
|
|
|
logger.info(f"✅ Position closed (PnL: ${trade_pnl:.2f})")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Position fully closed, PnL: ${trade_pnl:.2f}")
|
2026-01-30 11:39:44 -07:00
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
delattr(scale_out_profit_taking, "target_1_hit")
|
|
|
|
|
|
|
|
|
|
|
|
except AttributeError:
|
|
|
|
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
delattr(atr_based_trailing_stop, "trailing_stop")
|
|
|
|
|
|
|
|
|
|
|
|
except AttributeError:
|
|
|
|
|
|
|
|
|
|
|
|
pass
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Sleeping {seconds_to_human_readable(POLL_INTERVAL)} after exit")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
time.sleep(POLL_INTERVAL)
|
|
|
|
|
|
continue
|
2026-01-26 19:47:58 -07:00
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
if atr_based_trailing_stop(SYMBOL, entry_price, current_price, stop_loss, position_type):
|
|
|
|
|
|
qty = current_position_qty(SYMBOL)
|
2026-01-30 09:49:14 -07:00
|
|
|
|
if qty != 0:
|
|
|
|
|
|
if position_type == 'long':
|
|
|
|
|
|
submit_market_sell(SYMBOL, qty)
|
|
|
|
|
|
else:
|
|
|
|
|
|
submit_buy_to_cover(SYMBOL, abs(qty))
|
2026-01-26 20:30:24 -07:00
|
|
|
|
position_active = False
|
|
|
|
|
|
trade_count += 1
|
|
|
|
|
|
logger.info(f"🛑 Stop hit")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Stop hit, position closed")
|
2026-01-30 11:39:44 -07:00
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
delattr(scale_out_profit_taking, "target_1_hit")
|
|
|
|
|
|
|
|
|
|
|
|
except AttributeError:
|
|
|
|
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
delattr(atr_based_trailing_stop, "trailing_stop")
|
|
|
|
|
|
|
|
|
|
|
|
except AttributeError:
|
|
|
|
|
|
|
|
|
|
|
|
pass
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Sleeping {seconds_to_human_readable(POLL_INTERVAL)} after exit")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
time.sleep(POLL_INTERVAL)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if trades_today >= MAX_TRADES_PER_DAY:
|
|
|
|
|
|
logger.info(f"📊 Daily limit ({MAX_TRADES_PER_DAY}) - monitoring only")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Daily trade limit reached ({trades_today}/{MAX_TRADES_PER_DAY})")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
time.sleep(POLL_INTERVAL)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-01-30 11:36:16 -07:00
|
|
|
|
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
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
|
|
|
|
|
bars = get_recent_bars(SYMBOL, 50)
|
|
|
|
|
|
if bars is not None:
|
|
|
|
|
|
regime = detect_market_regime(bars)
|
|
|
|
|
|
else:
|
|
|
|
|
|
regime = 'unknown'
|
|
|
|
|
|
|
|
|
|
|
|
if signal in ['buy', 'sell'] and not position_active:
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Signal detected: {signal}, executing trade...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
buying_power = fetch_buying_power()
|
|
|
|
|
|
|
|
|
|
|
|
position_size = calculate_position_size(current_equity, signal_stop_loss, current_price, regime)
|
|
|
|
|
|
|
|
|
|
|
|
if buying_power >= position_size:
|
2026-01-30 11:36:16 -07:00
|
|
|
|
execution_price = False
|
2026-01-30 11:31:03 -07:00
|
|
|
|
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)
|
2026-01-30 11:36:16 -07:00
|
|
|
|
elif signal == 'sell':
|
2026-01-30 11:31:03 -07:00
|
|
|
|
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)
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
|
|
|
|
|
if execution_price:
|
|
|
|
|
|
trade_count += 1
|
|
|
|
|
|
trades_today += 1
|
|
|
|
|
|
entry_price = execution_price
|
2026-01-29 17:56:44 -07:00
|
|
|
|
entry_time = datetime.now(EASTERN)
|
2026-01-26 20:30:24 -07:00
|
|
|
|
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})")
|
|
|
|
|
|
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Trade executed: entry=${entry_price:.2f}, stop=${stop_loss:.2f}, regime={regime}")
|
|
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
atr_based_trailing_stop.trailing_stop = stop_loss
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Trailing stop initialized: ${stop_loss:.2f}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
else:
|
|
|
|
|
|
logger.warning(f"⚠️ Insufficient buying power: ${buying_power:.2f} < ${position_size:.2f}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Insufficient buying power: ${buying_power:.2f} < ${position_size:.2f}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
|
|
|
|
|
position_status = f"{position_type.upper()}" if position_active else "FLAT"
|
2026-01-27 17:09:01 -07:00
|
|
|
|
try:
|
2026-01-30 11:39:44 -07:00
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
2026-01-27 17:09:01 -07:00
|
|
|
|
except:
|
2026-01-30 11:39:44 -07:00
|
|
|
|
|
|
|
|
|
|
current_time = datetime.now(EASTERN).strftime("%I:%M:%S %p ET")
|
2026-01-27 17:09:01 -07:00
|
|
|
|
|
2026-01-26 20:30:24 -07:00
|
|
|
|
hourly_trend = check_multiframe_confluence(SYMBOL)
|
|
|
|
|
|
|
|
|
|
|
|
status_msg = f"⏱️ {current_time} | {position_status} | {regime.upper()}"
|
|
|
|
|
|
if position_active:
|
|
|
|
|
|
pnl_pct = ((current_price - entry_price) / entry_price) * 100 if position_type == 'long' else ((entry_price - current_price) / entry_price) * 100
|
|
|
|
|
|
status_msg += f" | PnL: {pnl_pct:+.2f}%"
|
|
|
|
|
|
status_msg += f" | H:{hourly_trend} | VIX:{vix_level:.1f} | {trades_today}/{MAX_TRADES_PER_DAY}"
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(status_msg)
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Sleeping {seconds_to_human_readable(POLL_INTERVAL)}...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
time.sleep(POLL_INTERVAL)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info("🔚 Session ending...")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Session ending, closing all positions...")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
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...")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Day complete. Trades: {trade_count}, PnL: ${session_pnl:+.2f}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
|
|
|
|
|
|
time.sleep(3600)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"💥 Session error: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Session error: {e}")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
import traceback
|
|
|
|
|
|
logger.error(traceback.format_exc())
|
|
|
|
|
|
logger.info("⏳ Waiting 5 min before retry...")
|
|
|
|
|
|
time.sleep(300)
|
|
|
|
|
|
|
2026-01-26 18:13:13 -07:00
|
|
|
|
except KeyboardInterrupt:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info("🛑 User interrupt")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("User interrupt detected")
|
2026-01-26 20:30:24 -07:00
|
|
|
|
close_all_positions()
|
2026-01-26 18:13:13 -07:00
|
|
|
|
except Exception as e:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.error(f"💥 Fatal error: {e}")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print(f"Fatal error: {e}")
|
2026-01-26 18:44:08 -07:00
|
|
|
|
import traceback
|
|
|
|
|
|
logger.error(traceback.format_exc())
|
2026-01-26 18:13:13 -07:00
|
|
|
|
finally:
|
2026-01-26 20:30:24 -07:00
|
|
|
|
logger.info("🔚 Shutdown")
|
2026-01-28 11:57:33 -07:00
|
|
|
|
debug_print("Script shutdown")
|
2026-01-26 18:13:13 -07:00
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2026-01-27 17:32:34 -07:00
|
|
|
|
main()
|
2026-01-28 15:03:56 -07:00
|
|
|
|
|