Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions specs/001-etf-paper-trading-agent/contracts/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

---

Expand Down
76 changes: 68 additions & 8 deletions specs/001-etf-paper-trading-agent/contracts/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
```
Expand All @@ -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**:
Expand Down Expand Up @@ -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.
26 changes: 18 additions & 8 deletions specs/001-etf-paper-trading-agent/contracts/report-context.md
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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"

Expand All @@ -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)

Expand Down Expand Up @@ -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

Expand All @@ -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.
20 changes: 10 additions & 10 deletions specs/001-etf-paper-trading-agent/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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 |
Expand All @@ -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:

Expand All @@ -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 |
|---|---|---|
Expand All @@ -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 |
Expand Down Expand Up @@ -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).

---

Expand Down
28 changes: 13 additions & 15 deletions specs/001-etf-paper-trading-agent/research.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

---

Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion specs/001-etf-paper-trading-agent/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions specs/001-etf-paper-trading-agent/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
`<section id="investment-thesis">`. 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.
Expand Down
Loading