From 18b54122f36053e09a7da070b9bd06a1723f272e Mon Sep 17 00:00:00 2001 From: Mohammad Date: Sun, 9 Aug 2026 15:24:05 +0200 Subject: [PATCH] docs: bring specs back in line with the code Section B of the round-3 audit: documentation-only drift, no behaviour changes. data-model.md - Config model still had ticker1/ticker2; it is tickers: list[str] with any number supported. extended_hours was missing. session_money_budget now documents the >0 rule. - candle_timeframe was missing 10min and 15min. - Session id was "session-{random_seq}"; it is a zero-padded ordinal, run-1/session-0001, which is also what spec.md Clarification Q4 says. - execution_result values were EXECUTED/SKIPPED_BUDGET/SKIPPED_MARKET_CLOSED/ SKIPPED_API_ERROR; the code writes EXECUTED/HOLD/FAILED, with the specific reason in ORDER_FAILED telemetry. - trailing_stop_high_watermark was documented as NULL for non-Momentum; it is seeded to entry_price for every position (#130). - "zero, one, or two Positions per Session" predated multi-ticker. contracts/agents.md, research.md - The execution-telemetry known gap was closed by #141; both files still described it as open. Replaced with what is actually emitted. - research.md said the execution agent subclasses google.adk.BaseAgent and overrides _run_async_impl. It is a plain Python class with model = None; recorded why the ADK subclass was dropped. - The EVALUATION_FAILED-missing-from-EVENT_TYPES note was also stale. contracts/report-context.md - Named _run_session; the method is _process_session. - Omitted ticker_details[].execution_result, which is populated. - Said the feedback agent parses id="investment-thesis"; it is ticker-scoped as investment-thesis-{ticker} (#134), with a fallback for old reports. contracts/cli.md - Documented 3 of 6 commands; version, verify-telemetry and reset were missing. - --timeframe listed 3 of 5 values. - --budget said "0 or negative = no limit"; negative is now rejected. - Sample output used the old random-suffix session ids. spec.md - Said the investigation agent is invoked "per unblocked ticker"; it is invoked once with all unblocked tickers so it can reason about them side by side. tasks.md - Left as a historical task record, with a note at the top pointing at the two places it has been overtaken: the ordinal evaluation window (replaced by an absolute UTC deadline in #122) and the unscoped thesis id (#134). --- .../contracts/agents.md | 12 +-- .../contracts/cli.md | 76 +++++++++++++++++-- .../contracts/report-context.md | 26 +++++-- .../001-etf-paper-trading-agent/data-model.md | 20 ++--- specs/001-etf-paper-trading-agent/research.md | 28 ++++--- specs/001-etf-paper-trading-agent/spec.md | 2 +- specs/001-etf-paper-trading-agent/tasks.md | 17 +++++ 7 files changed, 134 insertions(+), 47 deletions(-) diff --git a/specs/001-etf-paper-trading-agent/contracts/agents.md b/specs/001-etf-paper-trading-agent/contracts/agents.md index 6cfd568..306758b 100644 --- a/specs/001-etf-paper-trading-agent/contracts/agents.md +++ b/specs/001-etf-paper-trading-agent/contracts/agents.md @@ -53,11 +53,13 @@ Gating below) and re-inserts them as Hold afterwards. 6. Place market order via `alpaca-py` 7. On success: write `Position` record to memory bank with `status=OPEN`, `direction=BUY` -**Known gap**: unlike `main_agent`, `monitor`, and `feedback_agent`, `execution/agent.py` -does not currently have a `TelemetryLogger` wired in and emits no telemetry events -(`ORDER_PLACED`/`ORDER_FAILED`/`BUDGET_CHECK`, though declared in `telemetry/logger.py`'s -`EVENT_TYPES`, are never emitted in the current implementation). See research.md -§Telemetry for the full known-gap note. +**Telemetry**: `execution/agent.py` takes a `TelemetryLogger` and emits on every order +path. `BUDGET_CHECK` before each entry (carrying `remaining_session_budget`, since the +session budget is spent down across tickers), `ORDER_PLACED` on a placed order, and +`ORDER_FAILED` on every refusal with a `reason` of `INSUFFICIENT_BUDGET`, +`FEEDBACK_BLOCKED`, `NO_OPEN_POSITION`, or `API_ERROR`. A run that silently places no +orders is therefore distinguishable from one where the agent decided to Hold (FR-017, +SC-004). --- diff --git a/specs/001-etf-paper-trading-agent/contracts/cli.md b/specs/001-etf-paper-trading-agent/contracts/cli.md index 7c1858e..a63a793 100644 --- a/specs/001-etf-paper-trading-agent/contracts/cli.md +++ b/specs/001-etf-paper-trading-agent/contracts/cli.md @@ -18,9 +18,9 @@ Options: --config PATH Path to JSON config file. Default: ./config.json --tickers TEXT Comma-separated ticker symbols, e.g. SPY,QQQ. Overrides config. --exchange TEXT Optional/informational. Alpaca routes automatically. Overrides config. - --timeframe TEXT Candle timeframe: 30min | 1H | 4H. Overrides config. + --timeframe TEXT Candle timeframe: 10min | 15min | 30min | 1H | 4H. Overrides config. --duration TEXT Run duration: e.g. 8H | 24H. Overrides config. - --budget FLOAT Session money budget in USD. Overrides config. 0 or negative = no limit. + --budget FLOAT Session money budget in USD. Overrides config. 0 = no limit. --stop-loss FLOAT Stop-loss percentage, e.g. 0.02 for 2%. Overrides config. --help Show this message and exit. ``` @@ -40,14 +40,14 @@ Memory bank: /home/user/.alphoryn/memory.db — 0 open positions loaded **Session completion** (one line per session; ticker decisions are pipe-separated, not one line per ticker): ``` -[run-1/session-a3f7] DECISION SPY: BUY (MEAN_REVERSION) | QQQ: HOLD (MOMENTUM) -[run-1/session-a3f7] Report -> reports/run-1/session-a3f7.html +[run-1/session-0001] DECISION SPY: BUY (MEAN_REVERSION) | QQQ: HOLD (MOMENTUM) +[run-1/session-0001] Report -> reports/run-1/session-0001.html ``` **Failure / skip**: ``` -[run-1/session-b9c2] SKIPPED investigation budget exceeded -[session-c1d4] MARKET_CLOSED — waiting for next candle +[run-1/session-0002] SKIPPED investigation budget exceeded +[run-1/session-0003] MARKET_CLOSED - waiting for next candle ``` **Exit codes**: @@ -100,7 +100,67 @@ Options: **Output** (table, most recent first; one column per ticker in the run's config snapshot): ``` Session Candle Close SPY QQQ -run-1/session-a3f7 2026-07-03 14:00 MR -> BUY (exec) MOM -> HOLD -run-1/session-b9c2 2026-07-03 15:00 MOM -> HOLD MOM -> SELL (exec) +run-1/session-0001 2026-07-03 14:00 MR -> BUY (exec) MOM -> HOLD +run-1/session-0002 2026-07-03 15:00 MOM -> HOLD MOM -> SELL (exec) ... ``` + +--- + +## Command: `alphoryn version` + +Print the version and exit. + +``` +Usage: alphoryn version + +Options: + --help +``` + +**Output**: +``` +Alphoryn v0.0.1 +``` + +--- + +## Command: `alphoryn verify-telemetry` + +Count what the memory bank actually recorded. Use it to confirm a run wrote anything at +all before going looking in GCP Logs Explorer. + +``` +Usage: alphoryn verify-telemetry [OPTIONS] + +Options: + --db PATH Memory bank path. Default: ~/.alphoryn/memory.db + --help +``` + +**Output**: +``` +Telemetry check for /home/you/.alphoryn/memory.db: + Runs recorded: 3 + Sessions recorded: 41 + Positions recorded: 6 +``` + +Exits 2 if the memory bank cannot be opened. + +--- + +## Command: `alphoryn reset` + +Delete the memory bank database. Prompts for confirmation unless `--force` is given. + +``` +Usage: alphoryn reset [OPTIONS] + +Options: + --db PATH Memory bank path to reset. Default: ~/.alphoryn/memory.db + --force, -f Skip the confirmation prompt. + --help +``` + +A database that does not exist is not an error - the command says so and exits 0. diff --git a/specs/001-etf-paper-trading-agent/contracts/report-context.md b/specs/001-etf-paper-trading-agent/contracts/report-context.md index ae3856e..4b72bd2 100644 --- a/specs/001-etf-paper-trading-agent/contracts/report-context.md +++ b/specs/001-etf-paper-trading-agent/contracts/report-context.md @@ -1,10 +1,10 @@ # Report Template Context: Alphoryn -Phase 1 output | Date: 2026-07-05 (updated 2026-07-21) | Plan: ../plan.md +Phase 1 output | Date: 2026-07-05 (updated 2026-08-09) | Plan: ../plan.md Documents the Jinja2 context object passed by `reports/generator.py` to the unified session report template (`templates/reports/session.html.j2`), as actually built by -`scheduler/scheduler.py::_run_session` (or equivalent session-loop method). +`scheduler/scheduler.py::_process_session`. ## Context Object Fields (as built by the scheduler) @@ -16,7 +16,8 @@ strategy: str|None -- known gap: currently set from the FIRST ticker's not per ticker (see Known Gaps) signals: dict|None -- known gap: currently always None (never populated) — the Signal Snapshot section of the template never renders -execution_result: str|None -- known gap: currently always None (never populated) +execution_result: str|None -- known gap: the top-level field is always None. The real + per-ticker result lives in ticker_details[].execution_result position: dict|None -- known gap: currently always None (never populated) — the Position section always renders "No position opened this session" @@ -27,6 +28,8 @@ action: str -- "BUY", "SELL", or "HOLD" strategy: str|None -- "MEAN_REVERSION" or "MOMENTUM"; None if HOLD with no strategy selected reasoning: str -- agent's full reasoning text; IS the investment thesis, rendered per ticker memory_summary: str|None -- known gap: currently always None (never populated from the memory bank) +execution_result: str|None -- "EXECUTED", "HOLD" or "FAILED" for this ticker, from + ExecutionAgent.execute(); None when execution did not run ## signals dict keys (when populated) @@ -54,9 +57,14 @@ scheduler never populates `signals`/`position`). ## Thesis extraction (feedback agent) -The feedback agent parses `section id="investment-thesis"` from the rendered HTML. -The template renders one such section per ticker (`{{ detail.ticker }} — Investment Thesis`), -and the `reasoning` field rendered inside is the thesis for that ticker. +The feedback agent parses `section id="investment-thesis-{ticker}"` from the rendered HTML +— the id is ticker-scoped, so a multi-ticker session cannot have one ticker's thesis judged +against another's outcome (issue #134). The `reasoning` field rendered inside is the thesis +for that ticker. + +Reports written before that change used one unscoped `investment-thesis` id for every +ticker. They are still reachable from the memory bank, so the agent falls back to the +unscoped id, and then to the whole document, before giving up. ## Known Gaps @@ -65,8 +73,10 @@ scheduler in the current implementation — they are always `None`, so the corre template sections never render real data: - `strategy` (top-level) — only the first ticker's strategy is passed; not accurate for multi-ticker sessions where tickers run different strategies (spec FR-008) -- `signals`, `execution_result`, `position` — always `None`; the Signal Snapshot and - Position sections of the report never show data even when a trade executed +- `signals`, top-level `execution_result`, `position` — always `None`; the Signal Snapshot + and Position sections of the report never show data even when a trade executed. + `ticker_details[].execution_result` *is* populated, so the per-ticker decision table does + show what happened to each order - `ticker_details[].memory_summary` — always `None`; the memory-context box never renders These are implementation gaps to track separately, not documentation errors. diff --git a/specs/001-etf-paper-trading-agent/data-model.md b/specs/001-etf-paper-trading-agent/data-model.md index b477d46..4462572 100644 --- a/specs/001-etf-paper-trading-agent/data-model.md +++ b/specs/001-etf-paper-trading-agent/data-model.md @@ -13,12 +13,12 @@ for all session parameters (design doc §Configuration table; spec FR-001). | Field | Type | Default | Notes | |---|---|---|---| -| `ticker1` | `str` | required | Ticker symbol (e.g., `SPY`) | -| `ticker2` | `str` | required | Ticker symbol (e.g., `QQQ`) | -| `candle_timeframe` | `str` | `"1H"` | One of: `"30min"`, `"1H"`, `"4H"` | +| `tickers` | `list[str]` | required | At least 2 ticker symbols, e.g. `["SPY", "QQQ"]`. Any number is supported; they are evaluated independently | +| `candle_timeframe` | `str` | `"1H"` | One of: `"10min"`, `"15min"`, `"30min"`, `"1H"`, `"4H"` | +| `extended_hours` | `bool` | `False` | Allows pre/post-market execution; testing affordance | | `run_duration` | `str` | `"24H"` | e.g., `"24H"`, `"8H"` | | `exchange` | `str \| None` | `None` | Optional, informational only — Alpaca routes US equities automatically; market hours from Alpaca calendar API | -| `session_money_budget` | `float \| None` | `None` | USD; `None` means no budget constraint | +| `session_money_budget` | `float \| None` | `None` | USD; must be > 0 when set. `None` means no budget constraint | | `stop_loss_pct` | `float` | `0.02` | e.g., `0.02` = 2% below entry price | | `currency` | `str` | `"USD"` | Display currency — USD for Alpaca paper accounts | | `memory_db_path` | `str` | `"~/.alphoryn/memory.db"` | SQLite file path | @@ -87,7 +87,7 @@ One record per candle close processed. Linked to its Run. | Column | Type | Notes | |---|---|---| -| `id` | `TEXT PK` | Composite: `run-{run_id}/session-{random_seq}` (spec Clarification Q4) | +| `id` | `TEXT PK` | Composite: `run-{run_id}/session-{ordinal}`, ordinal zero-padded to 4 digits, e.g. `run-1/session-0001` (spec Clarification Q4) | | `run_id` | `INTEGER FK → Run.id` | | | `candle_close_at` | `DATETIME` | Candle close timestamp (UTC) | | `created_at` | `DATETIME` | When session record was written | @@ -96,7 +96,7 @@ One record per candle close processed. Linked to its Run. | `ticker_decisions` | `TEXT \| NULL` | JSON object keyed by ticker symbol, e.g. `{"SPY": {"strategy": "MEAN_REVERSION", "decision": "BUY", "execution_result": "EXECUTED"}, ...}`. One entry per ticker processed this session — supports any number of configured tickers, not just two. | | `warnings` | `TEXT \| NULL` | JSON list of warning strings | -Per-ticker `strategy` is `MEAN_REVERSION` or `MOMENTUM`; `decision` is `BUY`, `SELL`, or `HOLD`; `execution_result` is `EXECUTED`, `SKIPPED_BUDGET`, `SKIPPED_MARKET_CLOSED`, or `SKIPPED_API_ERROR`. +Per-ticker `strategy` is `MEAN_REVERSION` or `MOMENTUM`; `decision` is `BUY`, `SELL`, or `HOLD`; `execution_result` is `EXECUTED`, `HOLD`, or `FAILED` — the value returned by `ExecutionAgent.execute()`. `FAILED` covers every refused or failed order (insufficient budget, feedback-blocked ticker, no open position to sell, Alpaca API error); the specific reason is in the `ORDER_FAILED` telemetry event, not in this field. Session `status` values distinguish *why* a candle produced no decision: @@ -114,7 +114,7 @@ Only `COMPLETED` counts against the run's session budget (FR-018). ### Position -One record per open paper trade. Ticker-scoped; the two tickers are fully independent. +One record per open paper trade. Ticker-scoped; tickers are fully independent of each other. | Column | Type | Notes | |---|---|---| @@ -128,7 +128,7 @@ One record per open paper trade. Ticker-scoped; the two tickers are fully indepe | `lot_size` | `REAL` | Units / shares purchased | | `stop_loss_price` | `REAL` | Derived: `entry_price * (1 - stop_loss_pct)` | | `exit_target` | `TEXT` | JSON: `{"type": "price_level", "value": 123.45}` for Mean Reversion; `{"type": "trailing_stop", "trail_pct": 0.015}` for Momentum | -| `trailing_stop_high_watermark` | `REAL \| NULL` | Updated by monitor when price makes a new high; used for trailing stop computation; NULL for non-Momentum positions | +| `trailing_stop_high_watermark` | `REAL \| NULL` | Initialised to `entry_price` for **every** position, then updated by the monitor when price makes a new high; used for trailing stop computation. Left NULL, a position that gaps down before ever printing a new high would seed its trail floor from the lower price (issue #130) | | `evaluation_window_close_at` | `DATETIME` | Absolute UTC deadline at which the window expires and the feedback agent fires. Derived at entry: `entry_time + N x candle_timeframe`, N = 4 for Mean Reversion, 2 for Momentum. Stored as wall-clock rather than a session ordinal so it stays meaningful across runs, restarts, and market-closed sessions | | `status` | `TEXT` | See Position States below | | `exit_price` | `REAL \| NULL` | NULL until closed | @@ -205,9 +205,9 @@ Run ──< Session ──< Position ──< FeedbackEvaluation ``` - One Run has many Sessions. -- One Session has zero, one, or two Positions (one per ticker, only if Buy was executed). +- One Session has zero or more Positions (at most one per configured ticker, only if a Buy was executed). - One Position has zero or one FeedbackEvaluation. -- One Session has zero, one, or two MemoryEntry records (one per ticker that was processed). +- One Session has zero or more MemoryEntry records (one per ticker that was processed). --- diff --git a/specs/001-etf-paper-trading-agent/research.md b/specs/001-etf-paper-trading-agent/research.md index 3667907..4eba89a 100644 --- a/specs/001-etf-paper-trading-agent/research.md +++ b/specs/001-etf-paper-trading-agent/research.md @@ -202,14 +202,14 @@ order placement, confirmation). No reasoning or language model is required. ADK provides the same ADK infrastructure (event bus, tool integration, lifecycle hooks) without attaching a model. -**Implementation**: `execution/agent.py` subclasses `google.adk.BaseAgent` and overrides -`_run_async_impl`. It calls `alpaca-py` directly (not via MCP server) for maximum control -and testability. Unit tests mock the `alpaca-py` client and assert fixed outputs for fixed -decision inputs — satisfying constitution Principle I (Determinism). - -**Why not pure Python?** Using ADK BaseAgent keeps the execution agent in the same event -and observability framework as the rest of the system (traces, lifecycle hooks, cancellation -propagation) without adding non-determinism. +**Implementation**: `execution/agent.py` is a plain Python class with `model = None`, +not an ADK agent. The ADK subclass was considered and dropped: the execution agent is +called synchronously by the scheduler with a `SessionDecision` and returns a per-ticker +result dict, so the ADK event bus and async lifecycle bought nothing and cost an async +boundary in the one place the system most needs to be simple and deterministic. It calls +`alpaca-py` directly (not via MCP server) for maximum control and testability. Unit tests +mock the `alpaca-py` client and assert fixed outputs for fixed decision inputs — +satisfying constitution Principle I (Determinism). --- @@ -255,13 +255,11 @@ correlation without a separate tracing backend. | `MONITOR_STARTED` / `MONITOR_STOPPED` | `scheduler` | (empty payload) | | `EVALUATION_FAILED` | `feedback_agent` | `position_id`, `ticker`, `error` | -**Known gap**: `ORDER_PLACED`, `ORDER_FAILED`, and `BUDGET_CHECK` are declared in the -`EVENT_TYPES` constant but are never emitted — `execution/agent.py` has no -`TelemetryLogger` wired in and does not call `emit()` at all. Also, `EVALUATION_FAILED` -(emitted by `feedback_agent.py`) is missing from the `EVENT_TYPES` constant itself -(the constant is documentation-only and not enforced by `emit()`, so this doesn't raise -an error, but it is inconsistent). These are implementation gaps to track separately, -not a documentation error. +`ORDER_PLACED`, `ORDER_FAILED`, and `BUDGET_CHECK` are emitted by +`execution/agent.py`, which takes a `TelemetryLogger` like the other components. +`EVALUATION_FAILED` is emitted by both `feedback_agent.py` (all 3 attempts failed) and +`scheduler.py` (an evaluation escaped the agent entirely), and is present in the +`EVENT_TYPES` constant. The constant is documentation-only and not enforced by `emit()`. GCP Logs Explorer is the primary observability UI — filter by `session_id`, `event_type`, `component`, or `etf` to query any slice of system activity. diff --git a/specs/001-etf-paper-trading-agent/spec.md b/specs/001-etf-paper-trading-agent/spec.md index 1a87a07..bae8eae 100644 --- a/specs/001-etf-paper-trading-agent/spec.md +++ b/specs/001-etf-paper-trading-agent/spec.md @@ -141,7 +141,7 @@ At a strategy-defined point after the entry session (1–2 sessions for Momentum The system is composed of four agents with a strict separation between reasoning (LLM-assisted) and execution (deterministic). This separation is a non-negotiable design principle: any agent that places or closes a trade MUST be deterministic and produce identical outputs for identical inputs. **Investigation Agent** (LLM-assisted, reasoning) -Responsible for market regime recognition and per-session decision-making. At each candle close it receives a frozen market data snapshot, plus each ticker's recent feedback judgments and strategy performance history from the memory bank, and produces a structured decision record — one action (Buy/Sell/Hold), strategy, lot size, exit target, and reasoning summary per ticker. Aside from the memory bank query, it operates exclusively on the frozen snapshot; no live market data may be queried during the decision process. Feedback-blocked tickers (FR-005) are excluded from its input entirely — it is never invoked for a blocked ticker, and that ticker's session outcome is recorded as Hold without an investigation call. Invoked once per candle close, per unblocked ticker. +Responsible for market regime recognition and per-session decision-making. At each candle close it receives a frozen market data snapshot, plus each ticker's recent feedback judgments and strategy performance history from the memory bank, and produces a structured decision record — one action (Buy/Sell/Hold), strategy, lot size, exit target, and reasoning summary per ticker. Aside from the memory bank query, it operates exclusively on the frozen snapshot; no live market data may be queried during the decision process. Feedback-blocked tickers (FR-005) are excluded from its input entirely — it is never invoked for a blocked ticker, and that ticker's session outcome is recorded as Hold without an investigation call. Invoked once per candle close, with all unblocked tickers in a single call — one snapshot, one decision record covering every ticker, so the agent can reason about them side by side. **Execution Agent** (deterministic, no reasoning) Responsible for carrying out the decisions produced by the investigation agent. Processes each ticker's decision sequentially, validates it against the session money budget, and submits market orders. Contains no LLM logic; given the same inputs it always produces the same result. Execution failures result in a Hold and a log entry, never in a retry loop. diff --git a/specs/001-etf-paper-trading-agent/tasks.md b/specs/001-etf-paper-trading-agent/tasks.md index c71dfb4..fa359c0 100644 --- a/specs/001-etf-paper-trading-agent/tasks.md +++ b/specs/001-etf-paper-trading-agent/tasks.md @@ -16,6 +16,23 @@ --- +## Note: this file is a historical record + +Task descriptions below say what was planned at the time and are left as written. Where +the shipped code has since moved on, the current behaviour is in `spec.md`, +`data-model.md`, and `contracts/`, and those win. Two differences show up repeatedly: + +- **Evaluation window.** T032–T035 and T038–T040 describe + `evaluation_window_session == current_session_ordinal`. That ordinal was replaced by an + absolute UTC deadline, `Position.evaluation_window_close_at`, so a window survives the + run that opened it (issue #122). See data-model.md §Position. +- **Thesis section id.** T037 and T038 describe parsing + `
`. The id is now ticker-scoped, + `investment-thesis-{ticker}`, so a multi-ticker session cannot judge one ticker's thesis + against another's outcome (issue #134). See contracts/report-context.md. + +--- + ## Phase 1: Setup **Purpose**: Project initialization and skeleton. Must complete before any module is written.