Files
Python/daytrader.py
T

406 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# Description: Day-Trading Script (Alpaca API)
# Usage: python3 daytrader.py
# Author: Justin Oros
# Source: https://github.com/JustinOros
import os
import sys
import time
import logging
import json
from pathlib import Path
from dotenv import load_dotenv
import alpaca_trade_api as tradeapi
# -----------------------------------------------------------------------------
# Configuration
# -----------------------------------------------------------------------------
# Path configuration
SCRIPT_DIR = Path(__file__).parent
CONFIG_PATH = SCRIPT_DIR / "daytrader.json"
ENV_PATH = SCRIPT_DIR / ".env"
# Default configuration
DEFAULT_CONFIG = {
"SYMBOL": "SPY",
"RISK_FRACTION": 0.02,
"SHORT_WINDOW": 5,
"LONG_WINDOW": 20,
"MIN_NOTIONAL": 1.0,
"POLL_INTERVAL": 30,
"MAX_DRAWDOWN": 0.05,
"PDT_RULE": True
}
# Load environment variables
if ENV_PATH.exists():
load_dotenv(ENV_PATH)
else:
# Create placeholder .env file
with open(ENV_PATH, "w") as f:
f.write('APCA_API_KEY_ID="YOUR_API_KEY_HERE"\n')
f.write('APCA_API_SECRET_KEY="YOUR_SECRET_KEY_HERE"\n')
f.write('APCA_API_BASE_URL="https://paper-api.alpaca.markets"\n')
print("⚠️ Created placeholder .env file.")
print(" Please add your Alpaca API keys to .env file")
sys.exit(1)
# Load configuration
if CONFIG_PATH.exists():
with open(CONFIG_PATH, "r") as f:
config = json.load(f)
else:
# Create default config
with open(CONFIG_PATH, "w") as f:
json.dump(DEFAULT_CONFIG, f, indent=4)
config = DEFAULT_CONFIG.copy()
print(f"✅ Created default config file at {CONFIG_PATH}")
# Extract configuration values
SYMBOL = config["SYMBOL"]
RISK_FRACTION = float(config["RISK_FRACTION"])
SHORT_WINDOW = int(config["SHORT_WINDOW"])
LONG_WINDOW = int(config["LONG_WINDOW"])
MIN_NOTIONAL = float(config["MIN_NOTIONAL"])
POLL_INTERVAL = int(config["POLL_INTERVAL"])
MAX_DRAWDOWN = float(config["MAX_DRAWDOWN"])
PDT_RULE = bool(config["PDT_RULE"])
# Initialize Alpaca API
api = tradeapi.REST(
os.getenv('APCA_API_KEY_ID'),
os.getenv('APCA_API_SECRET_KEY'),
os.getenv('APCA_API_BASE_URL'),
api_version='v2'
)
# -----------------------------------------------------------------------------
# Logging Configuration
# -----------------------------------------------------------------------------
# Set up logging to daytrader.log in script directory
LOG_PATH = SCRIPT_DIR / "daytrader.log"
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(LOG_PATH, mode='a'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
# Helper Functions
# -----------------------------------------------------------------------------
def seconds_to_human_readable(seconds):
"""Convert seconds to human-readable format (hours, minutes, seconds)."""
if seconds < 0:
return "0 seconds"
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
time_parts = []
if hours > 0:
time_parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
if minutes > 0:
time_parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
if secs > 0 and hours == 0: # Only show seconds if less than an hour
time_parts.append(f"{secs} second{'s' if secs != 1 else ''}")
return " ".join(time_parts) if time_parts else "0 seconds"
def format_market_time(dt_obj):
"""Format datetime object to readable string."""
return dt_obj.strftime("%Y-%m-%d %I:%M:%S %p %Z")
# -----------------------------------------------------------------------------
# Trading Functions
# -----------------------------------------------------------------------------
def wait_until_market_open():
"""Wait until the market opens."""
clock = api.get_clock()
now = clock.timestamp
next_open = clock.next_open
if not clock.is_open:
seconds_until_open = (next_open - now).total_seconds()
if seconds_until_open > 0:
readable_time = seconds_to_human_readable(seconds_until_open)
logger.info(f"🕒 Market opens at {format_market_time(next_open)}")
logger.info(f"⏱️ Waiting {readable_time}...")
# Sleep in smaller chunks to allow for graceful interruption
while seconds_until_open > 0:
sleep_time = min(60, seconds_until_open) # Check every minute max
time.sleep(sleep_time)
seconds_until_open -= sleep_time
# Update remaining time display periodically
if sleep_time >= 60:
remaining_readable = seconds_to_human_readable(seconds_until_open)
logger.info(f"⏱️ {remaining_readable} remaining...")
else:
logger.info("✅ Market is open!")
else:
logger.info("✅ Market is open!")
def fetch_equity():
"""Fetch the current account equity."""
try:
account = api.get_account()
return float(account.equity)
except Exception as e:
logger.error(f"❌ Failed to fetch equity: {e}")
return 0.0
def fetch_buying_power():
"""Fetch the current buying power."""
try:
account = api.get_account()
return float(account.buying_power)
except Exception as e:
logger.error(f"❌ Failed to fetch buying power: {e}")
return 0.0
def get_day_trade_count():
"""Get the current day trade count."""
try:
account = api.get_account()
return int(account.day_trade_count)
except Exception as e:
logger.error(f"❌ Failed to fetch day trade count: {e}")
return 0
def submit_buy(symbol, notional):
"""Submit a buy order."""
if notional < MIN_NOTIONAL:
logger.warning(f"⚠️ Notional ${notional:.2f} < minimum ${MIN_NOTIONAL} - skipping.")
return False
try:
api.submit_order(
symbol=symbol,
notional=round(notional, 2),
side="buy",
type="market",
time_in_force="day"
)
logger.info(f"🟢 BUY ${notional:.2f} of {symbol}")
return True
except Exception as e:
logger.error(f"❌ Failed to buy {symbol}: {e}")
return False
def submit_sell(symbol, qty):
"""Submit a sell order."""
try:
api.submit_order(
symbol=symbol,
qty=qty,
side="sell",
type="market",
time_in_force="day"
)
logger.info(f"🔴 SELL {qty} shares of {symbol}")
return True
except Exception as e:
logger.error(f"❌ Failed to sell {symbol}: {e}")
return False
def close_all_positions():
"""Close all open positions."""
try:
positions = api.list_positions()
if not positions:
logger.info("✅ No open positions to close.")
return
logger.warning("⚠️ Closing all open positions...")
for pos in positions:
submit_sell(pos.symbol, int(float(pos.qty)))
logger.info("✅ All positions closed.")
except Exception as e:
logger.error(f"❌ Failed to close positions: {e}")
def get_recent_bars(symbol, limit=20):
"""Get recent bar data for a symbol."""
try:
bars = api.get_bars(
symbol,
"minute",
limit=limit
).df
return bars
except Exception as e:
logger.error(f"❌ Failed to fetch bars for {symbol}: {e}")
return None
def ma_cross_signal(symbol):
"""Generate a moving average crossover signal."""
bars = get_recent_bars(symbol, LONG_WINDOW + 5)
if bars is None or len(bars) < LONG_WINDOW:
return None
closes = bars['close']
short_ma = closes.rolling(window=SHORT_WINDOW).mean().iloc[-1]
long_ma = closes.rolling(window=LONG_WINDOW).mean().iloc[-1]
if short_ma > long_ma:
return "buy"
elif short_ma < long_ma:
return "sell"
else:
return None
def current_position_qty(symbol):
"""Get the current position quantity for a symbol."""
try:
positions = api.list_positions()
for pos in positions:
if pos.symbol == symbol:
return int(float(pos.qty))
return 0
except Exception as e:
logger.error(f"❌ Failed to fetch positions: {e}")
return 0
def pdt_allows_new_trade():
"""Check if PDT rules allow a new trade."""
if not PDT_RULE:
return True
equity = fetch_equity()
day_trade_count = get_day_trade_count()
# PDT rule: If equity < $25,000, max 3 day trades per 5 rolling days
if equity < 25000:
if day_trade_count >= 3:
logger.error(f"🛑 PDT rule triggered: {day_trade_count} day-trades in rolling 5-day window")
return False
return True
def get_market_status():
"""Get current market status and next open/close times."""
clock = api.get_clock()
status = "open" if clock.is_open else "closed"
next_event = clock.next_open if not clock.is_open else clock.next_close
event_type = "open" if not clock.is_open else "close"
return {
"status": status,
"next_event": next_event,
"event_type": event_type,
"timestamp": clock.timestamp
}
# -----------------------------------------------------------------------------
# Main Trading Loop
# -----------------------------------------------------------------------------
def main():
"""Main trading function."""
logger.info("🎯 Starting daytrader.py...")
# Display current market status
market_info = get_market_status()
logger.info(f"🏛️ Market is currently {market_info['status'].upper()}")
if market_info['status'] == 'closed':
logger.info(f"📅 Next market {market_info['event_type']}: {format_market_time(market_info['next_event'])}")
# Wait for market to open
wait_until_market_open()
# Record opening equity
opening_equity = fetch_equity()
if opening_equity == 0:
logger.error("💥 No equity available. Exiting...")
return
logger.info(f"💰 Opening equity: ${opening_equity:.2f}")
# Compute per-trade notional
per_trade_notional = max(MIN_NOTIONAL, opening_equity * RISK_FRACTION)
logger.info(f"🎯 Per-trade notional: ${per_trade_notional:.2f}")
# Display trading parameters
logger.info(f"⚙️ Trading configuration:")
logger.info(f" Symbol: {SYMBOL}")
logger.info(f" Risk per trade: {RISK_FRACTION:.1%}")
logger.info(f" Max drawdown: {MAX_DRAWDOWN:.1%}")
logger.info(f" MA Windows: {SHORT_WINDOW}/{LONG_WINDOW} minutes")
logger.info(f" PDT Rule enforced: {PDT_RULE}")
# Main trading loop
trade_count = 0
try:
while True:
# Check if market is open
clock = api.get_clock()
if not clock.is_open:
logger.info("❌ Market is closed. Exiting...")
break
# Check equity drop
current_equity = fetch_equity()
drawdown = (opening_equity - current_equity) / opening_equity
if drawdown > MAX_DRAWDOWN:
logger.error(f"💸 Maximum drawdown exceeded: {drawdown:.2%}. Stopping...")
break
# Check PDT rule
if not pdt_allows_new_trade():
logger.error("🛑 PDT rule violation. Stopping...")
break
# Generate trading signal
signal = ma_cross_signal(SYMBOL)
if signal == "buy":
buying_power = fetch_buying_power()
if buying_power >= per_trade_notional:
if submit_buy(SYMBOL, per_trade_notional):
trade_count += 1
logger.info(f"✅ Buy order executed for {SYMBOL} (Trade #{trade_count})")
else:
logger.warning(f"⚠️ Insufficient buying power: ${buying_power:.2f}")
elif signal == "sell":
qty = current_position_qty(SYMBOL)
if qty > 0:
if submit_sell(SYMBOL, qty):
trade_count += 1
logger.info(f"✅ Sell order executed for {SYMBOL} (Trade #{trade_count})")
else:
logger.info("ℹ️ No position to sell")
# Display current status
current_time = clock.timestamp.strftime("%I:%M:%S %p")
logger.info(f"⏱️ {current_time} - Waiting {POLL_INTERVAL} seconds for next check...")
time.sleep(POLL_INTERVAL)
except KeyboardInterrupt:
logger.info("🛑 Script interrupted by user")
except Exception as e:
logger.error(f"💥 Unexpected error: {e}")
finally:
logger.info("🔚 Script ending. Closing any remaining positions...")
close_all_positions()
logger.info(f"📊 Session summary: {trade_count} trades executed")
logger.info("✅ daytrader.py finished.")
if __name__ == "__main__":
main()