Alpaca Trader
This commit is contained in:
commit
be9f5fb95b
15 files changed
+1247
No files matched your search
@@ -0,0 +1,70 @@
|
||||
# Alpaca Trader
|
||||
|
||||
Automated day trading bot for Alpaca Markets using technical indicators and risk management.
|
||||
|
||||
## Features
|
||||
|
||||
- Multiple technical indicators (SMA/EMA, RSI, ADX, ATR, MACD, Bollinger Bands)
|
||||
- Advanced risk management with trailing stops and profit targets
|
||||
- Market regime detection (trend/range/high vol/low vol)
|
||||
- Multi-timeframe confluence analysis
|
||||
- Position sizing based on account equity and risk per trade
|
||||
- Support for both long and short positions
|
||||
- Configurable via JSON config file
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- Alpaca Markets account (paper or live)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Create `.env` file in `alpaca_trader/` directory:
|
||||
```
|
||||
APCA_API_KEY_ID="your_api_key"
|
||||
APCA_API_SECRET_KEY="your_secret_key"
|
||||
APCA_API_BASE_URL="https://paper-api.alpaca.markets"
|
||||
```
|
||||
|
||||
2. Modify `config.json` to adjust trading parameters:
|
||||
- `SYMBOL`: Stock to trade (default: SPY)
|
||||
- `RISK_PER_TRADE`: Risk per trade as % of equity (default: 0.005)
|
||||
- `MAX_TRADES_PER_DAY`: Daily trade limit (default: 5)
|
||||
- `ENABLE_SHORT_SELLING`: Enable/disable short positions (default: true)
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
python run.py
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```bash
|
||||
python -m alpaca_trader
|
||||
```
|
||||
|
||||
## Key Parameters
|
||||
|
||||
- **SHORT_WINDOW**: Fast moving average period (default: 20)
|
||||
- **LONG_WINDOW**: Slow moving average period (default: 50)
|
||||
- **ADX_THRESHOLD**: Minimum ADX for trend detection (default: 20)
|
||||
- **ATR_STOP_MULTIPLIER**: Stop loss distance in ATR units (default: 1.5)
|
||||
- **USE_TRAILING_STOP**: Enable trailing stop loss (default: true)
|
||||
- **PROFIT_TARGET_1**: First profit target in R (default: 1.5)
|
||||
- **PROFIT_TARGET_2**: Second profit target in R (default: 3.0)
|
||||
|
||||
## Logging
|
||||
|
||||
- `trading.log`: Main trading activity log
|
||||
- `debug.log`: Detailed debug information
|
||||
|
||||
## Warning
|
||||
|
||||
This is for educational purposes. Test thoroughly in paper trading before using real capital. Trading involves risk of loss.
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
__all__ = ["cli", "engine", "indicators"]
|
||||
@@ -0,0 +1,4 @@
|
||||
from .engine import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,91 @@
|
||||
import time
|
||||
import backoff
|
||||
import alpaca_trade_api as tradeapi
|
||||
|
||||
|
||||
class AlpacaClient:
|
||||
def __init__(self, api_key_id, api_secret_key, base_url, api_version="v2"):
|
||||
self.api = tradeapi.REST(api_key_id, api_secret_key, base_url, api_version=api_version)
|
||||
|
||||
@backoff.on_exception(backoff.expo, (tradeapi.rest.APIError, ConnectionError), max_tries=5, jitter=backoff.full_jitter)
|
||||
def get_account(self):
|
||||
return self.api.get_account()
|
||||
|
||||
@backoff.on_exception(backoff.expo, (tradeapi.rest.APIError, ConnectionError), max_tries=5, jitter=backoff.full_jitter)
|
||||
def get_clock(self):
|
||||
return self.api.get_clock()
|
||||
|
||||
@backoff.on_exception(backoff.expo, (tradeapi.rest.APIError, ConnectionError), max_tries=5, jitter=backoff.full_jitter)
|
||||
def get_bars(self, symbol, timeframe, **kwargs):
|
||||
bars = self.api.get_bars(symbol, timeframe, **kwargs)
|
||||
if bars is None:
|
||||
return None
|
||||
return bars.df
|
||||
|
||||
@backoff.on_exception(backoff.expo, (tradeapi.rest.APIError, ConnectionError), max_tries=5, jitter=backoff.full_jitter)
|
||||
def get_latest_quote(self, symbol):
|
||||
return self.api.get_latest_quote(symbol)
|
||||
|
||||
@backoff.on_exception(backoff.expo, (tradeapi.rest.APIError, ConnectionError), max_tries=5, jitter=backoff.full_jitter)
|
||||
def submit_order(self, **kwargs):
|
||||
return self.api.submit_order(**kwargs)
|
||||
|
||||
@backoff.on_exception(backoff.expo, (tradeapi.rest.APIError, ConnectionError), max_tries=5, jitter=backoff.full_jitter)
|
||||
def get_order(self, order_id):
|
||||
return self.api.get_order(order_id)
|
||||
|
||||
@backoff.on_exception(backoff.expo, (tradeapi.rest.APIError, ConnectionError), max_tries=5, jitter=backoff.full_jitter)
|
||||
def cancel_order(self, order_id):
|
||||
return self.api.cancel_order(order_id)
|
||||
|
||||
@backoff.on_exception(backoff.expo, (tradeapi.rest.APIError, ConnectionError), max_tries=5, jitter=backoff.full_jitter)
|
||||
def list_positions(self):
|
||||
return self.api.list_positions()
|
||||
|
||||
@backoff.on_exception(backoff.expo, (tradeapi.rest.APIError, ConnectionError), max_tries=5, jitter=backoff.full_jitter)
|
||||
def list_orders(self, **kwargs):
|
||||
return self.api.list_orders(**kwargs)
|
||||
|
||||
@backoff.on_exception(backoff.expo, (tradeapi.rest.APIError, ConnectionError), max_tries=5, jitter=backoff.full_jitter)
|
||||
def close_all_positions(self):
|
||||
return self.api.close_all_positions()
|
||||
|
||||
@backoff.on_exception(backoff.expo, (tradeapi.rest.APIError, ConnectionError), max_tries=5, jitter=backoff.full_jitter)
|
||||
def get_position(self, symbol):
|
||||
return self.api.get_position(symbol)
|
||||
|
||||
def place_order(self, symbol, side, notional, limit_price, limit_order_timeout):
|
||||
quote = self.get_latest_quote(symbol)
|
||||
if quote is None:
|
||||
return None
|
||||
bid_price = getattr(quote, 'bid_price', None)
|
||||
ask_price = getattr(quote, 'ask_price', None)
|
||||
if limit_price:
|
||||
price_source = limit_price
|
||||
else:
|
||||
price_source = bid_price if side == "buy" else ask_price
|
||||
if price_source is None:
|
||||
return None
|
||||
shares = int(notional / price_source)
|
||||
if shares == 0:
|
||||
return None
|
||||
if limit_price:
|
||||
order = self.submit_order(symbol=symbol, qty=shares, side=side, type="limit", limit_price=round(limit_price, 2), time_in_force="day")
|
||||
start = time.time()
|
||||
while time.time() - start < limit_order_timeout:
|
||||
status = self.get_order(order.id)
|
||||
if status.status == "filled":
|
||||
return float(status.filled_avg_price)
|
||||
if status.status in {"cancelled", "expired", "rejected"}:
|
||||
return None
|
||||
time.sleep(2)
|
||||
self.cancel_order(order.id)
|
||||
return None
|
||||
order = self.submit_order(symbol=symbol, qty=shares, side=side, type="market", time_in_force="day")
|
||||
status = self.get_order(order.id)
|
||||
while status.status not in {"filled", "cancelled", "expired", "rejected"}:
|
||||
time.sleep(0.5)
|
||||
status = self.get_order(order.id)
|
||||
if status.status == "filled":
|
||||
return float(status.filled_avg_price)
|
||||
return None
|
||||
@@ -0,0 +1,5 @@
|
||||
from .engine import run
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"DEBUG_MODE": true,
|
||||
"SYMBOL": "SPY",
|
||||
"BAR_TIMEFRAME": "5Min",
|
||||
"RISK_PER_TRADE": 0.005,
|
||||
"SHORT_WINDOW": 20,
|
||||
"LONG_WINDOW": 50,
|
||||
"MIN_NOTIONAL": 1.0,
|
||||
"POLL_INTERVAL": 60,
|
||||
"MAX_DRAWDOWN": 0.12,
|
||||
"PDT_RULE": true,
|
||||
"USE_TRAILING_STOP": true,
|
||||
"PROFIT_TARGET_1": 1.5,
|
||||
"PROFIT_TARGET_2": 3.0,
|
||||
"VOLATILITY_ADJUSTMENT": true,
|
||||
"MARKET_HOURS_FILTER": false,
|
||||
"ENABLE_SLIPPAGE": true,
|
||||
"SLIPPAGE_PCT": 0.0005,
|
||||
"COMMISSION_PCT": 0.0005,
|
||||
"MIN_SIGNAL_STRENGTH": 0.5,
|
||||
"BACKTEST_DAYS": 90,
|
||||
"USE_LIMIT_ORDERS": true,
|
||||
"LIMIT_ORDER_TIMEOUT": 60,
|
||||
"ADX_THRESHOLD": 20,
|
||||
"VOLUME_MULTIPLIER": 0.5,
|
||||
"ATR_STOP_MULTIPLIER": 1.5,
|
||||
"MAX_HOLD_TIME": 7200,
|
||||
"REGIME_DETECTION": true,
|
||||
"MULTIFRAME_FILTER": true,
|
||||
"BB_WINDOW": 20,
|
||||
"BB_STD": 2.0,
|
||||
"USE_EMA": true,
|
||||
"REQUIRE_CANDLE_PATTERN": false,
|
||||
"USE_PIVOT_POINTS": false,
|
||||
"VIX_THRESHOLD": 20,
|
||||
"USE_VIX_FILTER": true,
|
||||
"USE_FIBONACCI": false,
|
||||
"MAX_TRADES_PER_DAY": 5,
|
||||
"SKIP_MONDAYS_FRIDAYS": false,
|
||||
"USE_200_SMA_FILTER": true,
|
||||
"REQUIRE_MACD_CONFIRMATION": false,
|
||||
"MIN_RISK_REWARD": 1.5,
|
||||
"PULLBACK_PERCENTAGE": 0.382,
|
||||
"ENABLE_SHORT_SELLING": true
|
||||
}
|
||||
@@ -0,0 +1,826 @@
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import pytz
|
||||
|
||||
from .api import AlpacaClient
|
||||
from .indicators import sma, ema, rsi, atr, adx, macd, bollinger
|
||||
from .filters import check_volume, check_candle_pattern, check_macd_confirmation, check_200_sma_filter, check_multiframe_confluence as _check_multiframe_confluence, detect_market_regime, get_vix
|
||||
from .utils import EASTERN, seconds_to_human_readable
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
LOG_PATH = SCRIPT_DIR / "trading.log"
|
||||
DEBUG_LOG_PATH = SCRIPT_DIR / "debug.log"
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(LOG_PATH, mode='a'),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
debug_logger = logging.getLogger('debug')
|
||||
debug_logger.setLevel(logging.DEBUG)
|
||||
debug_handler = logging.FileHandler(DEBUG_LOG_PATH, mode='a')
|
||||
debug_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
|
||||
debug_logger.addHandler(debug_handler)
|
||||
debug_logger.propagate = False
|
||||
|
||||
CONFIG_PATH = SCRIPT_DIR / "config.json"
|
||||
ENV_PATH = SCRIPT_DIR / ".env"
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"DEBUG_MODE": True,
|
||||
"SYMBOL": "SPY",
|
||||
"BAR_TIMEFRAME": "5Min",
|
||||
"RISK_PER_TRADE": 0.005,
|
||||
"SHORT_WINDOW": 20,
|
||||
"LONG_WINDOW": 50,
|
||||
"MIN_NOTIONAL": 1.0,
|
||||
"POLL_INTERVAL": 60,
|
||||
"MAX_DRAWDOWN": 0.12,
|
||||
"PDT_RULE": True,
|
||||
"USE_TRAILING_STOP": True,
|
||||
"PROFIT_TARGET_1": 1.5,
|
||||
"PROFIT_TARGET_2": 3.0,
|
||||
"VOLATILITY_ADJUSTMENT": True,
|
||||
"MARKET_HOURS_FILTER": False,
|
||||
"ENABLE_SLIPPAGE": True,
|
||||
"SLIPPAGE_PCT": 0.0005,
|
||||
"COMMISSION_PCT": 0.0005,
|
||||
"MIN_SIGNAL_STRENGTH": 0.5,
|
||||
"BACKTEST_DAYS": 90,
|
||||
"USE_LIMIT_ORDERS": True,
|
||||
"LIMIT_ORDER_TIMEOUT": 60,
|
||||
"ADX_THRESHOLD": 20,
|
||||
"VOLUME_MULTIPLIER": 0.5,
|
||||
"ATR_STOP_MULTIPLIER": 1.5,
|
||||
"MAX_HOLD_TIME": 7200,
|
||||
"REGIME_DETECTION": True,
|
||||
"MULTIFRAME_FILTER": True,
|
||||
"BB_WINDOW": 20,
|
||||
"BB_STD": 2.0,
|
||||
"USE_EMA": True,
|
||||
"REQUIRE_CANDLE_PATTERN": False,
|
||||
"USE_PIVOT_POINTS": False,
|
||||
"VIX_THRESHOLD": 20,
|
||||
"USE_VIX_FILTER": True,
|
||||
"USE_FIBONACCI": False,
|
||||
"MAX_TRADES_PER_DAY": 5,
|
||||
"SKIP_MONDAYS_FRIDAYS": False,
|
||||
"USE_200_SMA_FILTER": True,
|
||||
"REQUIRE_MACD_CONFIRMATION": False,
|
||||
"MIN_RISK_REWARD": 1.5,
|
||||
"PULLBACK_PERCENTAGE": 0.382,
|
||||
"ENABLE_SHORT_SELLING": False
|
||||
}
|
||||
|
||||
if not ENV_PATH.exists():
|
||||
placeholder = (
|
||||
'APCA_API_KEY_ID="YOUR_REAL_KEY_ID"\n'
|
||||
'APCA_API_SECRET_KEY="YOUR_REAL_SECRET_KEY"\n'
|
||||
'APCA_API_BASE_URL="https://paper-api.alpaca.markets"\n'
|
||||
)
|
||||
with open(ENV_PATH, "w") as f:
|
||||
f.write(placeholder)
|
||||
load_dotenv(ENV_PATH)
|
||||
logger.warning("⚠️ .env file was missing – a placeholder has been created at:")
|
||||
logger.warning(f" {ENV_PATH}")
|
||||
logger.warning(" Edit this file and replace the placeholder values with your real Alpaca API credentials.")
|
||||
logger.warning(' Example lines to replace:')
|
||||
logger.warning(' APCA_API_KEY_ID="YOUR_REAL_KEY_ID"')
|
||||
logger.warning(' APCA_API_SECRET_KEY="YOUR_REAL_SECRET_KEY"')
|
||||
logger.warning(' After editing, restart the script.')
|
||||
sys.exit(1)
|
||||
else:
|
||||
load_dotenv(ENV_PATH)
|
||||
|
||||
if CONFIG_PATH.exists():
|
||||
try:
|
||||
with open(CONFIG_PATH, "r") as f:
|
||||
config = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
print("⚠️ config.json is invalid – recreating with defaults")
|
||||
config = DEFAULT_CONFIG.copy()
|
||||
with open(CONFIG_PATH, "w") as f:
|
||||
json.dump(DEFAULT_CONFIG, f, indent=4)
|
||||
else:
|
||||
with open(CONFIG_PATH, "w") as f:
|
||||
json.dump(DEFAULT_CONFIG, f, indent=4)
|
||||
config = DEFAULT_CONFIG.copy()
|
||||
print(f"✅ Created default config file at {CONFIG_PATH}")
|
||||
|
||||
DEBUG_MODE = bool(config.get("DEBUG_MODE", False))
|
||||
SYMBOL = config["SYMBOL"]
|
||||
BAR_TIMEFRAME = config.get("BAR_TIMEFRAME", "5Min")
|
||||
RISK_PER_TRADE = float(config["RISK_PER_TRADE"])
|
||||
SHORT_WINDOW = int(config["SHORT_WINDOW"])
|
||||
LONG_WINDOW = int(config["LONG_WINDOW"])
|
||||
|
||||
if SHORT_WINDOW >= LONG_WINDOW:
|
||||
logger.error(f"⚠️ Configuration error: SHORT_WINDOW ({SHORT_WINDOW}) must be less than LONG_WINDOW ({LONG_WINDOW})")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
test_client = AlpacaClient(
|
||||
os.getenv("APCA_API_KEY_ID"),
|
||||
os.getenv("APCA_API_SECRET_KEY"),
|
||||
os.getenv("APCA_API_BASE_URL", "https://paper-api.alpaca.markets"),
|
||||
api_version="v2"
|
||||
)
|
||||
test_client.get_account()
|
||||
logger.info("✅ API credentials validated")
|
||||
except Exception as e:
|
||||
logger.error(f"⚠️ Invalid API credentials: {e}")
|
||||
logger.error(" Please check your .env file and ensure your Alpaca API keys are correct")
|
||||
sys.exit(1)
|
||||
|
||||
MIN_NOTIONAL = float(config["MIN_NOTIONAL"])
|
||||
POLL_INTERVAL = int(config["POLL_INTERVAL"])
|
||||
MAX_DRAWDOWN = float(config["MAX_DRAWDOWN"])
|
||||
PDT_RULE = bool(config["PDT_RULE"])
|
||||
USE_TRAILING_STOP = bool(config["USE_TRAILING_STOP"])
|
||||
PROFIT_TARGET_1 = float(config["PROFIT_TARGET_1"])
|
||||
PROFIT_TARGET_2 = float(config["PROFIT_TARGET_2"])
|
||||
VOLATILITY_ADJUSTMENT = bool(config["VOLATILITY_ADJUSTMENT"])
|
||||
MARKET_HOURS_FILTER = bool(config["MARKET_HOURS_FILTER"])
|
||||
ENABLE_SLIPPAGE = bool(config["ENABLE_SLIPPAGE"])
|
||||
SLIPPAGE_PCT = float(config["SLIPPAGE_PCT"])
|
||||
COMMISSION_PCT = float(config["COMMISSION_PCT"])
|
||||
MIN_SIGNAL_STRENGTH = float(config["MIN_SIGNAL_STRENGTH"])
|
||||
BACKTEST_DAYS = int(config["BACKTEST_DAYS"])
|
||||
USE_LIMIT_ORDERS = bool(config["USE_LIMIT_ORDERS"])
|
||||
LIMIT_ORDER_TIMEOUT = int(config["LIMIT_ORDER_TIMEOUT"])
|
||||
ADX_THRESHOLD = float(config["ADX_THRESHOLD"])
|
||||
VOLUME_MULTIPLIER = float(config["VOLUME_MULTIPLIER"])
|
||||
ATR_STOP_MULTIPLIER = float(config["ATR_STOP_MULTIPLIER"])
|
||||
MAX_HOLD_TIME = int(config["MAX_HOLD_TIME"])
|
||||
REGIME_DETECTION = bool(config["REGIME_DETECTION"])
|
||||
MULTIFRAME_FILTER = bool(config["MULTIFRAME_FILTER"])
|
||||
BB_WINDOW = int(config["BB_WINDOW"])
|
||||
BB_STD = float(config["BB_STD"])
|
||||
USE_EMA = bool(config["USE_EMA"])
|
||||
REQUIRE_CANDLE_PATTERN = bool(config["REQUIRE_CANDLE_PATTERN"])
|
||||
USE_PIVOT_POINTS = bool(config["USE_PIVOT_POINTS"])
|
||||
VIX_THRESHOLD = float(config["VIX_THRESHOLD"])
|
||||
USE_VIX_FILTER = bool(config["USE_VIX_FILTER"])
|
||||
USE_FIBONACCI = bool(config["USE_FIBONACCI"])
|
||||
MAX_TRADES_PER_DAY = int(config["MAX_TRADES_PER_DAY"])
|
||||
SKIP_MONDAYS_FRIDAYS = bool(config["SKIP_MONDAYS_FRIDAYS"])
|
||||
USE_200_SMA_FILTER = bool(config["USE_200_SMA_FILTER"])
|
||||
REQUIRE_MACD_CONFIRMATION = bool(config["REQUIRE_MACD_CONFIRMATION"])
|
||||
MIN_RISK_REWARD = float(config["MIN_RISK_REWARD"])
|
||||
PULLBACK_PERCENTAGE = float(config["PULLBACK_PERCENTAGE"])
|
||||
ENABLE_SHORT_SELLING = bool(config.get("ENABLE_SHORT_SELLING", False))
|
||||
|
||||
api = AlpacaClient(
|
||||
os.getenv('APCA_API_KEY_ID'),
|
||||
os.getenv('APCA_API_SECRET_KEY'),
|
||||
os.getenv('APCA_API_BASE_URL'),
|
||||
api_version='v2'
|
||||
)
|
||||
|
||||
def debug_print(message):
|
||||
if DEBUG_MODE:
|
||||
debug_logger.debug(f"🔎 {message}")
|
||||
print(f"{datetime.now(EASTERN).strftime('%Y-%m-%d %H:%M:%S,%f')[:-3]} - DEBUG - 🔎 {message}", flush=True)
|
||||
|
||||
def fetch_equity():
|
||||
debug_print("Fetching account equity")
|
||||
account = api.get_account()
|
||||
equity = float(account.equity)
|
||||
debug_print(f"Current equity: ${equity:.2f}")
|
||||
return equity
|
||||
|
||||
def fetch_buying_power():
|
||||
debug_print("Fetching buying power")
|
||||
account = api.get_account()
|
||||
bp = float(account.buying_power)
|
||||
debug_print(f"Buying power: ${bp:.2f}")
|
||||
return bp
|
||||
|
||||
def get_recent_bars(symbol, limit=100):
|
||||
debug_print(f"Fetching {limit} bars for {symbol} ({BAR_TIMEFRAME})")
|
||||
try:
|
||||
bars = api.get_bars(symbol, BAR_TIMEFRAME, limit=limit)
|
||||
if bars is None or len(bars) == 0:
|
||||
debug_print(f"No bars returned for {symbol}")
|
||||
return None
|
||||
debug_print(f"Retrieved {len(bars)} bars")
|
||||
return bars
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching bars: {e}")
|
||||
debug_print(f"Error fetching bars: {e}")
|
||||
return None
|
||||
|
||||
def current_position_qty(symbol):
|
||||
debug_print(f"Checking position for {symbol}")
|
||||
try:
|
||||
positions = api.list_positions()
|
||||
for pos in positions:
|
||||
if pos.symbol == symbol:
|
||||
qty = float(pos.qty)
|
||||
debug_print(f"Found position: {qty} shares")
|
||||
return qty
|
||||
debug_print("No position found")
|
||||
return 0
|
||||
except Exception as e:
|
||||
debug_print(f"Error checking position: {e}")
|
||||
return 0
|
||||
|
||||
def close_all_positions():
|
||||
debug_print("Closing all positions")
|
||||
try:
|
||||
api.close_all_positions()
|
||||
logger.info("✅ All positions closed")
|
||||
debug_print("All positions closed successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing positions: {e}")
|
||||
debug_print(f"Error closing positions: {e}")
|
||||
|
||||
def get_bid_ask(symbol):
|
||||
debug_print(f"Getting bid/ask for {symbol}")
|
||||
try:
|
||||
quote = api.get_latest_quote(symbol)
|
||||
bid = float(quote.bid_price)
|
||||
ask = float(quote.ask_price)
|
||||
debug_print(f"Bid: ${bid:.2f}, Ask: ${ask:.2f}")
|
||||
return bid, ask
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting quote: {e}")
|
||||
debug_print(f"Error getting quote: {e}")
|
||||
return None, None
|
||||
|
||||
def submit_market_buy(symbol, position_size):
|
||||
debug_print(f"Submitting market buy order: {symbol}, size=${position_size:.2f}")
|
||||
try:
|
||||
execution_price = api.place_order(symbol, "buy", position_size, None, LIMIT_ORDER_TIMEOUT)
|
||||
if execution_price:
|
||||
logger.info(f"🟢 BUY {symbol} @ ${execution_price:.2f}")
|
||||
debug_print(f"Buy order filled @ ${execution_price:.2f}")
|
||||
return execution_price
|
||||
except Exception as e:
|
||||
logger.error(f"Buy order failed: {e}")
|
||||
debug_print(f"Buy order failed: {e}")
|
||||
return None
|
||||
|
||||
def submit_market_sell(symbol, qty):
|
||||
debug_print(f"Submitting market sell order: {symbol}, qty={qty}")
|
||||
try:
|
||||
shares = int(qty)
|
||||
order = api.submit_order(symbol=symbol, qty=shares, side="sell", type="market", time_in_force="day")
|
||||
status = api.get_order(order.id)
|
||||
while status.status not in {"filled", "cancelled", "expired", "rejected"}:
|
||||
time.sleep(0.5)
|
||||
status = api.get_order(order.id)
|
||||
if status.status == "filled":
|
||||
price = float(status.filled_avg_price)
|
||||
logger.info(f"🔴 SELL {symbol} @ ${price:.2f}")
|
||||
debug_print(f"Sell order filled @ ${price:.2f}")
|
||||
return price
|
||||
except Exception as e:
|
||||
logger.error(f"Sell order failed: {e}")
|
||||
debug_print(f"Sell order failed: {e}")
|
||||
return None
|
||||
|
||||
def submit_limit_buy(symbol, position_size, limit_price):
|
||||
debug_print(f"Submitting limit buy: {symbol}, size=${position_size:.2f}, limit=${limit_price:.2f}")
|
||||
try:
|
||||
execution_price = api.place_order(symbol, "buy", position_size, limit_price, LIMIT_ORDER_TIMEOUT)
|
||||
if execution_price:
|
||||
logger.info(f"🟢 BUY {symbol} @ ${execution_price:.2f}")
|
||||
debug_print(f"Limit buy filled @ ${execution_price:.2f}")
|
||||
else:
|
||||
debug_print("Limit order timeout, attempting market order")
|
||||
execution_price = api.place_order(symbol, "buy", position_size, None, LIMIT_ORDER_TIMEOUT)
|
||||
if execution_price:
|
||||
logger.info(f"🟢 BUY {symbol} @ ${execution_price:.2f} (market)")
|
||||
debug_print(f"Market buy filled @ ${execution_price:.2f}")
|
||||
return execution_price
|
||||
except Exception as e:
|
||||
logger.error(f"Buy order failed: {e}")
|
||||
debug_print(f"Buy order failed: {e}")
|
||||
return None
|
||||
|
||||
def submit_short_sell(symbol, position_size):
|
||||
debug_print(f"Submitting short sell: {symbol}, size=${position_size:.2f}")
|
||||
try:
|
||||
execution_price = api.place_order(symbol, "sell", position_size, None, LIMIT_ORDER_TIMEOUT)
|
||||
if execution_price:
|
||||
logger.info(f"🔴 SHORT {symbol} @ ${execution_price:.2f}")
|
||||
debug_print(f"Short sell filled @ ${execution_price:.2f}")
|
||||
return execution_price
|
||||
except Exception as e:
|
||||
logger.error(f"Short sell failed: {e}")
|
||||
debug_print(f"Short sell failed: {e}")
|
||||
return None
|
||||
|
||||
def submit_limit_short_sell(symbol, position_size, limit_price):
|
||||
debug_print(f"Submitting limit short: {symbol}, size=${position_size:.2f}, limit=${limit_price:.2f}")
|
||||
try:
|
||||
execution_price = api.place_order(symbol, "sell", position_size, limit_price, LIMIT_ORDER_TIMEOUT)
|
||||
if execution_price:
|
||||
logger.info(f"🔴 SHORT {symbol} @ ${execution_price:.2f}")
|
||||
debug_print(f"Limit short filled @ ${execution_price:.2f}")
|
||||
else:
|
||||
debug_print("Limit order timeout, attempting market order")
|
||||
execution_price = api.place_order(symbol, "sell", position_size, None, LIMIT_ORDER_TIMEOUT)
|
||||
if execution_price:
|
||||
logger.info(f"🔴 SHORT {symbol} @ ${execution_price:.2f} (market)")
|
||||
debug_print(f"Market short filled @ ${execution_price:.2f}")
|
||||
return execution_price
|
||||
except Exception as e:
|
||||
logger.error(f"Short sell failed: {e}")
|
||||
debug_print(f"Short sell failed: {e}")
|
||||
return None
|
||||
|
||||
def submit_buy_to_cover(symbol, qty):
|
||||
debug_print(f"Submitting buy to cover: {symbol}, qty={qty}")
|
||||
try:
|
||||
shares = int(qty)
|
||||
order = api.submit_order(symbol=symbol, qty=shares, side="buy", type="market", time_in_force="day")
|
||||
status = api.get_order(order.id)
|
||||
while status.status not in {"filled", "cancelled", "expired", "rejected"}:
|
||||
time.sleep(0.5)
|
||||
status = api.get_order(order.id)
|
||||
if status.status == "filled":
|
||||
price = float(status.filled_avg_price)
|
||||
logger.info(f"🟢 COVER {symbol} @ ${price:.2f}")
|
||||
debug_print(f"Buy to cover filled @ ${price:.2f}")
|
||||
return price
|
||||
except Exception as e:
|
||||
logger.error(f"Buy to cover failed: {e}")
|
||||
debug_print(f"Buy to cover failed: {e}")
|
||||
return None
|
||||
|
||||
def calculate_position_size(equity, stop_loss, current_price):
|
||||
debug_print(f"Calculating position size: equity=${equity:.2f}, stop=${stop_loss:.2f}, price=${current_price:.2f}")
|
||||
risk_amount = equity * RISK_PER_TRADE
|
||||
price_risk = abs(current_price - stop_loss)
|
||||
if price_risk == 0:
|
||||
debug_print("Price risk is zero, returning MIN_NOTIONAL")
|
||||
return MIN_NOTIONAL
|
||||
shares = risk_amount / price_risk
|
||||
position_value = shares * current_price
|
||||
max_position = equity * 0.25
|
||||
if position_value > max_position:
|
||||
position_value = max_position
|
||||
debug_print(f"Position capped at 25% equity: ${position_value:.2f}")
|
||||
if position_value < MIN_NOTIONAL:
|
||||
position_value = MIN_NOTIONAL
|
||||
debug_print(f"Position set to minimum: ${position_value:.2f}")
|
||||
debug_print(f"Calculated position size: ${position_value:.2f}")
|
||||
return position_value
|
||||
|
||||
def advanced_signal_generator(symbol):
|
||||
debug_print(f"Generating signal for {symbol}")
|
||||
bars = get_recent_bars(symbol, 200)
|
||||
if bars is None or len(bars) < LONG_WINDOW:
|
||||
debug_print("Insufficient data for signal generation")
|
||||
return None, 0, 0, None
|
||||
|
||||
closes = bars['close']
|
||||
highs = bars['high']
|
||||
lows = bars['low']
|
||||
current_price = closes.iloc[-1]
|
||||
|
||||
debug_print("Calculating indicators...")
|
||||
if USE_EMA:
|
||||
short_ma = ema(closes, SHORT_WINDOW).iloc[-1]
|
||||
long_ma = ema(closes, LONG_WINDOW).iloc[-1]
|
||||
else:
|
||||
short_ma = sma(closes, SHORT_WINDOW).iloc[-1]
|
||||
long_ma = sma(closes, LONG_WINDOW).iloc[-1]
|
||||
|
||||
rsi_val = rsi(closes, 14).iloc[-1]
|
||||
adx_val = adx(highs, lows, closes).iloc[-1]
|
||||
atr_val = atr(highs, lows, closes).iloc[-1]
|
||||
upper, middle, lower = bollinger(closes, BB_WINDOW, BB_STD)
|
||||
|
||||
debug_print(f"Indicators: MA_short={short_ma:.2f}, MA_long={long_ma:.2f}, RSI={rsi_val:.1f}, ADX={adx_val:.1f}")
|
||||
|
||||
vix_level = get_vix(api, SYMBOL, USE_VIX_FILTER)
|
||||
if USE_VIX_FILTER and vix_level > VIX_THRESHOLD:
|
||||
debug_print(f"VIX filter triggered: {vix_level:.1f} > {VIX_THRESHOLD}")
|
||||
return None, 0, 0, None
|
||||
|
||||
if not check_volume(bars, VOLUME_MULTIPLIER):
|
||||
debug_print("Volume filter failed")
|
||||
return None, 0, 0, None
|
||||
|
||||
bullish_pattern, bearish_pattern = check_candle_pattern(bars)
|
||||
macd_signal = check_macd_confirmation(bars)
|
||||
multiframe_trend = check_multiframe_confluence(SYMBOL, USE_EMA) if MULTIFRAME_FILTER else "neutral"
|
||||
regime = detect_market_regime(bars, ADX_THRESHOLD) if REGIME_DETECTION else "trend"
|
||||
|
||||
debug_print(f"Filters: regime={regime}, multiframe={multiframe_trend}, macd={macd_signal}")
|
||||
|
||||
signal = None
|
||||
strength = 0
|
||||
stop = 0
|
||||
position_type = None
|
||||
|
||||
if regime == "trend":
|
||||
if short_ma > long_ma and rsi_val < 55:
|
||||
if REQUIRE_CANDLE_PATTERN and not bullish_pattern:
|
||||
debug_print("Bullish signal rejected: candle pattern required")
|
||||
elif REQUIRE_MACD_CONFIRMATION and macd_signal != "bullish":
|
||||
debug_print("Bullish signal rejected: MACD confirmation required")
|
||||
else:
|
||||
signal = "buy"
|
||||
strength = min(1.0, (adx_val / 40) * 0.7 + 0.3)
|
||||
stop = current_price - atr_val * ATR_STOP_MULTIPLIER
|
||||
position_type = "long"
|
||||
debug_print(f"BUY signal: strength={strength:.2f}, stop=${stop:.2f}")
|
||||
|
||||
if short_ma < long_ma and rsi_val > 45:
|
||||
if REQUIRE_CANDLE_PATTERN and not bearish_pattern:
|
||||
debug_print("Bearish signal rejected: candle pattern required")
|
||||
elif REQUIRE_MACD_CONFIRMATION and macd_signal != "bearish":
|
||||
debug_print("Bearish signal rejected: MACD confirmation required")
|
||||
else:
|
||||
signal = "sell"
|
||||
strength = min(1.0, (adx_val / 40) * 0.7 + 0.3)
|
||||
stop = current_price + atr_val * ATR_STOP_MULTIPLIER
|
||||
position_type = "short"
|
||||
debug_print(f"SELL signal: strength={strength:.2f}, stop=${stop:.2f}")
|
||||
|
||||
elif regime == "range":
|
||||
if current_price <= lower.iloc[-1] and rsi_val < 30:
|
||||
if REQUIRE_CANDLE_PATTERN and not bullish_pattern:
|
||||
debug_print("Range buy rejected: candle pattern required")
|
||||
else:
|
||||
signal = "buy"
|
||||
strength = 0.85
|
||||
stop = current_price - atr_val * ATR_STOP_MULTIPLIER
|
||||
position_type = "long"
|
||||
debug_print(f"Range BUY signal: strength={strength:.2f}, stop=${stop:.2f}")
|
||||
|
||||
if current_price >= upper.iloc[-1] and rsi_val > 70:
|
||||
if REQUIRE_CANDLE_PATTERN and not bearish_pattern:
|
||||
debug_print("Range sell rejected: candle pattern required")
|
||||
else:
|
||||
signal = "sell"
|
||||
strength = 0.85
|
||||
stop = current_price + atr_val * ATR_STOP_MULTIPLIER
|
||||
position_type = "short"
|
||||
debug_print(f"Range SELL signal: strength={strength:.2f}, stop=${stop:.2f}")
|
||||
|
||||
if strength < MIN_SIGNAL_STRENGTH:
|
||||
debug_print(f"Signal rejected: strength {strength:.2f} < {MIN_SIGNAL_STRENGTH}")
|
||||
return None, 0, 0, None
|
||||
|
||||
return signal, strength, stop, position_type
|
||||
|
||||
def scale_out_profit_taking(symbol, entry_price, current_price, stop_loss, position_type):
|
||||
debug_print(f"Checking scale out: entry=${entry_price:.2f}, current=${current_price:.2f}")
|
||||
|
||||
if position_type == 'long':
|
||||
profit_pct = ((current_price - entry_price) / entry_price) * 100
|
||||
else:
|
||||
profit_pct = ((entry_price - current_price) / entry_price) * 100
|
||||
|
||||
risk_pct = abs((entry_price - stop_loss) / entry_price) * 100
|
||||
|
||||
target_1_pct = risk_pct * PROFIT_TARGET_1
|
||||
target_2_pct = risk_pct * PROFIT_TARGET_2
|
||||
|
||||
if not hasattr(scale_out_profit_taking, "target_1_hit"):
|
||||
scale_out_profit_taking.target_1_hit = False
|
||||
|
||||
if profit_pct >= target_1_pct and not scale_out_profit_taking.target_1_hit:
|
||||
qty = current_position_qty(symbol)
|
||||
if qty != 0:
|
||||
half_qty = int(qty / 2)
|
||||
if half_qty > 0:
|
||||
debug_print(f"Target 1 hit ({target_1_pct:.2f}%), scaling out {half_qty} shares")
|
||||
if position_type == 'long':
|
||||
submit_market_sell(symbol, half_qty)
|
||||
else:
|
||||
submit_buy_to_cover(symbol, half_qty)
|
||||
scale_out_profit_taking.target_1_hit = True
|
||||
logger.info(f"💰 Partial profit @ {profit_pct:.2f}% ({half_qty} shares)")
|
||||
debug_print(f"Partial profit taken: {half_qty} shares @ {profit_pct:.2f}%")
|
||||
|
||||
if profit_pct >= target_2_pct:
|
||||
qty = current_position_qty(symbol)
|
||||
if qty != 0:
|
||||
debug_print(f"Target 2 hit ({target_2_pct:.2f}%), closing remaining {qty} shares")
|
||||
if position_type == 'long':
|
||||
submit_market_sell(symbol, qty)
|
||||
else:
|
||||
submit_buy_to_cover(symbol, qty)
|
||||
logger.info(f"💰💰 Full profit @ {profit_pct:.2f}%")
|
||||
debug_print(f"Full profit target hit: closed @ {profit_pct:.2f}%")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def atr_based_trailing_stop(symbol, entry_price, current_price, initial_stop, position_type):
|
||||
debug_print(f"Checking trailing stop: entry=${entry_price:.2f}, current=${current_price:.2f}")
|
||||
|
||||
if not hasattr(atr_based_trailing_stop, "trailing_stop"):
|
||||
atr_based_trailing_stop.trailing_stop = initial_stop
|
||||
debug_print(f"Initialized trailing stop: ${initial_stop:.2f}")
|
||||
|
||||
bars = get_recent_bars(symbol, 50)
|
||||
if bars is None or len(bars) < 14:
|
||||
debug_print("Insufficient data for ATR calculation")
|
||||
return False
|
||||
|
||||
current_atr = atr(bars['high'], bars['low'], bars['close']).iloc[-1]
|
||||
|
||||
if position_type == 'long':
|
||||
new_stop = current_price - (current_atr * ATR_STOP_MULTIPLIER)
|
||||
if new_stop > atr_based_trailing_stop.trailing_stop:
|
||||
debug_print(f"Updating trailing stop: ${atr_based_trailing_stop.trailing_stop:.2f} -> ${new_stop:.2f}")
|
||||
atr_based_trailing_stop.trailing_stop = new_stop
|
||||
|
||||
if current_price <= atr_based_trailing_stop.trailing_stop:
|
||||
debug_print(f"Long stop hit: ${current_price:.2f} <= ${atr_based_trailing_stop.trailing_stop:.2f}")
|
||||
return True
|
||||
else:
|
||||
new_stop = current_price + (current_atr * ATR_STOP_MULTIPLIER)
|
||||
if new_stop < atr_based_trailing_stop.trailing_stop:
|
||||
debug_print(f"Updating trailing stop: ${atr_based_trailing_stop.trailing_stop:.2f} -> ${new_stop:.2f}")
|
||||
atr_based_trailing_stop.trailing_stop = new_stop
|
||||
|
||||
if current_price >= atr_based_trailing_stop.trailing_stop:
|
||||
debug_print(f"Short stop hit: ${current_price:.2f} >= ${atr_based_trailing_stop.trailing_stop:.2f}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def check_multiframe_confluence(symbol):
|
||||
debug_print(f"Checking multiframe confluence for {symbol}")
|
||||
if not MULTIFRAME_FILTER:
|
||||
return "neutral"
|
||||
return _check_multiframe_confluence(symbol, api, USE_EMA)
|
||||
|
||||
def main():
|
||||
logger.info("🚀 Trading engine starting...")
|
||||
debug_print("Trading engine initialized")
|
||||
logger.info(f"📊 Symbol: {SYMBOL}, Timeframe: {BAR_TIMEFRAME}")
|
||||
logger.info(f"⚙️ Risk/Trade: {RISK_PER_TRADE*100:.2f}%, Stop Mult: {ATR_STOP_MULTIPLIER}x")
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
clock = api.get_clock()
|
||||
if not clock.is_open:
|
||||
next_open = clock.next_open.astimezone(EASTERN)
|
||||
wait_time = (next_open - datetime.now(EASTERN)).total_seconds()
|
||||
logger.info(f"🌙 Market closed. Next open: {next_open.strftime('%I:%M %p ET on %A, %B %d')}")
|
||||
debug_print(f"Market closed, waiting {seconds_to_human_readable(int(wait_time))} until next open")
|
||||
time.sleep(min(wait_time, 3600))
|
||||
continue
|
||||
|
||||
logger.info("🔔 Market open - session starting")
|
||||
debug_print("Market open, starting trading session")
|
||||
|
||||
opening_equity = fetch_equity()
|
||||
logger.info(f"💵 Starting equity: ${opening_equity:.2f}")
|
||||
|
||||
position_active = False
|
||||
entry_price = 0
|
||||
entry_time = None
|
||||
stop_loss = 0
|
||||
position_type = None
|
||||
trade_count = 0
|
||||
trades_today = 0
|
||||
total_pnl = 0
|
||||
|
||||
while clock.is_open:
|
||||
clock = api.get_clock()
|
||||
current_equity = fetch_equity()
|
||||
drawdown = (opening_equity - current_equity) / opening_equity if opening_equity > 0 else 0
|
||||
|
||||
if drawdown > MAX_DRAWDOWN:
|
||||
logger.warning(f"⚠️ Max drawdown reached: {drawdown:.2%}")
|
||||
debug_print(f"Max drawdown triggered: {drawdown:.2%}")
|
||||
close_all_positions()
|
||||
logger.info("🛑 Trading halted for the day")
|
||||
time.sleep(3600)
|
||||
break
|
||||
|
||||
bars = get_recent_bars(SYMBOL, 10)
|
||||
if bars is None or len(bars) == 0:
|
||||
debug_print("No bars available, retrying...")
|
||||
time.sleep(30)
|
||||
continue
|
||||
|
||||
current_price = bars['close'].iloc[-1]
|
||||
vix_level = get_vix(api, SYMBOL, USE_VIX_FILTER)
|
||||
|
||||
if position_active:
|
||||
debug_print(f"Managing active position: {position_type}, entry=${entry_price:.2f}")
|
||||
|
||||
if MAX_HOLD_TIME > 0 and entry_time:
|
||||
time_in_trade = (datetime.now(EASTERN) - entry_time).total_seconds()
|
||||
if time_in_trade > MAX_HOLD_TIME:
|
||||
logger.info(f"⏰ Max hold time ({MAX_HOLD_TIME//60} min)")
|
||||
debug_print(f"Max hold time exceeded, closing position")
|
||||
qty = current_position_qty(SYMBOL)
|
||||
if qty != 0:
|
||||
if position_type == 'long':
|
||||
submit_market_sell(SYMBOL, qty)
|
||||
else:
|
||||
submit_buy_to_cover(SYMBOL, abs(qty))
|
||||
position_active = False
|
||||
trade_count += 1
|
||||
try:
|
||||
delattr(scale_out_profit_taking, "target_1_hit")
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
delattr(atr_based_trailing_stop, "trailing_stop")
|
||||
except AttributeError:
|
||||
pass
|
||||
debug_print(f"Sleeping {seconds_to_human_readable(POLL_INTERVAL)} after exit")
|
||||
time.sleep(POLL_INTERVAL)
|
||||
continue
|
||||
|
||||
if scale_out_profit_taking(SYMBOL, entry_price, current_price, stop_loss, position_type):
|
||||
remaining_qty = current_position_qty(SYMBOL)
|
||||
if remaining_qty == 0:
|
||||
position_active = False
|
||||
if position_type == 'long':
|
||||
trade_pnl = (current_price - entry_price) * 100
|
||||
else:
|
||||
trade_pnl = (entry_price - current_price) * 100
|
||||
total_pnl += trade_pnl
|
||||
logger.info(f"✅ Position closed (PnL: ${trade_pnl:.2f})")
|
||||
debug_print(f"Position fully closed, PnL: ${trade_pnl:.2f}")
|
||||
try:
|
||||
delattr(scale_out_profit_taking, "target_1_hit")
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
delattr(atr_based_trailing_stop, "trailing_stop")
|
||||
except AttributeError:
|
||||
pass
|
||||
debug_print(f"Sleeping {seconds_to_human_readable(POLL_INTERVAL)} after exit")
|
||||
time.sleep(POLL_INTERVAL)
|
||||
continue
|
||||
|
||||
if atr_based_trailing_stop(SYMBOL, entry_price, current_price, stop_loss, position_type):
|
||||
qty = current_position_qty(SYMBOL)
|
||||
if qty != 0:
|
||||
if position_type == 'long':
|
||||
submit_market_sell(SYMBOL, qty)
|
||||
else:
|
||||
submit_buy_to_cover(SYMBOL, abs(qty))
|
||||
position_active = False
|
||||
trade_count += 1
|
||||
logger.info("🛑 Stop hit")
|
||||
debug_print("Stop hit, position closed")
|
||||
try:
|
||||
delattr(scale_out_profit_taking, "target_1_hit")
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
delattr(atr_based_trailing_stop, "trailing_stop")
|
||||
except AttributeError:
|
||||
pass
|
||||
debug_print(f"Sleeping {seconds_to_human_readable(POLL_INTERVAL)} after exit")
|
||||
time.sleep(POLL_INTERVAL)
|
||||
continue
|
||||
|
||||
if trades_today >= MAX_TRADES_PER_DAY:
|
||||
logger.info(f"📊 Daily limit ({MAX_TRADES_PER_DAY}) - monitoring only")
|
||||
debug_print(f"Daily trade limit reached ({trades_today}/{MAX_TRADES_PER_DAY})")
|
||||
time.sleep(POLL_INTERVAL)
|
||||
continue
|
||||
|
||||
signal, strength, signal_stop_loss, signal_position_type = advanced_signal_generator(SYMBOL)
|
||||
|
||||
if signal == 'sell' and not ENABLE_SHORT_SELLING:
|
||||
debug_print("Short selling disabled, ignoring sell signal")
|
||||
signal = None
|
||||
|
||||
bars = get_recent_bars(SYMBOL, 50)
|
||||
if bars is not None:
|
||||
regime = detect_market_regime(bars, ADX_THRESHOLD)
|
||||
else:
|
||||
regime = 'unknown'
|
||||
|
||||
if signal in ['buy', 'sell'] and not position_active:
|
||||
debug_print(f"Signal detected: {signal}, executing trade...")
|
||||
buying_power = fetch_buying_power()
|
||||
position_size = calculate_position_size(current_equity, signal_stop_loss, current_price)
|
||||
|
||||
if buying_power >= position_size:
|
||||
execution_price = None
|
||||
|
||||
if signal == 'buy':
|
||||
if USE_LIMIT_ORDERS:
|
||||
bid, ask = get_bid_ask(SYMBOL)
|
||||
limit_price = bid
|
||||
execution_price = submit_limit_buy(SYMBOL, position_size, limit_price)
|
||||
else:
|
||||
execution_price = submit_market_buy(SYMBOL, position_size)
|
||||
elif signal == 'sell':
|
||||
if USE_LIMIT_ORDERS:
|
||||
bid, ask = get_bid_ask(SYMBOL)
|
||||
limit_price = ask
|
||||
execution_price = submit_limit_short_sell(SYMBOL, position_size, limit_price)
|
||||
else:
|
||||
execution_price = submit_short_sell(SYMBOL, position_size)
|
||||
|
||||
if execution_price:
|
||||
trade_count += 1
|
||||
trades_today += 1
|
||||
entry_price = execution_price
|
||||
entry_time = datetime.now(EASTERN)
|
||||
stop_loss = signal_stop_loss
|
||||
position_active = True
|
||||
position_type = 'long' if signal == 'buy' else 'short'
|
||||
risk_amount = abs(entry_price - stop_loss) / entry_price
|
||||
|
||||
logger.info(f" Entry=${entry_price:.2f}, Stop=${stop_loss:.2f}, Risk={risk_amount:.2%}")
|
||||
logger.info(f" Regime={regime}, Strength={strength:.2f}, Trade #{trade_count} ({trades_today}/{MAX_TRADES_PER_DAY})")
|
||||
debug_print(f"Trade executed: entry=${entry_price:.2f}, stop=${stop_loss:.2f}, regime={regime}")
|
||||
|
||||
atr_based_trailing_stop.trailing_stop = stop_loss
|
||||
debug_print(f"Trailing stop initialized: ${stop_loss:.2f}")
|
||||
else:
|
||||
logger.warning(f"⚠️ Insufficient buying power: ${buying_power:.2f} < ${position_size:.2f}")
|
||||
debug_print(f"Insufficient buying power: ${buying_power:.2f} < ${position_size:.2f}")
|
||||
|
||||
position_status = f"{position_type.upper()}" if position_active else "FLAT"
|
||||
|
||||
try:
|
||||
ts = clock.timestamp
|
||||
if ts.tzinfo is None:
|
||||
ts = EASTERN.localize(ts)
|
||||
else:
|
||||
ts = ts.astimezone(EASTERN)
|
||||
current_time = ts.strftime("%I:%M:%S %p ET")
|
||||
except Exception:
|
||||
current_time = datetime.now(EASTERN).strftime("%I:%M:%S %p ET")
|
||||
|
||||
hourly_trend = check_multiframe_confluence(SYMBOL)
|
||||
status_msg = f"⏱️ {current_time} | {position_status} | {regime.upper()}"
|
||||
|
||||
if position_active:
|
||||
pnl_pct = ((current_price - entry_price) / entry_price) * 100 if position_type == 'long' else ((entry_price - current_price) / entry_price) * 100
|
||||
status_msg += f" | PnL: {pnl_pct:+.2f}%"
|
||||
|
||||
status_msg += f" | H:{hourly_trend} | VIX:{vix_level:.1f} | {trades_today}/{MAX_TRADES_PER_DAY}"
|
||||
logger.info(status_msg)
|
||||
|
||||
debug_print(f"Sleeping {seconds_to_human_readable(POLL_INTERVAL)}...")
|
||||
time.sleep(POLL_INTERVAL)
|
||||
|
||||
logger.info("🔚 Session ending...")
|
||||
debug_print("Session ending, closing all positions...")
|
||||
close_all_positions()
|
||||
|
||||
final_equity = fetch_equity()
|
||||
session_pnl = final_equity - opening_equity
|
||||
session_pnl_pct = (session_pnl / opening_equity) * 100 if opening_equity > 0 else 0
|
||||
|
||||
logger.info(f"📊 Summary: {trade_count} trades")
|
||||
logger.info(f"💰 Final: ${final_equity:.2f} (PNL: ${session_pnl:+.2f}, {session_pnl_pct:+.2f}%)")
|
||||
logger.info("✅ Day complete. Waiting for next session...")
|
||||
debug_print(f"Day complete. Trades: {trade_count}, PnL: ${session_pnl:+.2f}")
|
||||
|
||||
time.sleep(3600)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"💥 Session error: {e}")
|
||||
debug_print(f"Session error: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
logger.info("⏳ Waiting 5 min before retry...")
|
||||
time.sleep(300)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("🛑 User interrupt")
|
||||
debug_print("User interrupt detected")
|
||||
close_all_positions()
|
||||
except Exception as e:
|
||||
logger.error(f"💥 Fatal error: {e}")
|
||||
debug_print(f"Fatal error: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
finally:
|
||||
logger.info("🔚 Shutdown")
|
||||
debug_print("Script shutdown")
|
||||
|
||||
def run():
|
||||
return main()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,95 @@
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
from .indicators import ema, sma, rsi, adx, atr, bollinger, macd
|
||||
from .api import AlpacaClient
|
||||
from .utils import EASTERN
|
||||
|
||||
def check_volume(bars: pd.DataFrame, multiplier: float):
|
||||
if len(bars) < 20 or "volume" not in bars.columns:
|
||||
return True
|
||||
avg = bars["volume"].rolling(window=20).mean().iloc[-1]
|
||||
cur = bars["volume"].iloc[-1]
|
||||
return cur >= avg * multiplier
|
||||
|
||||
def check_candle_pattern(bars: pd.DataFrame):
|
||||
if len(bars) < 2:
|
||||
return False, False
|
||||
last = bars.iloc[-1]
|
||||
prev = bars.iloc[-2]
|
||||
bullish = last["close"] > last["open"] and prev["close"] < prev["open"] and last["close"] > prev["open"] and last["open"] < prev["close"]
|
||||
bearish = last["close"] < last["open"] and prev["close"] > prev["open"] and last["close"] < prev["open"] and last["open"] > prev["close"]
|
||||
return bullish, bearish
|
||||
|
||||
def check_macd_confirmation(bars: pd.DataFrame):
|
||||
if len(bars) < 35:
|
||||
return "neutral"
|
||||
macd_line, signal_line, _ = macd(bars["close"])
|
||||
if macd_line.iloc[-2] <= signal_line.iloc[-2] and macd_line.iloc[-1] > signal_line.iloc[-1]:
|
||||
return "bullish"
|
||||
if macd_line.iloc[-2] >= signal_line.iloc[-2] and macd_line.iloc[-1] < signal_line.iloc[-1]:
|
||||
return "bearish"
|
||||
return "neutral"
|
||||
|
||||
def check_200_sma_filter(symbol: str, client: AlpacaClient):
|
||||
daily = client.get_bars(symbol, "1Day", limit=210)
|
||||
if len(daily) < 200:
|
||||
return "neutral"
|
||||
sma_200 = sma(daily["close"], 200).iloc[-1]
|
||||
price = daily["close"].iloc[-1]
|
||||
if price > sma_200 * 1.01:
|
||||
return "bullish"
|
||||
if price < sma_200 * 0.99:
|
||||
return "bearish"
|
||||
return "neutral"
|
||||
|
||||
def check_multiframe_confluence(symbol: str, use_ema: bool, client: AlpacaClient = None):
|
||||
if client is None:
|
||||
from .engine import api as client
|
||||
hourly = client.get_bars(symbol, "1Hour", limit=50)
|
||||
if len(hourly) < 50:
|
||||
return "neutral"
|
||||
if use_ema:
|
||||
short = ema(hourly["close"], 20).iloc[-1]
|
||||
long = ema(hourly["close"], 50).iloc[-1]
|
||||
else:
|
||||
short = sma(hourly["close"], 20).iloc[-1]
|
||||
long = sma(hourly["close"], 50).iloc[-1]
|
||||
price = hourly["close"].iloc[-1]
|
||||
if short > long and price > short:
|
||||
return "bullish"
|
||||
if short < long and price < short:
|
||||
return "bearish"
|
||||
return "neutral"
|
||||
|
||||
def detect_market_regime(bars: pd.DataFrame, adx_threshold: float):
|
||||
if len(bars) < 50:
|
||||
return "unknown"
|
||||
current_adx = adx(bars["high"], bars["low"], bars["close"]).iloc[-1]
|
||||
current_atr = atr(bars["high"], bars["low"], bars["close"]).iloc[-1]
|
||||
atr_series = atr(bars["high"], bars["low"], bars["close"])
|
||||
percentile = (atr_series <= current_atr).mean() * 100
|
||||
if percentile > 70:
|
||||
return "high_vol"
|
||||
if percentile < 30:
|
||||
return "low_vol"
|
||||
if current_adx > adx_threshold:
|
||||
return "trend"
|
||||
return "range"
|
||||
|
||||
def get_vix(client: AlpacaClient, symbol: str, use_vix_filter: bool):
|
||||
if not use_vix_filter:
|
||||
return 0
|
||||
try:
|
||||
vix = client.get_bars("VIX", "1Day", limit=5)
|
||||
if len(vix) > 0:
|
||||
return vix["close"].iloc[-1]
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
spy = client.get_bars(symbol, "1Day", limit=20)
|
||||
if len(spy) >= 20:
|
||||
returns = spy["close"].pct_change()
|
||||
return returns.std() * (252 ** 0.5) * 100
|
||||
except:
|
||||
pass
|
||||
return 15
|
||||
@@ -0,0 +1,59 @@
|
||||
import pandas as pd
|
||||
|
||||
def sma(data, window):
|
||||
return data.rolling(window=window).mean()
|
||||
|
||||
def ema(data, window):
|
||||
return data.ewm(span=window, adjust=False).mean()
|
||||
|
||||
def rsi(data, window=14):
|
||||
delta = data.diff()
|
||||
gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
|
||||
loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
|
||||
loss = loss.replace(0, 0.0001)
|
||||
rs = gain / loss
|
||||
rsi_val = 100 - (100 / (1 + rs))
|
||||
return rsi_val
|
||||
|
||||
def atr(high, low, close, window=14):
|
||||
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_val = true_range.rolling(window=window).mean()
|
||||
return atr_val
|
||||
|
||||
def adx(high, low, close, window=14):
|
||||
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_val = 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_val)
|
||||
minus_di = 100 * (minus_dm.rolling(window=window).mean() / atr_val)
|
||||
di_sum = plus_di + minus_di
|
||||
di_sum = di_sum.replace(0, 0.0001)
|
||||
dx = 100 * abs(plus_di - minus_di) / di_sum
|
||||
adx_val = dx.rolling(window=window).mean()
|
||||
return adx_val
|
||||
|
||||
def macd(close, fast=12, slow=26, signal=9):
|
||||
ema_fast = ema(close, fast)
|
||||
ema_slow = ema(close, slow)
|
||||
macd_line = ema_fast - ema_slow
|
||||
signal_line = ema(macd_line, signal)
|
||||
histogram = macd_line - signal_line
|
||||
return macd_line, signal_line, histogram
|
||||
|
||||
def bollinger(close, window=20, num_std=2):
|
||||
middle = sma(close, window)
|
||||
std = close.rolling(window=window).std()
|
||||
upper = middle + (std * num_std)
|
||||
lower = middle - (std * num_std)
|
||||
return upper, middle, lower
|
||||
@@ -0,0 +1,18 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
@dataclass
|
||||
class PositionInfo:
|
||||
symbol: str
|
||||
entry_price: float
|
||||
entry_time: datetime
|
||||
stop_loss: float
|
||||
position_type: str
|
||||
quantity: float
|
||||
|
||||
@dataclass
|
||||
class RiskMetrics:
|
||||
max_drawdown: float
|
||||
total_pnl: float
|
||||
trade_count: int
|
||||
win_rate: float
|
||||
@@ -0,0 +1,19 @@
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
|
||||
EASTERN = pytz.timezone('US/Eastern')
|
||||
|
||||
def seconds_to_human_readable(seconds):
|
||||
if seconds < 60:
|
||||
return f"{seconds}s"
|
||||
elif seconds < 3600:
|
||||
return f"{seconds // 60}m {seconds % 60}s"
|
||||
elif seconds < 86400:
|
||||
hours = seconds // 3600
|
||||
minutes = (seconds % 3600) // 60
|
||||
return f"{hours}h {minutes}m"
|
||||
else:
|
||||
days = seconds // 86400
|
||||
hours = (seconds % 86400) // 3600
|
||||
minutes = (seconds % 3600) // 60
|
||||
return f"{days}d {hours}h {minutes}m"
|
||||
@@ -0,0 +1,8 @@
|
||||
pandas
|
||||
numpy
|
||||
pytz
|
||||
python-dotenv
|
||||
alpaca-trade-api
|
||||
backoff
|
||||
pydantic
|
||||
pydantic-settings
|
||||
Reference in new issue
Block a user