commit be9f5fb95bd714aa50abb503d493d35d4a74461f Author: Justin Oros Date: Fri Jan 30 16:30:43 2026 -0700 Alpaca Trader diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..ad441e6 Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..2065e7f --- /dev/null +++ b/README.md @@ -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. diff --git a/alpaca_trader/.DS_Store b/alpaca_trader/.DS_Store new file mode 100644 index 0000000..4a25c19 Binary files /dev/null and b/alpaca_trader/.DS_Store differ diff --git a/alpaca_trader/__init__.py b/alpaca_trader/__init__.py new file mode 100644 index 0000000..9844b23 --- /dev/null +++ b/alpaca_trader/__init__.py @@ -0,0 +1 @@ +__all__ = ["cli", "engine", "indicators"] diff --git a/alpaca_trader/__main__.py b/alpaca_trader/__main__.py new file mode 100644 index 0000000..d01454a --- /dev/null +++ b/alpaca_trader/__main__.py @@ -0,0 +1,4 @@ +from .engine import main + +if __name__ == "__main__": + main() diff --git a/alpaca_trader/api.py b/alpaca_trader/api.py new file mode 100644 index 0000000..d1be18f --- /dev/null +++ b/alpaca_trader/api.py @@ -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 diff --git a/alpaca_trader/cli.py b/alpaca_trader/cli.py new file mode 100644 index 0000000..91f1d7f --- /dev/null +++ b/alpaca_trader/cli.py @@ -0,0 +1,5 @@ +from .engine import run + +if __name__ == "__main__": + run() + diff --git a/alpaca_trader/config.json b/alpaca_trader/config.json new file mode 100644 index 0000000..3198f52 --- /dev/null +++ b/alpaca_trader/config.json @@ -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 +} diff --git a/alpaca_trader/engine.py b/alpaca_trader/engine.py new file mode 100644 index 0000000..c36f959 --- /dev/null +++ b/alpaca_trader/engine.py @@ -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() diff --git a/alpaca_trader/filters.py b/alpaca_trader/filters.py new file mode 100644 index 0000000..8768f14 --- /dev/null +++ b/alpaca_trader/filters.py @@ -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 diff --git a/alpaca_trader/indicators.py b/alpaca_trader/indicators.py new file mode 100644 index 0000000..e341318 --- /dev/null +++ b/alpaca_trader/indicators.py @@ -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 diff --git a/alpaca_trader/risk.py b/alpaca_trader/risk.py new file mode 100644 index 0000000..98a6be6 --- /dev/null +++ b/alpaca_trader/risk.py @@ -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 diff --git a/alpaca_trader/utils.py b/alpaca_trader/utils.py new file mode 100644 index 0000000..871efa7 --- /dev/null +++ b/alpaca_trader/utils.py @@ -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" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..59776f6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +pandas +numpy +pytz +python-dotenv +alpaca-trade-api +backoff +pydantic +pydantic-settings diff --git a/run.py b/run.py new file mode 100755 index 0000000..713ede0 --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +from alpaca_trader.engine import main + +if __name__ == "__main__": + main()