import logging from datetime import datetime, timedelta import config from db.db import mark_signal_executed, mark_signal_closed, get_executed_unclosed_signals logger = logging.getLogger(__name__) def _get_api(): try: from alpaca_trade_api import REST except ImportError: raise ImportError("alpaca-trade-api not installed. Run: pip install alpaca-trade-api") return REST( key_id=config.ALPACA_KEY, secret_key=config.ALPACA_SECRET, base_url=config.ALPACA_BASE_URL, ) def get_portfolio_value() -> float: return float(_get_api().get_account().portfolio_value) def get_open_positions_count() -> int: return len(_get_api().list_positions()) def execute_signal(signal: dict) -> bool: if not config.ALPACA_KEY or not config.ALPACA_SECRET: logger.warning("Alpaca credentials not configured") return False ticker = signal["ticker"] try: api = _get_api() if get_open_positions_count() >= config.MAX_POSITIONS: logger.warning(f"Max positions ({config.MAX_POSITIONS}) reached, skipping {ticker}") return False portfolio_value = get_portfolio_value() allocation = portfolio_value * config.POSITION_SIZE_PCT price = float(api.get_latest_trade(ticker).price) if price <= 0: logger.error(f"Invalid price for {ticker}: {price}") return False qty = int(allocation / price) if qty < 1: logger.warning(f"Allocation too small for {ticker}: ${allocation:.2f} at ${price:.2f}") return False existing_positions = {p.symbol: p for p in api.list_positions()} if ticker in existing_positions: existing_pct = float(existing_positions[ticker].market_value) / portfolio_value if existing_pct >= 0.10: logger.warning(f"Already at 10% cap for {ticker}, skipping") return False order = api.submit_order( symbol=ticker, qty=qty, side="buy", type="market", time_in_force="day", ) logger.info(f"Order submitted: {ticker} qty={qty} order_id={order.id}") mark_signal_executed(signal["id"]) return True except Exception as e: logger.error(f"Failed to execute signal for {ticker}: {e}") return False def close_position(ticker: str, signal_id: int) -> bool: try: _get_api().close_position(ticker) mark_signal_closed(signal_id) logger.info(f"Closed position: {ticker} (signal {signal_id})") return True except Exception as e: logger.error(f"Failed to close position {ticker}: {e}") return False def close_expired_positions(): if not config.ALPACA_KEY or not config.ALPACA_SECRET: return cutoff = datetime.utcnow() - timedelta(days=config.HOLDING_PERIOD_DAYS) signals = get_executed_unclosed_signals() for signal in signals: executed_at_str = signal.get("executed_at") if not executed_at_str: continue try: executed_at = datetime.strptime(executed_at_str, "%Y-%m-%dT%H:%M:%SZ") except ValueError: continue if executed_at <= cutoff: close_position(signal["ticker"], signal["id"])