From the multi-agent audit (ship-now, low-overfit-risk cluster): INGESTION RESILIENCY - client._request: 429/5xx now retry with Retry-After + exponential backoff (previously hard-failed, feeding silent-stale ingest). - SIP backfill feed (settings.backfill_feed='sip'): live realtime stays IEX, but backtest/backfill ingestion uses the full consolidated tape — IEX sees only ~2-5% of true volume (verified: AAPL 22,797 vs 452,704), which was silently corrupting every rvol/$-volume decision and backtest. eod_replay wired to it. - step_ingest freshness watchdog: emits a STALE event + board note when the newest bar is older than INGEST_STALE_SECONDS during RTH (45% of passes historically returned 0 rows with no error). Optional hard arm-block (INGEST_STALE_BLOCK, off by default). RISK - Portfolio gross-notional cap (TRADING_MAX_GROSS_NOTIONAL_PCT=0.60): both sizing paths now cap each entry to the remaining gross budget, so 6 x 16% can't pile the book into ~96% gross of correlated small-caps. Blocks/shrinks late entries. - Fixed the duplicate TRADING_MAX_CONCURRENT_POSITIONS in .env.example. MONITORING (UI) - Naked/unprotected-position alarm: _unprotected_positions joins the latest position + order snapshots (order sync now flattens legs) and the TUI board shows a red UNPROTECTED banner the instant a held long lacks stop coverage — the exact blind spot when APWC/NOWL went naked. Verified it flags today's 3 overnight positions. Tests: gross-notional cap blocks when budget full; 50 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
48 lines
1.7 KiB
Python
48 lines
1.7 KiB
Python
"""Alpaca paper-trading settings."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AlpacaPaperSettings:
|
|
api_key: str = ""
|
|
secret_key: str = ""
|
|
trading_base_url: str = "https://paper-api.alpaca.markets"
|
|
data_base_url: str = "https://data.alpaca.markets"
|
|
# live realtime feed. IEX is free + realtime but THIN (~2-5% of true volume),
|
|
# which corrupts every rvol / $-volume decision computed off it.
|
|
feed: str = "iex"
|
|
# historical/backfill feed. SIP is the consolidated tape (full volume) — use
|
|
# it for backtests/backfill so rvol & $-volume reflect reality. SIP realtime
|
|
# has a ~15-min embargo on free plans, which is why live stays on `feed`.
|
|
backfill_feed: str = "sip"
|
|
|
|
@classmethod
|
|
def from_env(cls, env: dict[str, str] | None = None) -> "AlpacaPaperSettings":
|
|
values = dict(os.environ)
|
|
if env is not None:
|
|
values.update(env)
|
|
api_key = values.get("ALPACA_API_KEY") or values.get("APCA_API_KEY_ID") or ""
|
|
secret = (
|
|
values.get("ALPACA_SECRET_KEY")
|
|
or values.get("APCA_API_SECRET_KEY")
|
|
or ""
|
|
)
|
|
base = values.get("APCA_API_BASE_URL", "https://paper-api.alpaca.markets")
|
|
feed = values.get("ALPACA_DATA_FEED", "iex")
|
|
backfill_feed = values.get("ALPACA_BACKFILL_FEED", "sip")
|
|
return cls(
|
|
api_key=api_key,
|
|
secret_key=secret,
|
|
trading_base_url=base.rstrip("/"),
|
|
feed=feed,
|
|
backfill_feed=backfill_feed,
|
|
)
|
|
|
|
@property
|
|
def is_configured(self) -> bool:
|
|
return bool(self.api_key and self.secret_key)
|