smaug/broker/alpaca_client.py
Claude 7e9221a914 feat: add PLAN.md and insider copytrade POC implementation
- PLAN.md: full implementation plan from issue
- config.py: configurable thresholds, API keys via .env
- ingestion/: EDGAR RSS poller + Form 4 XML parser
- db/: SQLite schema + interface (WAL mode)
- signals/: filter engine (buy/10b5-1/value/role) + cluster detector
- alerts/: Slack webhook alert with score gating
- broker/: Alpaca paper/live trade execution
- backtest/: historical signal backtesting with yfinance
- main.py: CLI entrypoint (run | fetch-once | backtest)
2026-05-04 16:15:22 +00:00

95 lines
2.8 KiB
Python

import logging
from typing import Optional
import config
from db.db import mark_signal_executed
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:
api = _get_api()
account = api.get_account()
return float(account.portfolio_value)
def get_open_positions_count() -> int:
api = _get_api()
return len(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()
positions_count = get_open_positions_count()
if 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
latest_trade = api.get_latest_trade(ticker)
price = float(latest_trade.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_value = float(existing_positions[ticker].market_value)
if existing_value / portfolio_value >= 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_after_days(ticker: str, holding_days: Optional[int] = None):
days = holding_days or config.HOLDING_PERIOD_DAYS
api = _get_api()
try:
api.close_position(ticker)
logger.info(f"Closed position: {ticker} after {days} days")
return True
except Exception as e:
logger.error(f"Failed to close position {ticker}: {e}")
return False