trader.cli.main module¶
Typer CLI — one of two entry points into the Core Service Layer.
The CLI only orchestrates: it resolves config, builds adapters, calls Core functions, and formats output. No trading logic lives here, so the Slice 2 daemon and a future web dashboard can call the same Core code.
- trader.cli.main.build_broker(config)[source]¶
Build the broker adapter for the resolved config.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_snapshot_repository(config)[source]¶
Build the snapshot repository against the configured database.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_bar_repository(config)[source]¶
Build the bar repository against the configured database.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_bar_cache(config, refresh_tail=None)[source]¶
Build the bar cache over the configured database and provider.
- Parameters:
config (EffectiveTradingConfig)
refresh_tail (timedelta | None)
- Return type:
- trader.cli.main.build_backtest_repository(config)[source]¶
Build the backtest repository against the configured database.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_trade_repository(config)[source]¶
Build the trade repository against the configured database.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_decision_repository(config)[source]¶
Build the decision repository against the configured database.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_outcome_repository(config)[source]¶
Build the outcome repository against the configured database.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_simulation_repository(config)[source]¶
Build the shadow-portfolio simulator’s repository against the configured database (issue #33).
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_watchlist_event_repository(config)[source]¶
Build the watchlist-event (discovery audit) repository.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_deployed_version_repository(config)[source]¶
Build the deployed-versions repository against the configured database.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_index_membership_repository(config)[source]¶
Build the index-membership repository against the configured database.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_analyst_consensus_repository(config)[source]¶
Build the analyst-consensus-history repository against the configured database.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_focus_list_repository(config)[source]¶
Build the focus_list repository against the configured database (issue #44).
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_news_archive_repository(config)[source]¶
Build the news-archive repository against the configured database.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_news_archive(config)[source]¶
Build the news archive over the configured database and Alpaca’s history.
Alpaca only, and not build_news_provider’s merge: yfinance has no archive to contribute — roughly the last ten items per ticker — so a merged provider could not answer for a past window at all. The consequence is recorded rather than hidden: a replayed prompt is thinner than the live one for the names whose yfinance coverage is better, which bounds the replay’s conclusion to the model’s skill at reading the Alpaca corpus.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.build_news_provider(config, *, news_window_hours=72.0)[source]¶
Build the news provider: yfinance and Alpaca (Benzinga), merged.
The two feeds are disjoint corpora — 0 of 10 shared headlines on every symbol probed, measured 2026-08-14 — so this is additive, not a choice between them. yfinance is the primary because it carries a summary far more often, and the merge prefers the primary’s copy of a shared story.
news_window_hours is the strategy’s own max_news_age_hours, passed in so that the merge’s freshness ordering and the strategy’s filter cannot mean different things by “fresh”.
- Parameters:
config (EffectiveTradingConfig)
news_window_hours (float)
- Return type:
- trader.cli.main.build_search_provider()[source]¶
Build the phrase-search provider discovery searches each theme with.
- Return type:
- trader.cli.main.build_analyst_provider()[source]¶
Build the analyst-consensus provider discovery filters candidates with.
lru_cache(maxsize=1) makes this a process-lifetime singleton (issue #100): every call site gets the same instance, so YFinanceAnalystProvider’s own per-symbol cache actually has repeat calls to hit, instead of starting empty every cycle. Every test that touches this replaces the whole name via monkeypatch.setattr(cli, “build_analyst_provider”, …), which bypasses the cache entirely — the real, decorated function is never called during the test suite.
- Return type:
- trader.cli.main.build_earnings_provider()[source]¶
Build the earnings-calendar provider LlmStrategy reads (issue #89).
Same process-lifetime-singleton reasoning as build_analyst_provider immediately above (issue #100): every call site shares one instance, so YFinanceEarningsProvider’s own per-symbol cache actually accumulates across cycles. Every test that touches this replaces the whole name via monkeypatch.setattr(cli, “build_earnings_provider”, …), the same way, which bypasses the cache entirely.
- Return type:
- trader.cli.main.build_sector_provider()[source]¶
Build the sector/industry provider –risk reports classify with.
Same singleton reasoning as build_analyst_provider (issue #100).
- Return type:
- trader.cli.main.build_index_provider()[source]¶
Build the index-membership scraper trader scan-universe uses.
- Return type:
- trader.cli.main.build_discovery_settings()[source]¶
Read discovery settings from config/watchlist.yaml.
A separate seam from load_watchlist() (which reads the same file for fixed_tickers) so a test can control discovery’s themes/filters without also having to fake the fixed-ticker list, and vice versa.
- Return type:
- trader.cli.main.build_llm_provider(model, timeout_seconds=120, temperature=0.0, num_ctx=4096)[source]¶
Build the Ollama provider from settings and strategy parameters.
- Parameters:
model (str)
timeout_seconds (int)
temperature (float)
num_ctx (int)
- Return type:
- trader.cli.main.build_ranker()[source]¶
An LlmRanker on the configured model, or None for the fallback.
The model comes from the configured llm strategy’s own params rather than from a second setting: one model name in one place cannot drift out of step with itself. A configuration with no llm strategy gets None, which run_once reads as confidence order — a rule-only setup should not stand up a provider pointed at a model nobody asked for.
Constructs only. OllamaProvider.__init__ opens no connection, so this is safe to call on a machine with no model server: a ranking failure then degrades that one cycle’s order rather than failing at startup.
- Return type:
Ranker | None
- trader.cli.main.build_replay_runner(config)[source]¶
Build a ReplayRunner for issue #7 step 4, over the configured database.
The model, window and lookback come from the configured llm strategy’s own params — the same source build_ranker reads — so a replay asks the same question production would have asked, not a second, independently drifting copy of the same settings. build_news_archive (Alpaca only, never the merged live feed) and build_provider (yfinance, for one forward-return fetch per symbol) are the same seams the rest of the CLI uses, so a test can substitute fakes for both without touching the network.
- Parameters:
config (EffectiveTradingConfig)
- Return type:
- trader.cli.main.llm_strategy_params()[source]¶
The params of the first configured llm strategy, or {}.
llm-check needs the model name, and the model name lives with the strategy that uses it rather than in a second config file.
- Return type:
dict[str, object]
- trader.cli.main.load_strategies()[source]¶
Read the configured strategies from config/strategies.yaml.
- Return type:
list[StrategyConfig]
- trader.cli.main.build_reporting_config()[source]¶
Read config/reporting.yaml, falling back to defaults if it is absent.
Same “absence is fine, wrongness is not” rule as build_daemon_config: a fresh clone with no reporting.yaml still gets a usable QQQ/VOO default, but a file that exists and fails the schema is a real error.
- Return type:
- trader.cli.main.build_ideas_scraper_config()[source]¶
Read config/ideas_scraper.yaml, falling back to defaults if it is absent — same “absence is fine, wrongness is not” rule as build_reporting_config (issue #75).
- Return type:
- trader.cli.main.build_simulation_settings()[source]¶
Read config/simulation.yaml, falling back to defaults if it is absent — same “absence is fine, wrongness is not” rule as build_reporting_config (issue #33).
- Return type:
- trader.cli.main.build_simulation_runner(config, pipeline_config, discovery, fixed)[source]¶
Bundle the shadow-portfolio simulator’s collaborators (issue #33).
Reuses config’s own database rather than a second connection — the simulator reads the same decisions/bars rows the real cycle just wrote, through repositories built the same way every other one in this module is.
- Parameters:
config (EffectiveTradingConfig)
pipeline_config (PipelineConfig)
discovery (DiscoverySettings)
fixed (Collection[str])
- Return type:
- trader.cli.main.build_daemon_config()[source]¶
Read config/daemon.yaml, falling back to defaults if it is absent.
Unlike the other configs, a missing file is not an error: the daemon’s defaults are usable, and a fresh clone should be able to run without creating a file first. A file that exists but is wrong is still an error — the fallback is for absence, not for invalidity.
- Return type:
- trader.cli.main.load_watchlist()[source]¶
The fixed tickers from config/watchlist.yaml.
- Return type:
list[str]
- trader.cli.main.load_benchmark_symbol()[source]¶
The relative-strength benchmark symbol from config/watchlist.yaml (issue #87). A third narrow seam alongside load_watchlist() and build_discovery_settings(), same reasoning as both: a test controlling one of the three should not have to also fake the other two.
- Return type:
str
- trader.cli.main.build_chat_service()[source]¶
Build the chat CLI’s ChatService (requirements §4, §13; issues #13-16).
Deliberately built from Settings() alone, never _load_config(): chat is a second entry point into the Core Service Layer that must work whether or not the trading loop’s broker credentials are configured — the same reasoning init_db_command and migrate already use for the database path in isolation. Chat reads trades/decisions/ account_snapshots and config/strategies.yaml, runs a backtest, and talks to Ollama; none of that touches Alpaca.
A strategy config error degrades to “no strategies configured” rather than failing chat outright — a broken strategies.yaml should not also take down the one tool that can explain what is wrong with it (issue #16).
- Return type:
- trader.cli.main.build_strategy_runs(entries, pipeline_config, decision_memory=None, *, config, alias_resolver=None, analyst_provider=None, earnings_provider=None)[source]¶
Build every configured strategy, with its mode and its evaluator.
An llm entry gets a provider built from its own params; a rule entry gets a BacktestEvaluator. A non-backtestable strategy gets no evaluator at all, since one could not gate it.
decision_memory is what lets an llm entry recognise that it already answered this question — the news-fingerprint gate. It is optional, and defaults to None so that a caller with no database (the CLI’s own tests, and llm-check) builds a strategy that simply calls the model every cycle. The production callers pass the same DecisionRepository they hand to run_once, so a decision is read back through the object that wrote it.
config is keyword-only and required: the Alpaca half of the news feed is built from its credentials, and a default would let a caller produce a credential-less feed by omission, whose first symptom is a log line that reads like a provider outage.
- Parameters:
entries (Sequence[StrategyConfig])
pipeline_config (PipelineConfig)
decision_memory (DecisionMemory | None)
config (EffectiveTradingConfig)
alias_resolver (Callable[[str], Sequence[str]] | None)
analyst_provider (AnalystProvider | None)
earnings_provider (EarningsProvider | None)
- Return type:
list[StrategyRun]
- trader.cli.main.execute_cycle(*, config, pipeline_config, strategy_entries, symbols, dry_run)[source]¶
Wire the collaborators and run one cycle.
A seam: the CLI test asserts what the command asked for without standing up the whole pipeline, and run_once stays free of construction concerns.
symbols is the operator-curated, fixed list from config/watchlist.yaml (fixed_tickers) — the same meaning it had before discovery existed. A fresh discovery scan runs every call (this command is one cycle per process invocation), and a symbol currently held is folded into the universe regardless of whether it is still discovered.
- Parameters:
config (EffectiveTradingConfig)
pipeline_config (PipelineConfig)
strategy_entries (Sequence[StrategyConfig])
symbols (list[str])
dry_run (bool)
- Return type:
- trader.cli.main.build_daemon_cycle(*, config, pipeline_config, daemon_config, strategy_entries, symbols, dry_run)[source]¶
Build the daemon’s object graph once, and bind it into a cycle callable.
Built once for the process’s life, deliberately: config edits take effect on restart, and flipping a strategy to mode: trading should happen at a moment the operator chose rather than at whichever tick follows a file save.
Returns the cycle callable and a read-only broker for the loop to check the clock and snapshot positions with. Two different objects over one connection on purpose — the loop cannot place an order because the thing it holds has no method to do so, while run_once keeps the raw order-placing broker it needs. Discovery’s own get_asset read goes through the same read-only broker, for the same reason get_clock does.
symbols is the operator-curated, fixed list (fixed_tickers) — its meaning is unchanged from before discovery existed. Discovery’s settings are read once here, at the same “built once for the process’s life” point as everything else in this function; only the scan itself — the part that changes cycle to cycle as new news arrives — runs inside cycle, which is what makes it recompute every cycle rather than once at startup.
- Parameters:
config (EffectiveTradingConfig)
pipeline_config (PipelineConfig)
daemon_config (DaemonConfig)
strategy_entries (Sequence[StrategyConfig])
symbols (list[str])
dry_run (bool)
- Return type:
tuple[Callable[[dict[str, Position] | None], CycleReport], BrokerAdapter]
- trader.cli.main.init_db_command()[source]¶
Create every table in the configured Postgres database.
Lives under trader setup (issue #90), not at the top level, so it reads as deliberately administrative rather than routine in –help output.
- Return type:
None
- trader.cli.main.config_check()[source]¶
Validate configuration and report the resolved trading mode.
- Return type:
None
- trader.cli.main.llm_check()[source]¶
Report whether the local model is reachable and pulled.
- Return type:
None
- trader.cli.main.scrape_ideas_command(url=<typer.models.ArgumentInfo object>)[source]¶
Fetch a web page and ask a local model what claude-alpaca-trader could build based on it. Writes a markdown file under the configured output directory (issue #75, stage 1 — filing the resulting ideas as GitLab issues is a separate, future, human/Claude-driven step).
Not a trading capability: touches no Position, Order, symbol, or the trader run/run-once cycle loop.
- Parameters:
url (str)
- Return type:
None
- trader.cli.main.account(risk=<typer.models.OptionInfo object>)[source]¶
Show the account summary and open positions, and save a snapshot.
- Parameters:
risk (bool)
- Return type:
None
- trader.cli.main.performance(start=<typer.models.OptionInfo object>, end=<typer.models.OptionInfo object>, since_creation=<typer.models.OptionInfo object>)[source]¶
Report the account’s real ROI against configurable benchmark tickers.
Compares the FIRST and LAST account_snapshots row in the window (saved by every trader account / trader run-once / trader run call) against a buy-and-hold of each ticker in config/reporting.yaml’s benchmark_tickers (QQQ and VOO by default) over the SAME calendar dates, fetched via the same yfinance path trader backtest uses.
This is a portfolio-level report over real account history — a different question from trader backtest’s own buy-and-hold baseline, which compares one simulated strategy run against the ONE symbol it traded.
Default start date (issue #57). When –start is omitted, the window starts at config/reporting.yaml’s default_report_start_date (a deployment-specific, dated finding — see docs/operational-timeline.md — NOT a code constant) rather than the earliest snapshot ever saved, because the raw earliest history mixes pre-service manual testing and daemon outages into what should be a measurement of the live service. Pass –since-creation to opt back into full history, or –start to pick an exact date — an explicit –start always wins over both the config default and –since-creation. If default_report_start_date is unset in config, this command’s behaviour is unchanged from before issue #57: full history by default.
- Parameters:
start (datetime | None)
end (datetime | None)
since_creation (bool)
- Return type:
None
- trader.cli.main.chart(days=<typer.models.OptionInfo object>, width=<typer.models.OptionInfo object>)[source]¶
Print an ASCII equity-curve sparkline from saved account snapshots (§12.3).
Reads account_snapshots — the rows trader account and trader run(-once) already save every cycle — and does not talk to the broker or place any order. Empty or single-snapshot history prints a short explanation instead of a curve; it is not an error.
- Parameters:
days (int)
width (int)
- Return type:
None
- trader.cli.main.web(host=<typer.models.OptionInfo object>, port=<typer.models.OptionInfo object>, reload=<typer.models.OptionInfo object>)[source]¶
Serve interactive equity-curve and P&L charts at http://HOST:PORT (§12.3, issue #20).
A second, browser-based renderer alongside trader chart/trader outcomes — same underlying data, not a replacement for either. Read-only: no route submits an order or writes to the database. Built from Settings() alone, like build_chat_service, so it works whether or not Alpaca credentials are configured; the only network call any page makes is the benchmark price fetch via yfinance, same as trader performance.
- Parameters:
host (str | None)
port (int | None)
reload (bool)
- Return type:
None
- trader.cli.main.quote(symbol)[source]¶
Print the latest price for SYMBOL.
- Parameters:
symbol (str)
- Return type:
None
- trader.cli.main.history(symbol, start=<typer.models.OptionInfo object>, end=<typer.models.OptionInfo object>, interval=<typer.models.OptionInfo object>)[source]¶
Print historical OHLCV bars for SYMBOL.
- Parameters:
symbol (str)
start (datetime)
end (datetime)
interval (str)
- Return type:
None
- trader.cli.main.fetch_bars(symbol, start=<typer.models.OptionInfo object>, end=<typer.models.OptionInfo object>, refresh=<typer.models.OptionInfo object>)[source]¶
Cache historical bars locally so backtests run offline and repeatably.
- Parameters:
symbol (str)
start (datetime)
end (datetime | None)
refresh (bool)
- Return type:
None
- class trader.cli.main.SeedFileFormat(*values)[source]¶
Bases:
StrEnumWhich vendor shape trader seed-bars should parse –file as.
Two values, not an auto-detected one: the two source files documented in issue #41 have genuinely different shapes and different adjustment conventions (yfinance Feather vs. an Interactive Brokers CSV export), and guessing which one a file is would be exactly the silent misinterpretation trader/marketdata/seed_import.py’s _to_decimal already refuses to do for a bare float. The operator states the format.
- FEATHER = 'feather'¶
- IBKR_CSV = 'ibkr-csv'¶
- trader.cli.main.seed_bars(file=<typer.models.OptionInfo object>, symbol=<typer.models.OptionInfo object>, interval=<typer.models.OptionInfo object>, file_format=<typer.models.OptionInfo object>, tz=<typer.models.OptionInfo object>)[source]¶
Seed historical bars from a third-party file into the local cache (issue #41).
A one-off import, not a live data source: writes through BarRepository.save_bars — the same path YFinanceProvider/BarCache use — so an imported bar is stored exactly like a live-fetched one, except for CachedBar.source, which this command always sets to a named vendor tag (never None, which is reserved for a bar this app fetched itself). save_bars already skips any timestamp already stored, so re-running this command against the same file is a safe no-op rather than a duplicate import; BarRepository.delete_bars separately refuses to delete a seeded row at all, so a later live refresh can never silently replace validated seed data with a differently-adjusted fetch.
Only what is explicitly named on the command line is imported — no directory scan, no “import everything under a data folder” default, per issue #41’s “import only what’s actually going to be used, not all 264MB reflexively.”
- Parameters:
file (Path)
symbol (str)
interval (str)
file_format (SeedFileFormat)
tz (str)
- Return type:
None
- trader.cli.main.backfill_news(start=<typer.models.OptionInfo object>, end=<typer.models.OptionInfo object>, symbol=<typer.models.OptionInfo object>)[source]¶
Archive Alpaca’s news history locally, so past decisions can be replayed.
Reads no broker and places no orders. Alpaca’s archive reaches back to 2023, which is the property yfinance lacks and the reason this table exists.
Every symbol is attempted even if one fails, because progress is per symbol and re-fetching what already succeeded costs requests for nothing. A failure still exits non-zero: the archive’s own coverage table is what makes a hole detectable later, and an exit code of 0 would hide it at the one moment someone is actually watching.
- Parameters:
start (datetime)
end (datetime | None)
symbol (list[str])
- Return type:
None
- trader.cli.main.scan_universe(index=<typer.models.OptionInfo object>)[source]¶
Scrape index membership and refresh stale analyst consensus (issue #27).
Separate from trader run’s daemon loop on purpose: this is the expensive, index-scale work (~500 symbols), meant to run on its own cadence (cron, or by hand) rather than every trading cycle. discover_symbols() only ever reads what this command already wrote — a cheap DB query, never a yfinance call at cycle scale — so a stale or never-run scan degrades to “this source contributes nothing,” the same shape themes: [] degrades to.
Every member is attempted even if one fails, same discipline as backfill-news: a failure still exits non-zero so a real gap is never silently hidden behind an exit code of 0.
- Parameters:
index (str)
- Return type:
None
- trader.cli.main.analyst_health(symbols=<typer.models.OptionInfo object>, rank_metric=<typer.models.OptionInfo object>, rank_limit=<typer.models.OptionInfo object>)[source]¶
A composite analyst health score, plus a rank-persistence screen (issue #39) — both computed over data this app already fetched and stored, and neither wired into any order-affecting path.
The health score (calculate_health_score, reimplemented in Decimal from a formula found in ../stock-screener) is scored per symbol from the newest persisted analyst_consensus_history opinion (AnalystConsensusRepository.latest_for_symbols, the same read discover_symbols()’s analyst-scan source uses) and the most recently cached bar close (BarCache, the same source discovery’s own price/volume filters read from) — never a fresh live analyst or price call this command triggers itself.
The rank-persistence screen ranks every symbol in analyst_consensus_ history against its peers at each historical trader scan-universe fetch, then reports the mean and standard deviation of each symbol’s rank across every fetch it appeared in — closing the gap docs/ ideas.md flagged: the consensus delta is stored but was never surfaced.
- Parameters:
symbols (str)
rank_metric (str)
rank_limit (int)
- Return type:
None
- trader.cli.main.build_focus_list(index=<typer.models.OptionInfo object>)[source]¶
Compute tomorrow’s focus list from analyst-scan candidates (issue #44).
Separate from trader scan-universe on purpose (Decision 2 of the design doc): that command refreshes index_membership/analyst_consensus_history, the two cheap tables this command only ever reads — never a yfinance call at index scale. Run scan-universe first, the same night (Decision 3); this command degrades to nothing useful against stale or absent membership, which is why an empty read fails loudly below rather than silently writing an empty focus list.
Reuses discover_symbols()’s existing analyst-scan branch (_scan_analyst_candidates + _evaluate_and_accept) rather than reimplementing candidate selection — called with themes=[] so only that branch runs, search never invoked. The same listing_gate/asset_gate/opinion_gate/price-volume chain every theme-search candidate already clears applies here, unchanged, including a fresh live analysts.get_opinion call per candidate.
Meant to run off market hours, on its own cadence (cron/launchd, or by hand) — never inside trader run’s daemon loop, the same framing scan-universe already documents for itself. Does not take the issue #43 single-instance lock (Decision 5): no order path, no LLM call, and its only write is an additive upsert into focus_list, a table the daemon never touches.
Every candidate’s own lookup failure is isolated by discover_symbols() itself (unchanged discipline) and costs only that candidate. The membership and consensus reads are re-checked directly here, ahead of discover_symbols(), so a broken read is a visible, non-zero-exit failure to an operator instead of silently degrading to “zero candidates found” — same discipline as scan-universe’s own “zero members scraped” check, extended to the consensus read.
- Parameters:
index (str)
- Return type:
None
- trader.cli.main.replay_score(out=<typer.models.OptionInfo object>, resume=<typer.models.OptionInfo object>, limit=<typer.models.OptionInfo object>, dry_run=<typer.models.OptionInfo object>)[source]¶
Replay the pre-registered decision sample and score it (issue #7 step 4).
Reads the news archive and the bar cache; calls Ollama (unless –dry-run); writes a per-row CSV and a JSON summary under –out. Places no orders and never contacts the broker — the replay drives LlmStrategy directly with an archive-backed news provider and a fixed clock, never OrderExecutor. Expect ~1.7-1.9 hours for the full 540 rows at the configured num_ctx; –dry-run costs nothing but a database read and exercises the point-in-time guards over the whole sample first.
Everything about the sample — the 9 symbols, the train/holdout split, the 40+20 per-symbol quota, the 72h eligibility window — is pre-registered and fixed in code, not read from config/watchlist.yaml: a config edit must never silently change what a run measures.
- Parameters:
out (Path)
resume (bool)
limit (int | None)
dry_run (bool)
- Return type:
None
- trader.cli.main.replay_rescore(csv_path=<typer.models.ArgumentInfo object>, out=<typer.models.OptionInfo object>)[source]¶
Re-score an existing replay CSV under the current formula (issue #29).
Calls no model and fetches nothing — every replayed decision (action, confidence, and every horizon’s forward return) is already in csv_path. Use this instead of a fresh replay-score run whenever the scoring code changes and an already-collected sample needs to be re-judged (edge_A’s degeneracy fix is the first case; it cost ~1.7-1.9 hours of Ollama calls to collect and must not cost that again to re-read).
Reads replay_summary.json next to csv_path, if present, only for provenance (seed, fetch_dates, dry_run, shortfalls) — never for scored data, which comes from the CSV alone. edge_grid_market_adjusted is not recomputed: SPY’s forward-price grid needed for it cannot be reconstructed from the CSV and this command makes no market-data fetch, so that section reports null. Everything else is fully reprocessed.
- Parameters:
csv_path (Path)
out (Path | None)
- Return type:
None
- trader.cli.main.strategies()[source]¶
List the strategies configured in config/strategies.yaml.
- Return type:
None
- trader.cli.main.backtest(strategy=<typer.models.OptionInfo object>, ticker=<typer.models.OptionInfo object>, start=<typer.models.OptionInfo object>, end=<typer.models.OptionInfo object>)[source]¶
Run one strategy against one symbol over a date range.
- Parameters:
strategy (str)
ticker (str)
start (datetime)
end (datetime)
- Return type:
None
- trader.cli.main.positions(risk=<typer.models.OptionInfo object>)[source]¶
Show open positions.
- Parameters:
risk (bool)
- Return type:
None
- trader.cli.main.run_once_command(dry_run=<typer.models.OptionInfo object>, strategy=<typer.models.OptionInfo object>)[source]¶
Walk the watchlist once: evaluate, trade, protect, and snapshot.
Guarded by a single-instance lock scoped to the resolved database path (issue #43): a second run/run-once pointed at the same database fails fast instead of racing this one.
- Parameters:
dry_run (bool)
strategy (str | None)
- Return type:
None
- trader.cli.main.run(dry_run=<typer.models.OptionInfo object>, max_cycles=<typer.models.OptionInfo object>)[source]¶
Run continuously: trade during market hours, idle when closed.
Stops cleanly on Ctrl-C or SIGTERM, finishing the cycle it is in first. Guarded by a single-instance lock scoped to the resolved database path (issue #43): a second run/run-once pointed at the same database fails fast instead of racing this one.
- Parameters:
dry_run (bool)
max_cycles (int | None)
- Return type:
None
- trader.cli.main.outcomes(limit=<typer.models.OptionInfo object>, start=<typer.models.OptionInfo object>, end=<typer.models.OptionInfo object>, since_creation=<typer.models.OptionInfo object>)[source]¶
Show closed round trips and their realized P/L.
Default start date (issue #57). Same rule as trader performance: without –start, this windows to config/reporting.yaml’s default_report_start_date onward (a dated, deployment-specific finding — see docs/operational-timeline.md — never a code constant), which excludes round trips CLOSED before the live service was running cleanly. –since-creation opts back into every closed round trip ever recorded; –start (or –start together with –since-creation) always wins. –limit still caps the page size within whatever window is in effect — the totals line below the table always spans every closed trip IN THE WINDOW, not just the page shown, exactly as it did before this window existed.
- Parameters:
limit (int)
start (datetime | None)
end (datetime | None)
since_creation (bool)
- Return type:
None
- trader.cli.main.sim_report(start=<typer.models.OptionInfo object>, end=<typer.models.OptionInfo object>, since_creation=<typer.models.OptionInfo object>)[source]¶
Show every strategy’s shadow-portfolio performance (issue #33), each paired with its Always-be-Buying (AbB) variant (issue #42).
One row pair per configured strategy — shadow and trading alike, since the whole point of the simulator is comparing all of them on equal footing, something the live account (which only the trading strategy ever touches) cannot show. “Return excl. stops” answers “how did this strategy’s own sell signals do”, the same ledger split the spec’s acceptance criteria asked for.
The AbB variant — same recorded decisions, sell signals ignored, only the trailing stop can exit — is shown directly under its strategy’s own row, with “ABB DELTA %” carrying the headline number: how much better or worse a never-sell policy would have done than the strategy’s own sell logic actually did. A strategy with no AbB portfolio yet (no cycle has stepped it since issue #42 shipped) just shows its normal row alone, with the delta column reading as unknown rather than crashing the command.
Default start date and windowing (issue #57). Without –start, this windows to config/reporting.yaml’s default_report_start_date onward (unset means unchanged, full-history behaviour); –since-creation opts back into full history; an explicit –start always wins over both. UNLIKE trader performance, windowing here does NOT re-run the simulator — see _sim_portfolio_metrics’s docstring for the exact design decision. In short: EQUITY and RETURN % are recomputed from the WINDOW’s own first/last sim_equity_snapshots row (mirroring trader performance’s own account.start_at/end_at/return_pct shape) rather than from the portfolio’s live current mark-to-market, and WINS/LOSSES/STOP EXITS/MAX DRAWDOWN % are all computed only from data that falls inside the window. A window containing fewer than 2 equity snapshots shows MISSING (-) for EQUITY/RETURN %/RETURN % EXCL. STOPS rather than a fabricated 0.00%.
- Parameters:
start (datetime | None)
end (datetime | None)
since_creation (bool)
- Return type:
None
- trader.cli.main.chat(message=<typer.models.OptionInfo object>)[source]¶
Chat with the local model about positions, trades, decisions, backtests, and strategy configs (requirements §13; issues #13-#16).
A second entry point into the Core Service Layer (requirements §4): this reads the same trades/decisions/account_snapshots tables the trading loop writes and the same config/strategies.yaml, but needs neither Alpaca credentials nor a running daemon — and conversely, trader run/run-once need no chat process alive. Nothing reachable from here can place an order or write to config/strategies.yaml; a backtest request against a non-backtestable strategy (ollama_news) is refused rather than attempted.
- Parameters:
message (str | None)
- Return type:
None