diff --git a/.gitignore b/.gitignore index 2efb2b8..06182a7 100644 --- a/.gitignore +++ b/.gitignore @@ -229,6 +229,7 @@ TELEMETRY_ACCESS.md # speckit spec-driven-development scaffolding (local only) .specify/ +specs/ # Local runtime config (copy config.example.json to get started) config.json diff --git a/TELEMETRY_ACCESS.md b/TELEMETRY_ACCESS.md deleted file mode 100644 index 2cf9dca..0000000 --- a/TELEMETRY_ACCESS.md +++ /dev/null @@ -1,299 +0,0 @@ -# Alphoryn Telemetry Systems - Access Guide - -## Two Kinds of Telemetry Data - -Alphoryn implements **two complementary telemetry systems** for complete observability: - ---- - -## 1. STRUCTURED EVENT LOGGING (Cloud Logging) - -### What it captures: -- 14 defined event types: AGENT_DECISION, TOOL_CALL, ORDER_PLACED, STOP_LOSS_TRIGGERED, etc. -- Every LLM decision, tool call, order execution, and system action -- Structured JSON format for easy parsing and correlation - -### Status: ⚠️ Code path works; verify events are actually landing in your project - -**Correction to an earlier claim in this doc**: telemetry events are **not** stored in the -local SQLite memory bank. `TelemetryLogger.emit()` (`alphoryn/telemetry/logger.py`) writes -*only* to Cloud Logging, or to stderr as a fallback if Cloud Logging is unreachable — never -to SQLite. The `sessions` / `positions` / `memory_entries` tables are separate trading-state -records (decisions, prices, P&L), not the structured event log. Don't confuse the two when -diagnosing "where did my telemetry go." - -**Verified 2026-07-10**: a manual `TelemetryLogger().emit(...)` call against this machine's -ADC credentials landed successfully in Cloud Logging (project `alphoryn`) within seconds. -But a query for `jsonPayload.component="main_agent"` returned **zero** results — meaning the -decisions from the earlier trading run never reached Cloud Logging at all. Most likely -explanation: at the time that run executed, the Cloud Logging client wasn't authenticated -yet (or failed for some other reason), so every `emit()` call silently fell back to stderr -per constitution Principle IV — and that stderr output wasn't captured to a persistent file. -**This is expected fail-safe behavior, not a bug** — but it does mean those specific events -are gone. If you need to be sure future runs are captured, redirect stderr to a file when -launching, e.g. `alphoryn run 2> telemetry-fallback.jsonl`, and check that file if Console -queries come up empty. - -#### A) GCP Cloud Logging (Primary, Persistent) - -Console → Logs Explorer. Two things trip people up on a "0 results" query: -1. **Time range picker** (top right) — Logs Explorer defaults to a short recent window - (often 1h). If the run you're checking happened earlier, widen it before trusting a - zero-result query. -2. **Project selector** (top left) — confirm you're viewing the `alphoryn` project, not - whatever project the Console last had selected. - -Pin the query to our custom log to rule out cross-project/cross-log noise: -``` -logName="projects/alphoryn/logs/alphoryn" - -# View all agent decisions -logName="projects/alphoryn/logs/alphoryn" -AND jsonPayload.event_type="AGENT_DECISION" -AND jsonPayload.component="main_agent" - -# Filter by session -logName="projects/alphoryn/logs/alphoryn" -AND jsonPayload.session_id="run-5/session-0001" - -# View all errors -logName="projects/alphoryn/logs/alphoryn" -AND jsonPayload.event_type="ORDER_FAILED" - -# By component (main_agent, execution_agent, monitor, scheduler) -logName="projects/alphoryn/logs/alphoryn" -AND jsonPayload.component="execution_agent" -``` - -#### B) Stderr Fallback (Development) -If Cloud Logging is unavailable, events are written to stderr as JSON: -```json -{ - "event_type": "AGENT_DECISION", - "session_id": "run-5/session-0001", - "component": "main_agent", - "ticker": "SPY", - "timestamp": "2026-07-10T13:00:45.123456+00:00", - "latency_ms": 2345, - "payload": { - "decision": "BUY", - "strategy": "MEAN_REVERSION", - "reasoning": "...", - "confidence": 0.85 - } -} -``` - -### Event Schema (All 14 Event Types) - -| Event Type | Component | When Emitted | Key Payload Fields | -|---|---|---|---| -| `AGENT_DECISION` | main_agent, feedback_agent | After agent decision | decision, reasoning, model_name, token_usage | -| `TOOL_CALL` | any agent | Before/after tool call | tool_name, tool_input, tool_output_summary | -| `SIGNAL_SNAPSHOT_BUILT` | main_agent | After snapshot creation | etf1_signals_summary, etf2_signals_summary | -| `ORDER_PLACED` | execution_agent | Order filled | ticker, side, qty, order_id | -| `ORDER_FAILED` | execution_agent | Order rejected | ticker, side, reason | -| `BUDGET_CHECK` | execution_agent | Budget validation | ticker, available_budget, required | -| `STOP_LOSS_TRIGGERED` | monitor | Price hits stop-loss | ticker, position_id, trigger_price | -| `PROFIT_TARGET_TRIGGERED` | monitor | Exit target reached | ticker, position_id, exit_target | -| `WINDOW_EXPIRY_TRIGGERED` | monitor | Evaluation window reached | ticker, position_id | -| `POSITION_CLOSED` | monitor | Position exit confirmed | ticker, position_id, exit_reason, pnl | -| `SESSION_START` | scheduler | Session begins | candle_close_at, open_positions_count | -| `SESSION_END` | scheduler | Session completes | status, duration_ms | -| `MARKET_CLOSED` | scheduler | Market unavailable | reason | -| `BUDGET_TIMEOUT` | scheduler | Time budget exceeded | phase, elapsed_ms | - ---- - -## 2. OPENTELEMETRY TRACING (Cloud Trace) - -### What it captures: -- Distributed traces of execution flow -- Span timing and latency analysis -- Dependency relationships between components -- Error and exception details - -### Status: ✅ Confirmed working — both Cloud Logging and Cloud Trace export verified end-to-end - -#### Full LLM capture (added 2026-08-09, branch `feat/otel-full-llm-capture`) - -What is now recorded for every LLM call, on top of the prompt/response content -that already worked: - -- **System prompt and tool definitions.** `setup_otel()` opts into the - experimental GenAI semantic conventions - (`OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`). Only that path - emits `gen_ai.system_instructions` and `gen_ai.tool_definitions`; the stable - path records what the model said but never what it was told or what tools it - had. Set the variable to `stable` to opt back out. -- **Thinking / reasoning.** Both agents now pass - `thinking_config(include_thoughts=True)` (`alphoryn/agents/thinking.py`). - Gemini reasons either way, but only returns the summary when asked, so before - this there was literally nothing for OTel to capture. - - ⚠️ Thought summaries arrive as extra text parts **before** the answer, flagged - `thought=True`. Any code reading `parts[0].text` or the first non-empty text - part will parse a thought as the answer. Both agents route through - `is_thought_part()`; any new response reader must too. - -#### Setup now fails loudly (exit code 4) - -`setup_otel()` used to catch every exception, log a warning, and let the run -continue with no exporters at all. Three ways that lost a whole run silently: -`google.auth.default()` failing; credentials resolving with no project ID (ADK -returns empty hooks here rather than raising, so nothing was ever thrown); and -a crash losing whatever was still buffered in the `BatchSpanProcessor`. - -It now raises `TelemetrySetupError`, the CLI reports it and exits **4**, and -`flush_otel()` is registered with `atexit`. `alphoryn run` also prints -`Telemetry -> GCP project ''` at startup — telemetry landing in the wrong -project (e.g. `wortcast`, gcloud's default on this box) looks identical to -telemetry landing nowhere. - -This does not contradict constitution Principle IV. Principle IV governs -per-event emission at run time, and `TelemetryLogger.emit` still falls back to -stderr and never blocks. Setup is a preflight check, the same class of thing as -config validation, which already exits 1. - -`alphoryn/telemetry/otel.py:setup_otel()` calls `get_gcp_exporters(enable_cloud_tracing=True, -enable_cloud_logging=True)` unconditionally at every CLI startup (`cli/main.py`). Getting here -required fixing three stacked gaps: - -1. **Cloud Logging crashed on startup** — `pyproject.toml` was missing - `opentelemetry-exporter-gcp-logging` (a pre-release package, `1.12.0a0`), which provides - the `opentelemetry.exporter.cloud_logging` module. PR #110 added - `opentelemetry-exporter-gcp-trace` and `opentelemetry-exporter-gcp-monitoring` but missed - this one. -2. **Spans were never created** — `setup_otel()` only passed `enable_cloud_logging=True` to - `get_gcp_exporters()`; `enable_cloud_tracing` defaults to `False`, so the span - exporter/processor was never constructed. -3. **Span export 400'd once created** — `telemetry.googleapis.com` rejects any span batch - whose OTel `Resource` lacks a `gcp.project_id` attribute. ADK's default resource detector - only reads it from the standard `OTEL_RESOURCE_ATTRIBUTES` env var, so `setup_otel()` now - resolves the project ID via `google.auth.default()` and sets that env var before - `maybe_set_otel_providers()` builds the `TracerProvider`. This also required adding - `opentelemetry-exporter-otlp-proto-http` as a dependency (the actual span exporter used by - ADK's `_get_gcp_span_exporter()`, pinned to match `google-adk`'s - `opentelemetry-api`/`opentelemetry-sdk` constraints). - -Verified 2026-07-10 end-to-end with a real `alphoryn run` session (`run-8/session-0001`): - -- ✅ `setup_otel()` completes with no warning (previously crashed on every startup) -- ✅ GenAI prompt/response content is exported to Cloud Logging via OTel — confirmed logs - under `projects/alphoryn/logs/gen_ai.system.message`, `gen_ai.user.message`, and - `gen_ai.choice`, timestamps matching the run exactly -- ✅ **Cloud Trace spans confirmed.** `trace_v1.ListTraces()` for the run window returned a - 22-span trace (`invocation` → `invoke_agent alphoryn_main_agent` → `call_llm` → - `generate_content gemini-2.5-pro` → `execute_tool ...`), matching the run's actual - tool-call sequence. - -```bash -# Already in pyproject.toml dependencies — for an existing venv, just: -pip install "opentelemetry-exporter-gcp-logging>=1.12.0a0" "opentelemetry-exporter-otlp-proto-http>=1.36.0" -``` - -#### A) GCP Cloud Trace Console -``` -Google Cloud Console → Cloud Trace → Trace List - -# Filter by service -service.name = "alphoryn" - -# View latency timeline -- Shows each component's execution time -- Parallel/sequential operations -- Critical path analysis -``` - -#### B) Programmatic Access (Python) -```python -from google.cloud.trace_v2 import TraceServiceClient -from google.cloud.trace_v2.types import GetTraceRequest - -client = TraceServiceClient() -# Requires Google Cloud credentials -``` - ---- - -## Comparison: Cloud Logging vs. Cloud Trace - -| Aspect | Cloud Logging | Cloud Trace | -|--------|---------------|------------| -| **Type** | Structured Events | Distributed Tracing | -| **Local Storage** | None — Cloud or stderr only | N/A | -| **Remote Storage** | GCP Logs Explorer | GCP Cloud Trace | -| **Use Case** | Event correlation, debugging decisions | Latency analysis, performance | -| **Query Method** | Logs Explorer filters | Trace UI with timeline | -| **Data Retention** | ~30 days (GCP default) | ~30 days (GCP default) | -| **Enabled by default** | ✅ Yes (`TelemetryLogger`, called from every component) | ✅ Yes (`setup_otel()` at CLI startup) | -| **Fallback** | stderr (when Cloud unavailable) | None — traces are simply dropped | -| **Verified 2026-07-10** | ✅ Confirmed — custom events + OTel `gen_ai.*` logs both landed | ✅ Confirmed — real spans found in Cloud Trace after a real run (see status above) | - ---- - -## Quick Start: View Your Telemetry - -### Step 1: Confirm credentials are live and events actually land -```bash -python -c " -from alphoryn.telemetry.logger import TelemetryLogger -t = TelemetryLogger() -print('cloud_logger initialized:', t._cloud_logger is not None) -t.emit('SESSION_START', 'diagnostic', {'test': True}, session_id='diag-test') -" -``` -If `cloud_logger initialized: False`, Cloud Logging isn't reachable and everything is going -to stderr instead — run `gcloud auth application-default login` and retry. - -### Step 2: Run alphoryn — events auto-export - -⚠️ On at least one Windows dev machine, `python -m alphoryn.cli.main run ...` silently -no-ops — exits 0, prints nothing, writes no memory-bank record. Root cause not yet -diagnosed; if a run "completes" instantly with no output, use the module-attribute -invocation instead, which is confirmed working: -```bash -python -c " -from alphoryn.cli.main import app -import sys -sys.argv = ['alphoryn', 'run', '--config', 'config.json'] -app() -" -``` -The installed console script (`alphoryn run ...`, from `[project.scripts]` in -`pyproject.toml`) was not tested here but is the intended long-term entry point. - -### Step 3: View in Google Cloud Console -1. Go to https://console.cloud.google.com/logs -2. Confirm the **project selector** (top left) is on `alphoryn` -3. Widen the **time range** (top right) to cover when the run actually happened -4. Filter: `logName="projects/alphoryn/logs/alphoryn" AND jsonPayload.component="main_agent"` -5. Inspect decision reasoning and latency - -### Step 4: View traces in Cloud Trace -Enabled by default as of 2026-07-10 (see status above) — traces are emitted automatically -alongside the run in Step 2, no separate flag or install needed. -``` -https://console.cloud.google.com/traces → filter service.name = "alphoryn" -``` - ---- - -## Feedback Agent Telemetry - -When the feedback agent evaluates a closed position: -1. **Event**: `AGENT_DECISION` with `component="feedback_agent"` -2. **Payload**: `outcome_judgment` (CORRECT | INCORRECT | NEUTRAL), `reasoning`, `thesis_vs_outcome` -3. **Database**: `FeedbackEvaluation` record created with full evaluation details -4. **Correlates**: Via `position_id` back to entry decision - ---- - -## Telemetry Guarantees - -Per Constitution Principle IV (Fail Loud, Hold Safe): -- ✅ All events emitted synchronously (no loss) -- ✅ Cloud Logging unavailable → fallback to stderr (never blocks) -- ✅ Structured JSON schema maintained (parseable always) -- ✅ Every failure emitted with sufficient diagnostic context -- ✅ Session_id on every event for trace correlation diff --git a/specs/001-etf-paper-trading-agent/checklists/requirements.md b/specs/001-etf-paper-trading-agent/checklists/requirements.md deleted file mode 100644 index fdb1dc6..0000000 --- a/specs/001-etf-paper-trading-agent/checklists/requirements.md +++ /dev/null @@ -1,46 +0,0 @@ -# Specification Quality Checklist: Alphoryn — Automated Ticker Paper Trading System - -**Purpose**: Validate specification completeness and quality before proceeding to planning -**Created**: 2026-07-03 -**Updated**: 2026-07-07 -**Feature**: [spec.md](../spec.md) - -## Content Quality - -- [x] No implementation details (languages, frameworks, APIs) -- [x] Focused on user value and business needs -- [x] Written for non-technical stakeholders -- [x] All mandatory sections completed - -## Requirement Completeness - -- [x] No [NEEDS CLARIFICATION] markers remain -- [x] Requirements are testable and unambiguous -- [x] Success criteria are measurable -- [x] Success criteria are technology-agnostic (no implementation details) -- [x] All acceptance scenarios are defined -- [x] Edge cases are identified -- [x] Scope is clearly bounded -- [x] Dependencies and assumptions identified - -## Feature Readiness - -- [x] All functional requirements have clear acceptance criteria -- [x] User scenarios cover primary flows -- [x] Feature meets measurable outcomes defined in Success Criteria -- [x] No implementation details leak into specification - -## Notes - -- All items pass. Spec updated 2026-07-07 (two passes) to reflect V0.0.1 implemented state: - - Terminology updated from "ETF" to "ticker" in spec.md and data-model.md (2026-07-07 pass; - PR #99 covered the codebase but missed contracts/agents.md, contracts/report-context.md, - and research.md — those were brought in line separately on 2026-07-21) - - Config: removed `exchange`, added `extended_hours` and `memory_db_path`; tickers is now `list[str]` (min 2) - - Session ID corrected to sequential format (`run-3/session-0001`) - - US1 scenario 1 session count corrected (24H/1H = 24 sessions, not 6) - - FR-007 budget updated to timeframe-relative (87% investigate / 13% decide+execute) - - FR-011 updated: unified HTML report covering all tickers per session - - Status changed to Implemented (all 43 tasks complete, 440 tests, 100% coverage) - - Clarification session 2026-07-07 added: ticker count, market/exchange model, new config fields, agent architecture separation - - Agent Architecture section added: four-agent topology, LLM vs deterministic split, interaction flow, communication pattern diff --git a/specs/001-etf-paper-trading-agent/contracts/agents.md b/specs/001-etf-paper-trading-agent/contracts/agents.md deleted file mode 100644 index 306758b..0000000 --- a/specs/001-etf-paper-trading-agent/contracts/agents.md +++ /dev/null @@ -1,181 +0,0 @@ -# Agent Integration Contracts: Alphoryn - -**Phase 1 output** | **Date**: 2026-07-05 | **Plan**: [../plan.md](../plan.md) - -Documents the data structures and protocols at the three internal integration boundaries -where components hand off control or data to each other. These are not persisted to the -memory bank — they are in-process Python objects. - ---- - -## Decision Handoff (`main_agent` → `execution/agent.py`) - -After investigation, `main_agent` produces a `SessionDecision` containing one `AssetDecision` -per ticker, in a list (supports any number of configured tickers, not just two). This object -is passed directly to `execution/agent.py` as a Python dataclass. - -```python -@dataclass(frozen=True) -class AssetDecision: - ticker: str - action: Literal["BUY", "SELL", "HOLD"] - strategy: Literal["MEAN_REVERSION", "MOMENTUM"] | None - lot_size: int | None # shares; None if action is SELL or HOLD - exit_target: dict | None # None if action is not BUY - reasoning: str # emitted to telemetry; not stored in DB - -@dataclass(frozen=True) -class SessionDecision: - session_id: str - decisions: list[AssetDecision] -``` - -`exit_target` format matches `Position.exit_target` in data-model.md: -- Mean Reversion: `{"type": "price_level", "value": 123.45}` -- Momentum: `{"type": "trailing_stop", "trail_pct": 0.015}` - -Blocked tickers never reach this handoff as anything but Hold: the scheduler filters them -out of the investigation input before `main_agent` is invoked (FR-005, see §Investigation -Gating below) and re-inserts them as Hold afterwards. - -**Execution sequence in `execution/agent.py`** (per ticker, sequential, via `execute()` iterating `decision.decisions`): -1. If action is HOLD: skip -2. If action is SELL: close the ticker's open position — submit a sell for the position's - full `lot_size`, then update `exit_price` / `exit_time` / `exit_reason=AGENT_EXIT` / - `status=CLOSED_AGENT_EXIT` on the existing row. No new `Position` row is written. If the - ticker has no open position the order is **rejected**, not submitted: submitting it - would open a short, which v0.0.1 does not support -3. If action is BUY and the ticker is feedback-blocked: force HOLD (FR-014). This is a - second gate for direct callers of `ExecutionAgent`; the scheduler has normally already - excluded the ticker. SELL is exempt — the blocking position is the very thing it unwinds -4. Budget check via `alpaca-py` account API (entry only; closing never needs buying power) -5. If budget insufficient: skip the ticker's order -6. Place market order via `alpaca-py` -7. On success: write `Position` record to memory bank with `status=OPEN`, `direction=BUY` - -**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). - ---- - -## Feedback Trigger (`scheduler` → `agents/feedback_agent.py`) - -The scheduler owns feedback triggering. At the start of each session, before running -investigation, the scheduler queries the memory bank for positions due for evaluation. - -**Trigger condition** (checked by `scheduler/scheduler.py` at each session start): -```python -# Positions whose evaluation window has closed and are closed but not yet evaluated -positions_due = memory_bank.query( - status IN ('CLOSED_STOP_LOSS', 'CLOSED_PROFIT_TARGET', 'CLOSED_WINDOW_EXPIRY'), - evaluation_window_close_at <= now, - # No FeedbackEvaluation record exists yet -) -``` - -The scheduler passes a `FeedbackInput` to `agents/feedback_agent.py` for each position due: - -```python -@dataclass(frozen=True) -class FeedbackInput: - position_id: int - session_id: str # session in which the position was opened - ticker: str - strategy: Literal["MEAN_REVERSION", "MOMENTUM"] - html_report_path: str # from Session.html_report_path of the entry session - entry_price: float - exit_price: float - exit_reason: str -``` - -**Feedback agent responsibilities**: -1. Fetch the candle close price at evaluation time via the same `MarketDataClient` (`market_data/client.py`) the Investigation Agent uses, querying the specific past timestamp rather than the latest candle -2. Read HTML report at `html_report_path` to extract the original investment thesis -3. Compare thesis to outcome → produce `CORRECT`, `INCORRECT`, or `NEUTRAL` judgment -4. Write `FeedbackEvaluation` record to memory bank -5. Update `Position.status` to `EVALUATED` -6. Write `MemoryEntry` record for the ticker/strategy pair -7. Emit `AGENT_DECISION` telemetry event - -**Retry policy** (spec FR-016a): up to 3 attempts per position. On 3rd failure: -- Write `FeedbackEvaluation` with `attempt_count=3` and partial data -- Update `Position.status` to `EVALUATION_FAILED` -- Emit `EVALUATION_FAILED` telemetry event (`position_id`, `ticker`, `error`) -- Unblock the ticker for new positions - -**Ordering**: feedback evaluation runs before investigation in the same session. If -evaluation for multiple positions is due in the same session, they run sequentially. - ---- - -## Monitor → Memory Bank (position close protocol) - -The monitor communicates with the rest of the system exclusively through the memory bank. -There is no inter-thread signaling — the monitor writes, the scheduler reads. - -**On exit condition detected** (`monitor/monitor.py`): -1. Call `alpaca-py` `close_position(ticker)` to close the position on Alpaca -2. On success: write `Position.exit_price`, `Position.exit_time`, `Position.exit_reason`, - update `Position.status` to `CLOSED_STOP_LOSS` / `CLOSED_PROFIT_TARGET` / `CLOSED_WINDOW_EXPIRY` -3. Emit the appropriate trigger telemetry event (`STOP_LOSS_TRIGGERED`, `PROFIT_TARGET_TRIGGERED`, - or `WINDOW_EXPIRY_TRIGGERED`, payload `ticker`/`exit_price`/`exit_reason`) followed by - `POSITION_CLOSED` (payload `ticker`/`status`) -4. On `close_position` API failure: the bank is left unchanged and no telemetry is emitted; - the position remains `OPEN` and is retried on the next poll cycle - -**Monitor lifecycle**: -- Constructed by `cli/main.py` together with its `threading.Event` stop signal, and handed - to the `Scheduler`, which owns the thread lifecycle -- Started as `threading.Thread` by `scheduler/scheduler.py` at run startup, after candle - alignment and before the first session -- The monitor needs nothing published to it during a run: each position carries its own - absolute `evaluation_window_close_at` deadline, so window expiry is driven by the wall - clock rather than by run-scoped session numbering -- Stopped via the `threading.Event` stop signal when the run ends normally or on hard abort -- If the run ends with positions still `OPEN`, the monitor thread is NOT stopped - the - scheduler waits candle by candle until all open positions are closed (stop-loss, profit - target, or window expiry). The CLI process must remain alive while positions are open. -- The stop signal is set only when no positions remain in `OPEN` status - ---- - -## Memory Bank Startup Load - -On `alphoryn run` startup, `memory/bank.py` loads all open positions: - -```python -# All positions with status OPEN, regardless of run -open_positions = session.query(Position).filter( - Position.status == "OPEN" -).order_by(Position.entry_time.asc()).all() -``` - -These are passed to the session loop and monitor at startup. Per FR-019: if a position -exists for a ticker from a prior run, that ticker is blocked until the position closes -*and* its feedback evaluation completes. - ---- - -## Investigation Gating (FR-005) - -A ticker is **feedback-blocked** while it holds a position that is still `OPEN`, or that -has closed but has no `FeedbackEvaluation` yet. `EVALUATED` clears the block, and so does -`EVALUATION_FAILED` (§Retry policy unblocks the ticker rather than stranding it). - -`MemoryBank.get_feedback_blocked_tickers()` is the single definition. At each session start -the scheduler: - -1. Computes the blocked set and emits `TICKER_BLOCKED` per blocked configured ticker -2. Passes only the unblocked tickers to `main_agent.decide` — a blocked ticker is never - part of an Investigation Agent call, which is the FR-005 requirement -3. Skips the `main_agent.decide` call entirely when every configured ticker is blocked -4. Re-expands the returned decision over all configured tickers in config order, recording - Hold for the blocked ones so the session outcome is still written - -Blocked tickers outside `cfg.tickers` (a stale position on a ticker no longer traded) are -ignored. diff --git a/specs/001-etf-paper-trading-agent/contracts/cli.md b/specs/001-etf-paper-trading-agent/contracts/cli.md deleted file mode 100644 index 48f5afc..0000000 --- a/specs/001-etf-paper-trading-agent/contracts/cli.md +++ /dev/null @@ -1,167 +0,0 @@ -# CLI Contract: Alphoryn - -**Phase 1 output** | **Date**: 2026-07-03 (updated 2026-07-21) | **Plan**: [../plan.md](../plan.md) - -Implemented by: `alphoryn/cli/main.py` (Typer) - ---- - -## Command: `alphoryn run` - -Start a paper trading session. Config file is the base; CLI options override individual -fields. At least `--tickers` (2 or more, comma-separated) must be present, via config or CLI. - -``` -Usage: alphoryn run [OPTIONS] - -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: 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 = no limit. - --stop-loss FLOAT Stop-loss percentage, e.g. 0.02 for 2%. Overrides config. - --help Show this message and exit. -``` - -**Known gap**: `extended_hours` and `memory_db_path` are config-only — there is no -`--extended-hours` or `--memory-db-path` CLI override for `run` (unlike `status`/`history`, -which take `--db`). - -**Startup output** (to stdout before first candle close): -``` -Alphoryn v0.0.1 — Paper Trading -Tickers: SPY, QQQ | Timeframe: 1H | Duration: 24H -Sessions planned: 6 -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-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-0002] SKIPPED investigation budget exceeded -[run-1/session-0003] MARKET_CLOSED - waiting for next candle -``` - -**Exit codes**: -| Code | Meaning | -|---|---| -| 0 | Run completed normally | -| 1 | Config validation error | -| 2 | Memory bank inaccessible or corrupt (hard abort) | -| 3 | Google Secret Manager unreachable at startup | -| 4 | OpenTelemetry could not be wired up - the run would not be traced | - ---- - -## Command: `alphoryn status` - -Show the current run state and all open positions. - -``` -Usage: alphoryn status [OPTIONS] - -Options: - --db PATH Memory bank path. Default: ~/.alphoryn/memory.db - --help -``` - -**Output** (one line per ticker configured for the latest run, from its config snapshot): -``` -Current run: run-1 (started 2026-07-03 14:00 UTC) -Sessions: 3 completed, 3 remaining - -Open positions: - SPY MEAN_REVERSION BUY @ 540.12 Stop: 529.32 Status: OPEN - QQQ (no open position) -``` - ---- - -## Command: `alphoryn history` - -Show session history from the memory bank. - -``` -Usage: alphoryn history [OPTIONS] - -Options: - --run INT Filter by run number. Default: latest run. - --db PATH Memory bank path. Default: ~/.alphoryn/memory.db - --help -``` - -**Output** (table, most recent first; one column per ticker in the run's config snapshot): -``` -Session Candle Close SPY QQQ -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/config-schema.md b/specs/001-etf-paper-trading-agent/contracts/config-schema.md deleted file mode 100644 index bb70afb..0000000 --- a/specs/001-etf-paper-trading-agent/contracts/config-schema.md +++ /dev/null @@ -1,64 +0,0 @@ -# Config Schema: Alphoryn - -**Phase 1 output** | **Date**: 2026-07-03 | **Plan**: [../plan.md](../plan.md) - -Implemented by: `alphoryn/config/models.py` (Pydantic) - ---- - -## JSON Config File (`config.json`) - -All fields optional except where noted. CLI arguments override any field. -No secrets belong in this file — credentials are fetched from Google Secret Manager. - -```json -{ - "tickers": ["SPY", "QQQ"], - "candle_timeframe": "1H", - "run_duration": "24H", - "extended_hours": false, - "session_money_budget": 1000.0, - "stop_loss_pct": 0.02, - "currency": "USD", - "memory_db_path": "~/.alphoryn/memory.db" -} -``` - -## Field Reference - -| Field | Required | Type | Allowed Values | Notes | -|---|---|---|---|---| -| `tickers` | Yes | list of string | Minimum 2 US-listed tickers | Evaluated independently; no cross-ticker correlation logic | -| `candle_timeframe` | No | string | `"10min"`, `"15min"`, `"30min"`, `"1H"`, `"4H"` | Default: `"1H"` | -| `run_duration` | No | string | `"NHM"` format, e.g. `"24H"`, `"8H"` | Default: `"24H"` | -| `extended_hours` | No | boolean | `true`/`false` | Default: `false`. Allows pre/post-market execution; testing affordance. | -| `exchange` | No | string or null | Any string | Optional, informational only — Alpaca routes US equities automatically; market hours from Alpaca calendar API. Default: `null`. | -| `session_money_budget` | No | float or null | Positive USD amount | `null` = no budget limit | -| `stop_loss_pct` | No | float | `(0, 1)` exclusive | Default: `0.02` (2%). Applied as hard config value at trade entry. | -| `currency` | No | string | `"USD"` | Default: `"USD"`. Only USD supported in v0.0.1 (Alpaca paper accounts are USD). | -| `memory_db_path` | No | string | Valid filesystem path | Default: `~/.alphoryn/memory.db` | - -## Validation Rules - -- `tickers` must contain at least 2 symbols (enforced by Pydantic validator) -- `run_duration` must be evenly divisible by `candle_timeframe` — if not, system warns and - rounds down at startup (spec FR-003); this is a warning, not a config error -- `stop_loss_pct` must be in range `(0, 1)` exclusive -- `session_money_budget`, if set, must be > 0 - -## Google Secret Manager Secrets - -These are NOT in the config file. They are fetched at startup by `secrets/client.py` and -injected as environment variables (`ALPACA_API_KEY`, `ALPACA_SECRET_KEY`) before the -Alpaca MCP server connection is established. - -| Secret name (GCP) | Env var injected | Required | -|---|---|---| -| `alphoryn-alpaca-api-key` | `ALPACA_API_KEY` | Yes | -| `alphoryn-alpaca-secret-key` | `ALPACA_SECRET_KEY` | Yes | - -Alpaca keys are obtained from alpaca.markets (free paper trading account). -MCP server runs in paper trading mode by default (`ALPACA_PAPER_TRADE=true`). - -GCP credentials: Application Default Credentials (`gcloud auth application-default login` -or set `GOOGLE_APPLICATION_CREDENTIALS`). No additional secret needed for Secret Manager itself. diff --git a/specs/001-etf-paper-trading-agent/contracts/report-context.md b/specs/001-etf-paper-trading-agent/contracts/report-context.md deleted file mode 100644 index 4b72bd2..0000000 --- a/specs/001-etf-paper-trading-agent/contracts/report-context.md +++ /dev/null @@ -1,82 +0,0 @@ -# Report Template Context: Alphoryn - -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::_process_session`. - -## Context Object Fields (as built by the scheduler) - -session_id: str -candle_close_at: str -- formatted "2026-07-05 14:00 UTC" -tickers: list[str] -- all tickers processed this session, e.g. ["SPY", "QQQ"] -ticker_details: list[dict] -- one entry per ticker, see below -strategy: str|None -- known gap: currently set from the FIRST ticker's decision only, - 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: 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" - -## ticker_details dict keys (one per ticker) - -ticker: str -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) - -rsi_14, adx_14, ema_20, ema_50, sma_20, bollinger_upper, bollinger_lower, -bollinger_pct_b, macd_line, macd_signal, macd_histogram, volume_vs_avg, -current_price, price_vs_ema_20_pct, price_vs_sma_20_pct - -All floats. Matches `AssetSignals` in data-model.md. - -## position dict keys (when populated) - -entry_price: float -lot_size: int -stop_loss_price: float -exit_target: dict -- {"type": "price_level", "value": 467.32} or {"type": "trailing_stop", "trail_pct": 0.015} -trailing_stop_high_watermark: float|None -- Momentum only; None for Mean Reversion - -## Template - -A single unified template renders the whole session report: -`templates/reports/session.html.j2`. It lists all tickers' decisions in a table (`ticker_details`), -renders one Investment Thesis section per ticker, and renders a single Signal Snapshot and -Position section (see Known Gaps — these are currently always empty in practice since the -scheduler never populates `signals`/`position`). - -## Thesis extraction (feedback agent) - -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 - -The following context fields are wired into the template but never populated by the -scheduler in the current implementation — they are always `None`, so the corresponding -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`, 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 deleted file mode 100644 index 4462572..0000000 --- a/specs/001-etf-paper-trading-agent/data-model.md +++ /dev/null @@ -1,222 +0,0 @@ -# Data Model: Alphoryn — Automated Ticker Paper Trading System - -**Phase 1 output** | **Date**: 2026-07-03 | **Plan**: [plan.md](plan.md) - -Design doc reference: `alphoryn_V_0_0.1.md §Memory Bank §Structure` - ---- - -## Config Model (Pydantic — not persisted) - -`AlphorynConfig` — loaded at startup, validated, passed to all components. Source of truth -for all session parameters (design doc §Configuration table; spec FR-001). - -| Field | Type | Default | Notes | -|---|---|---|---| -| `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; 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 | - -**Derived at startup** (not stored in config file): -- `session_count`: `int` = `floor(run_duration_seconds / candle_timeframe_seconds)` -- `alpaca_paper_mode`: `bool` = always `True` at v0.0.1 - ---- - -## SignalSnapshot (dataclass — not persisted) - -Frozen set of computed signals returned by the `build_snapshot` ADK tool. The agent calls -`build_snapshot` and receives this object; raw market data is fetched and processed -internally by `market_data/client.py` — the agent never sees OHLCV bars. Once -`build_snapshot` returns, no further market data tool calls may occur during investigation -(Principle V: Snapshot Isolation). - -| Field | Type | Notes | -|---|---|---| -| `captured_at` | `datetime` | Candle close timestamp (UTC) | -| `signals` | `dict[str, AssetSignals]` | Computed signals keyed by ticker symbol — one entry per configured ticker, not fixed to two | - -**`AssetSignals` fields** (computed by `market_data/client.py` from `alpaca-py` bars): - -| Field | Type | Description | -|---|---|---| -| `rsi_14` | `float` | RSI 14-period (0–100) | -| `adx_14` | `float` | Average Directional Index 14-period (0–100; >25 = trend) | -| `ema_20` | `float` | 20-period EMA price | -| `ema_50` | `float` | 50-period EMA price | -| `sma_20` | `float` | 20-period SMA price | -| `bollinger_upper` | `float` | Upper Bollinger Band (20-period, 2 std dev) | -| `bollinger_lower` | `float` | Lower Bollinger Band | -| `bollinger_pct_b` | `float` | %B: 0=lower band, 1=upper band (can exceed 0–1 range) | -| `macd_line` | `float` | EMA12 − EMA26 | -| `macd_signal` | `float` | 9-period EMA of MACD line | -| `macd_histogram` | `float` | `macd_line − macd_signal` | -| `volume_vs_avg` | `float` | Current volume / 20-period average volume | -| `current_price` | `float` | Latest close price | -| `price_vs_ema_20_pct` | `float` | `(current_price − ema_20) / ema_20 × 100` | -| `price_vs_sma_20_pct` | `float` | `(current_price − sma_20) / sma_20 × 100` | - ---- - -## Database Entities (SQLAlchemy / SQLite) - -### Run - -Tracks each invocation of `alphoryn run`. Sequential run number persists the `run-N` part -of the session identity scheme (spec Clarification Q4). - -| Column | Type | Notes | -|---|---|---| -| `id` | `INTEGER PK AUTOINCREMENT` | Sequential run number | -| `started_at` | `DATETIME` | UTC | -| `ended_at` | `DATETIME \| NULL` | NULL while running | -| `config_snapshot` | `TEXT` | JSON dump of AlphorynConfig (non-secret fields only) | -| `session_count_planned` | `INTEGER` | Derived at startup | - ---- - -### Session - -One record per candle close processed. Linked to its Run. - -| Column | Type | Notes | -|---|---|---| -| `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 | -| `status` | `TEXT` | `COMPLETED`, `SKIPPED_TIMEOUT`, `SKIPPED_MARKET_CLOSED`, `SKIPPED_DATA_UNAVAILABLE`, `SKIPPED_OVERRUN` | -| `html_report_path` | `TEXT \| NULL` | Relative path to HTML report file | -| `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`, `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: - -| Status | Meaning | -|---|---| -| `COMPLETED` | The session ran end to end and produced a decision record | -| `SKIPPED_TIMEOUT` | The investigation or execute budget ran out | -| `SKIPPED_MARKET_CLOSED` | The market was closed at that candle | -| `SKIPPED_DATA_UNAVAILABLE` | Market data could not be fetched | -| `SKIPPED_OVERRUN` | The previous session was still running when this candle closed — the data was fine | - -Only `COMPLETED` counts against the run's session budget (FR-018). - ---- - -### Position - -One record per open paper trade. Ticker-scoped; tickers are fully independent of each other. - -| Column | Type | Notes | -|---|---|---| -| `id` | `INTEGER PK AUTOINCREMENT` | | -| `session_id` | `TEXT FK → Session.id` | Entry session | -| `ticker` | `TEXT` | Ticker symbol | -| `strategy` | `TEXT` | `MEAN_REVERSION` or `MOMENTUM` | -| `direction` | `TEXT` | `BUY` (only Buy positions tracked; Sell closes an existing position) | -| `entry_price` | `REAL` | Execution fill price | -| `entry_time` | `DATETIME` | UTC | -| `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` | 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 | -| `exit_time` | `DATETIME \| NULL` | NULL until closed | -| `exit_reason` | `TEXT \| NULL` | `STOP_LOSS`, `PROFIT_TARGET`, `WINDOW_EXPIRY` (all monitor-driven), `AGENT_EXIT` (main agent decided Sell) | - -**Position States** (design doc §Step 4; spec FR-014): - -``` -OPEN - → CLOSED_STOP_LOSS (monitor: price ≤ stop_loss_price) - → CLOSED_PROFIT_TARGET (monitor: price reaches exit_target) - → CLOSED_WINDOW_EXPIRY (monitor: evaluation_window_close_at reached) - → CLOSED_AGENT_EXIT (execution agent: main agent decided Sell) - ↓ -EVALUATED (feedback agent: wrote evaluation record) -EVALUATION_FAILED (feedback agent: 3 retries exhausted — spec FR-016a) -``` - -All four `CLOSED_*` statuses block their ticker until evaluated (FR-005) and are picked up -by the feedback trigger once `evaluation_window_close_at` passes. - -**Sell handling**: a Sell decision closes the ticker's open position in place — it updates -`exit_price` / `exit_time` / `exit_reason` / `status` on the existing row and never writes -a new one. A Sell on a ticker with no open position is rejected outright rather than -submitted, since submitting it would open a short. Short positions are out of scope for -v0.0.1, which is why `direction` is always `BUY` and the monitor's exit checks are -long-only. - ---- - -### FeedbackEvaluation - -Written by the feedback agent after comparing thesis to outcome -(design doc §Feedback Loop; spec FR-016). - -| Column | Type | Notes | -|---|---|---| -| `id` | `INTEGER PK AUTOINCREMENT` | | -| `position_id` | `INTEGER FK → Position.id` | | -| `evaluated_at` | `DATETIME` | UTC | -| `evaluation_session_id` | `TEXT FK → Session.id` | Session at which evaluation ran | -| `candle_close_price` | `REAL` | 1H candle close at evaluation time | -| `thesis_summary` | `TEXT` | Extracted from entry HTML report | -| `outcome_judgment` | `TEXT` | `CORRECT`, `INCORRECT`, `NEUTRAL` | -| `reasoning` | `TEXT` | Agent explanation | -| `attempt_count` | `INTEGER` | 1–3 (spec FR-016a retry policy) | - ---- - -### MemoryEntry - -Per-ticker, per-strategy running performance record. Queryable by the main agent during -investigation (design doc §Memory Bank §Strategy performance log). - -| Column | Type | Notes | -|---|---|---| -| `id` | `INTEGER PK AUTOINCREMENT` | | -| `ticker` | `TEXT` | Ticker symbol | -| `strategy` | `TEXT` | `MEAN_REVERSION` or `MOMENTUM` | -| `session_id` | `TEXT FK → Session.id` | | -| `decision` | `TEXT` | `BUY`, `SELL`, `HOLD` | -| `outcome_judgment` | `TEXT \| NULL` | NULL until feedback evaluated | -| `regime_context` | `TEXT` | JSON summary of market conditions at session time | -| `created_at` | `DATETIME` | | - ---- - -## Entity Relationships - -``` -Run ──< Session ──< Position ──< FeedbackEvaluation - └──< MemoryEntry -``` - -- One Run has many Sessions. -- 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 or more MemoryEntry records (one per ticker that was processed). - ---- - -## Key Invariants - -- A Position in status `OPEN` always has a non-null `stop_loss_price` and `exit_target`. -- At most one Position per ticker may be in status `OPEN` at any time. -- A Session's `html_report_path` is non-null iff `status = COMPLETED`. -- `FeedbackEvaluation.attempt_count` is always ≤ 3; if 3 and evaluation not complete, - parent Position moves to `EVALUATION_FAILED` and the ticker is unblocked (spec FR-016a). -- `MemoryEntry.outcome_judgment` is populated only after the linked Position has a - `FeedbackEvaluation` record. diff --git a/specs/001-etf-paper-trading-agent/plan.md b/specs/001-etf-paper-trading-agent/plan.md deleted file mode 100644 index 16c590e..0000000 --- a/specs/001-etf-paper-trading-agent/plan.md +++ /dev/null @@ -1,265 +0,0 @@ -# Implementation Plan: Alphoryn — Automated Ticker Paper Trading System - -**Branch**: `001-etf-paper-trading-agent` | **Date**: 2026-07-03 | **Spec**: [spec.md](spec.md) - -**Input**: Feature specification from `/specs/001-etf-paper-trading-agent/spec.md` - -## Summary - -Alphoryn V0.0.1 is a CLI application for automated ticker paper trading driven by LLM agents -(Google ADK + Gemini). The user configures a session via a JSON config file with optional -CLI argument overrides. The system autonomously executes a candle-by-candle -investigate-decide-execute loop for a user-supplied list of tickers (min 2). A local SQLite database serves -as the memory bank; API credentials are managed via Google Secret Manager. Paper trading -and market data are provided by Alpaca (via `alpaca-py` SDK for deterministic components -and the Alpaca MCP server as tools for LLM agents). Every LLM agent decision emits a -structured event log (decisions, tool calls, orders, monitor triggers) to Cloud Logging. - ---- - -## Technical Context - -**Language/Version**: Python 3.13+ - -**Primary Dependencies**: -- `google-adk` — Google ADK agent framework (main agent, feedback agent; Gemini models) -- `alpaca-py` — Alpaca SDK for deterministic components (market data snapshots, order execution, position monitoring, stop-loss polling) -- `alpaca-mcp-server` — Alpaca MCP server configured as tool provider for LLM agents (order management, market data, account info, market calendar) -- `typer` — CLI framework (argument parsing + JSON config file override) -- `pydantic` + `pydantic-settings` — config validation and layered loading -- `sqlalchemy` — SQLite ORM for memory bank (schema, queries, migrations) -- `google-cloud-secret-manager` — API key retrieval at runtime -- `jinja2` — HTML report generation from unified session.html.j2 template -- `google-cloud-logging` — structured event log upload (all component activity → GCP Cloud Logging) -- `ruff` — linting (zero violations; CI gate) -- `pytest` + `pytest-cov` — testing (100% coverage; CI gate) - -**Storage**: SQLite via SQLAlchemy (local file, memory bank + session/position state); -local filesystem (HTML reports, JSON config, Jinja2 templates) - -**Market scope**: Alpaca covers US equities (NYSE, NASDAQ, AMEX). Tickers must be US-listed. -Market hours are sourced from Alpaca's market calendar API; no exchange config needed. - -**Testing**: pytest, 100% coverage enforced by CI. No `pragma: no cover`. Agent paths -(main agent, feedback agent) tested with recorded/stubbed Google ADK responses. - -**Target Platform**: Single-process, single-machine CLI. Linux/macOS/Windows, Python 3.13+. - -**Project Type**: CLI application - -**Performance Goals**: -- Candle-close to first investigation action: ≤60 seconds (data fetch + snapshot build) -- Investigation heartbeat: every 5 minutes (user-visible aliveness signal) -- Stop-loss monitor poll interval: ≤30 seconds (react within one 1-minute candle) -- Session startup (config load + memory bank read + position load): ≤2 minutes - -**Constraints**: -- Investigation agent calls `build_snapshot` ADK tool during pre-investigation; once it returns a frozen `SignalSnapshot`, no further market data tool calls occur during investigation (Principle V). `data_fetch` is internal to `market_data/client.py` — not exposed to the agent -- Execution agent (ADK BaseAgent, no LLM) and position monitor MUST be fully deterministic; determinism verified by zero model calls, not zero ADK calls -- Memory bank MUST be readable at startup — inaccessible/corrupt → hard abort with error -- 52-min investigation + 7-min execute time budgets enforced per session; overrun → Hold - -**Scale/Scope**: Single user, a list of tickers (min 2), one session active at a time, local execution. -Background stop-loss monitor runs as a separate thread alongside the session loop. - ---- - -## Constitution Check - -*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* - -| Principle | Status | Verification | -|---|---|---| -| I. Determinism in Execution | ✅ PASS | `execution/agent.py` is an ADK `BaseAgent` with no LLM model configured; `monitor/monitor.py` is pure Python. Both call only `alpaca-py` with fixed inputs. Unit tests mock Alpaca SDK responses and assert identical outputs for identical inputs. | -| II. Test Coverage | ✅ PASS | 100% pytest coverage enforced in CI. ADK agent paths tested with stubbed `google-adk` responses (see `research.md §Testing ADK Agents`). Ruff configured in `pyproject.toml`. | -| III. Session Budget Enforced | ✅ PASS | `scheduler/scheduler.py` enforces session budgets proportional to candle timeframe (87% investigate / 13% decide+execute; for 1H: 52 min / 7 min) via `asyncio.wait_for` with explicit Hold fallback. Heartbeat emitted every 5 min during investigation. | -| IV. Fail Loud, Hold Safe | ✅ PASS | All failure modes in spec FR-017 and design doc §Failure Handling table produce Hold + structured JSON log entry. No silent failures. | -| V. Snapshot Isolation | ✅ PASS | Main agent calls `build_snapshot` ADK tool during pre-investigation; `data_fetch` is internal and not agent-accessible. Once `build_snapshot` returns a frozen `SignalSnapshot`, the system prompt and integration tests prohibit any further market data tool calls during investigation (see `research.md §Snapshot Isolation`). | - -No violations — Complexity Tracking not required. - ---- - -## Project Structure - -### Documentation (this feature) - -```text -specs/001-etf-paper-trading-agent/ -├── plan.md # This file -├── research.md # Phase 0 decisions and rationale -├── data-model.md # Phase 1 entity schema -├── quickstart.md # Phase 1 setup guide -├── contracts/ -│ ├── cli.md # CLI command contract -│ └── config-schema.md # JSON config file schema -└── tasks.md # Phase 2 output (/speckit-tasks — not yet created) -``` - -### Source Code (repository root) - -```text -alphoryn/ -├── cli/ -│ └── main.py # Typer app; entry point; config file + CLI override resolution -├── config/ -│ ├── models.py # Pydantic AlphorynConfig model (all session parameters) -│ └── loader.py # Layered load: JSON file → CLI arg overrides → validated model -├── agents/ -│ ├── main_agent.py # Google ADK main agent (regime recognition + per-ticker decision) -│ ├── feedback_agent.py # Google ADK feedback agent (thesis vs outcome evaluation) -│ └── prompts.py # Prompt templates for both agents -├── execution/ -│ └── agent.py # ADK BaseAgent (no LLM); deterministic Buy/Sell/Hold via Alpaca alpaca-py tools -├── monitor/ -│ └── monitor.py # Deterministic position monitor (stop-loss, profit target, window expiry) -├── scheduler/ -│ └── scheduler.py # Candle boundary alignment; market hours check; session budget enforcement -├── market_data/ -│ └── client.py # alpaca-py wrapper; internal data_fetch + signal computation; exposes build_snapshot as the sole ADK tool; price polling for stop-loss monitor -├── memory/ -│ ├── schema.py # SQLAlchemy models: Run, Session, Position, FeedbackEvaluation, MemoryEntry -│ └── bank.py # Memory bank interface: read/write, startup load, corruption detection -├── reports/ -│ └── generator.py # Jinja2 HTML report builder (unified session.html.j2 template) -├── secrets/ -│ └── client.py # Google Secret Manager wrapper (Alpaca API key + secret key; injects as env vars for MCP server) -├── telemetry/ -│ ├── logger.py # System-wide structured event emitter: sends JSON events to Cloud Logging for all components (agents, execution, monitor, scheduler) -│ └── otel.py # OTel trace exporter setup (Cloud Trace via Google ADK GCP exporters); called once at CLI startup -├── strategies/ # Strategy signal rules (mean_reversion.md, momentum.md) -│ ├── mean_reversion.md -│ └── momentum.md -└── skills/ # Investigation skill files, one dir per skill with a SKILL.md: - # identify-regime, mean-reversion-entry, momentum-entry, - # size-position, read-memory - -tests/ -├── unit/ # Pure function tests; deterministic components -├── integration/ # Full session cycle with stubbed ADK + stubbed Alpaca -└── contract/ # CLI contract tests; config schema validation - -templates/ -└── reports/ # Jinja2 HTML templates (session.html.j2) - -config.json # Example configuration (non-secret values only) -pyproject.toml # Dependencies, Ruff config, pytest config -``` - -**Structure Decision**: Single-project layout. Each top-level sub-package maps directly to -one agent or deterministic component from design doc §Agents table. `agents/` holds both -LLM-assisted agents; `execution/` holds the ADK BaseAgent execution agent (no LLM); -`monitor/` holds the pure-Python deterministic position monitor; `memory/` holds the SQLite -bank; `market_data/` and `secrets/` hold external integrations; `telemetry/` holds the -structured event logging layer used by all components (agents, execution, monitor, scheduler). -This makes constitution Principle I (Determinism) trivially verifiable: any file under -`execution/` or `monitor/` must contain zero LLM model calls. - ---- - -## Implementation Cross-References - -For each source package, the authoritative design doc section(s) to consult during -implementation. All section references are to `alphoryn_V_0_0.1.md`. - -| Package / Module | Implements | Design Doc Reference | Spec Reference | -|---|---|---|---| -| `cli/main.py` | Entry point (`alphoryn run`); startup validation; session count display; `alphoryn status` (current run + open positions); `alphoryn history` (session table by run); skipped-session counter (FR-018) | §Core Concepts §Run Duration; §Step 1 §Wait for Candle Close | FR-001–004; FR-018; US1; SC-001; contracts/cli.md (all three commands) | -| `config/models.py` | AlphorynConfig schema, stop-loss %, all parameters | §Configuration table | FR-001; contracts/config-schema.md | -| `config/loader.py` | JSON file → CLI override resolution | §Configuration table | FR-001; contracts/cli.md | -| `scheduler/scheduler.py` | Candle boundary alignment; market hours; budget timers; triggers feedback agent for positions whose `evaluation_window_close_at` deadline has passed; owns skipped-session logic (FR-018) | §Core Concepts §Candle Close, §Session; §Step 1, §Step 2, §Step 3; §Session Workflow diagram | FR-002–005; FR-007; FR-018; SC-002; contracts/agents.md §Feedback Trigger | -| `market_data/client.py` | Internal `data_fetch` (not agent-accessible) + signal computation; exposes `build_snapshot` as sole ADK tool returning a frozen `SignalSnapshot`; 1-min bar polling for stop-loss monitor | §Data Access Pattern; §Step 5 §Resources; research.md §Paper Trading API; research.md §build_snapshot | FR-006; SC-005 | -| `memory/schema.py` | SQLAlchemy entity schema | §Memory Bank §Structure; §Write responsibilities table; data-model.md | FR-012; FR-019 | -| `memory/bank.py` | Startup position load (all `status=OPEN` positions across runs); corruption abort; per-session writes; carry-over position blocking check; write ordering on partial failure | §Memory Bank §Purpose; §Step 8 §Update Memory; §Failure Handling | FR-005; FR-012; FR-019; Clarification Q3; data-model.md §Key Invariants | -| `agents/main_agent.py` | Regime recognition per ticker; strategy selection; investigation; calls `build_snapshot` tool; outputs `SessionDecision` to execution agent; queries MemoryEntry for prior performance context | §Step 5 §Investigate §Agent tasks; §Step 6 §Decide; §Agents (Main agent row); strategies/mean_reversion.md; strategies/momentum.md; research.md §Snapshot Isolation | FR-008; FR-009; data-model.md §MemoryEntry; contracts/agents.md §Decision Handoff | -| `agents/feedback_agent.py` | Thesis vs outcome evaluation; retry policy; memory write; receives `FeedbackInput` from scheduler; extracts thesis from HTML report; uses Alpaca MCP `get_bars` for evaluation-time candle close; updates Position status to EVALUATED or EVALUATION_FAILED | §Feedback Loop (full section); §Agents (Feedback agent row); §Data Access Pattern (1H candle close row); research.md §Paper Trading API | FR-015; FR-016; FR-016a; Clarification Q1; data-model.md §FeedbackEvaluation; contracts/agents.md §Feedback Trigger | -| `agents/prompts.py` | System prompts for both agents; main agent prompt must include Snapshot Isolation enforcement clause and memory bank context format; feedback agent prompt must define thesis extraction format | §Step 5 §Resources; §Feedback Loop §Inputs; research.md §Snapshot Isolation | FR-008; FR-015; data-model.md §MemoryEntry; contracts/agents.md | -| `execution/agent.py` | ADK BaseAgent (no LLM model); Buy/Sell/Hold via Alpaca `alpaca-py` (market orders); sequential budget check via account API; determinism verified by zero model calls | §Step 7 §Execute; §Agents (Execution agent row); §Failure Handling (paper trading API, budget, market closed rows); research.md §Paper Trading API | FR-009; FR-010; Clarification Q2 | -| `monitor/monitor.py` | Stop-loss, profit target, window expiry via `alpaca-py` latest-bar polling and `close_position`; runs as background thread; writes position status to memory bank on close | §Agents (Deterministic workflow row); §Data Access Pattern (real-time price row); §Failure Handling (real-time price feed row); research.md §Real-Time Price Feed | FR-013; FR-014; SC-005; Clarification Q1; data-model.md §Position §Position States | -| `reports/generator.py` | Unified HTML session report (session.html.j2; all tickers in one report); stores report path in `Session.html_report_path`; path format: `reports/run-{id}/session-{seq}.html` | §Step 8 §HTML Report; §Open Items (HTML report template) | FR-011; Clarification Q4; data-model.md §Session | -| `secrets/client.py` | Google Secret Manager key retrieval at startup | research.md §API Key Management | FR-001; quickstart.md | -| `telemetry/logger.py` | System-wide structured event emitter: all agent decisions, tool calls, execution orders, monitor triggers, and scheduler events sent to Cloud Logging as typed JSON events | research.md §Telemetry | FR-007; FR-008; FR-015 | -| `telemetry/otel.py` | OTel trace exporter setup; wires Google ADK's GCP exporters for Cloud Trace; called once at CLI startup before agent init; fail-silent if exporter packages missing | research.md §Telemetry | FR-007 | - ---- - -## Build Order - -Modules must be built in dependency order. Blocked modules cannot be completed until their -open items are resolved (see §Open Items). Integration boundaries requiring the agent -handoff contracts are noted — see `contracts/agents.md` before implementing those modules. - -### Stage 1 — Foundation (no inter-module dependencies) - -1. `config/models.py` — Pydantic AlphorynConfig; no dependencies -2. `secrets/client.py` — GCP Secret Manager wrapper; no dependencies -3. `telemetry/logger.py` — Cloud Logging event emitter; no dependencies -4. `telemetry/otel.py` — OTel trace exporter setup; called once at CLI startup; no dependencies - -### Stage 2 — Storage and Config Loading - -5. `config/loader.py` — needs `config/models.py` -6. `memory/schema.py` — SQLAlchemy entity schema; needs `config/models.py` -7. `memory/bank.py` — needs `memory/schema.py` - -### Stage 3 — Data and Execution (can be built in parallel after Stage 2) - -8. `market_data/client.py` — `build_snapshot` ADK tool + price polling; needs `alpaca-py`. - Signal fields defined in `data-model.md §AssetSignals`; computation logic from `alpaca-py` - bars (RSI, EMA, SMA, Bollinger, MACD, volume ratio). -9. `execution/agent.py` — ADK BaseAgent; needs `memory/bank.py`, `telemetry/logger.py`. - Requires `contracts/agents.md §Decision Handoff` before building input interface. -10. `monitor/monitor.py` — background thread; needs `memory/bank.py`, `market_data/client.py` - (price polling only), `telemetry/logger.py`. Trailing stop mechanics defined in - `alphoryn/strategies/momentum.md §Trailing Stop`. - -### Stage 4 — Scheduling and Reporting (after Stage 3) - -11. `scheduler/scheduler.py` — needs `market_data/client.py`, `memory/bank.py`, - `telemetry/logger.py`. Feedback trigger logic requires `contracts/agents.md §Feedback - Trigger`. Candle alignment and budget enforcement are unblocked. -12. `reports/generator.py` — Jinja2 template rendering; templates in `templates/reports/`; - context contract in `contracts/report-context.md`. - -### Stage 5 — Agents (after Stage 4) - -13. `agents/prompts.py` — strategy definitions in `alphoryn/strategies/`; skills in - `alphoryn/skills/`; Snapshot Isolation clause from `research.md §Snapshot Isolation`. -14. `agents/main_agent.py` — skills and strategy files fully authored. Requires - `contracts/agents.md §Decision Handoff` for output interface. -15. `agents/feedback_agent.py` — templates authored; thesis extraction via - `section#investment-thesis` (see `contracts/report-context.md §Thesis extraction`). - Requires `contracts/agents.md §Feedback Trigger` for input interface. - -### Stage 6 — CLI Integration (after all above) - -16. `cli/main.py` — integrates all modules; `alphoryn status` and `alphoryn history` are - fully unblocked; `alphoryn run` requires all prior stages. - -### Threading model - -The session loop runs on the main asyncio event loop. `monitor/monitor.py` runs as a -`threading.Thread` started at run startup and stopped via a `threading.Event` when the -run ends. The monitor communicates position close events by writing directly to the memory -bank (SQLite); the scheduler reads position state from the memory bank at each session -start. No inter-thread queues or events are needed beyond the stop signal. - ---- - -## Open Items - -All previously TBD design artifacts have been authored. All stages in §Build Order are -now fully unblocked (pending user refinement of strategies and skills). - -| Artifact | Status | Path | -|---|---|---| -| Mean Reversion strategy | Authored | `alphoryn/strategies/mean_reversion.md` | -| Momentum strategy | Authored | `alphoryn/strategies/momentum.md` | -| Skill: identify-regime | Authored | `alphoryn/skills/identify-regime/SKILL.md` | -| Skill: mean-reversion-entry | Authored | `alphoryn/skills/mean-reversion-entry/SKILL.md` | -| Skill: momentum-entry | Authored | `alphoryn/skills/momentum-entry/SKILL.md` | -| Skill: size-position | Authored | `alphoryn/skills/size-position/SKILL.md` | -| Skill: read-memory | Authored | `alphoryn/skills/read-memory/SKILL.md` | -| HTML report template (unified) | Authored | `templates/reports/session.html.j2` | -| Report context contract | Authored | `contracts/report-context.md` | -| Feedback evaluation window | Resolved | Mean Reversion: +4 sessions; Momentum: +2 sessions | diff --git a/specs/001-etf-paper-trading-agent/quickstart.md b/specs/001-etf-paper-trading-agent/quickstart.md deleted file mode 100644 index b07064b..0000000 --- a/specs/001-etf-paper-trading-agent/quickstart.md +++ /dev/null @@ -1,112 +0,0 @@ -# Quickstart: Alphoryn v0.0.1 - -**Phase 1 output** | **Date**: 2026-07-03 | **Plan**: [plan.md](plan.md) - ---- - -## Prerequisites - -- Python 3.13+ -- Free Alpaca paper trading account at [alpaca.markets](https://alpaca.markets) (no deposit required) -- Google Cloud project with Secret Manager API enabled -- `gcloud` CLI authenticated (`gcloud auth application-default login`) - ---- - -## 1. Install - -```bash -git clone alphoryn -cd alphoryn -pip install -e ".[dev]" -``` - -Verify: -```bash -alphoryn --help -ruff check . -pytest --cov=alphoryn -``` - ---- - -## 2. Get Alpaca paper trading API keys - -1. Sign up at [alpaca.markets](https://alpaca.markets) → create a paper trading account -2. Dashboard → Paper Trading → API Keys → Generate New Key -3. Copy the **API Key ID** and **Secret Key** (shown once) - ---- - -## 3. Store Alpaca credentials in Google Secret Manager - -```bash -echo -n "YOUR_ALPACA_API_KEY" | gcloud secrets create alphoryn-alpaca-api-key --data-file=- -echo -n "YOUR_ALPACA_SECRET_KEY" | gcloud secrets create alphoryn-alpaca-secret-key --data-file=- -``` - ---- - -## 4. Create your config file - -```bash -cp config.json.example config.json -``` - -Minimum required fields — at least 2 US-listed tickers: - -```json -{ - "tickers": ["SPY", "QQQ"] -} -``` - -See `contracts/config-schema.md` for all fields and defaults. -Note: tickers must be US-listed (NYSE/NASDAQ/AMEX) — Alpaca covers US equities only. - ---- - -## 5. Run - -```bash -# Use config.json in current directory -alphoryn run - -# Override individual fields -alphoryn run --tickers SPY,QQQ --duration 8H --stop-loss 0.02 - -# Use a different config file -alphoryn run --config /path/to/my-config.json -``` - -The system will: -1. Fetch Alpaca credentials from Google Secret Manager and connect to Alpaca paper account -2. Validate config and load open positions from the local memory bank -3. Display the planned session count and time until next candle close (NYSE hours) -4. Wait for the next candle boundary, then begin the investigate-decide-execute loop - ---- - -## 6. Monitor status - -```bash -# While a run is active (from another terminal) -alphoryn status - -# View session history -alphoryn history -alphoryn history --run 1 -``` - ---- - -## Troubleshooting - -| Error | Cause | Fix | -|---|---|---| -| Exit code 2: memory bank inaccessible | `~/.alphoryn/memory.db` missing or corrupt | Delete and restart (positions lost) or restore from backup | -| Exit code 3: Secret Manager unreachable | GCP credentials not set | Run `gcloud auth application-default login` | -| `alpaca.common.exceptions.APIError: 403` | Invalid or expired Alpaca API key | Regenerate key at alpaca.markets and update GCP secrets | -| `alpaca.common.exceptions.APIError: 422` | Invalid ticker symbol | Confirm ticker is US-listed and correct | -| `tickers must contain at least 2 symbols` validation error | Fewer than 2 tickers in config | Provide at least 2 tickers | -| Fractional session warning at startup | `run_duration` not evenly divisible by `candle_timeframe` | Adjust either field; system rounds down and proceeds | diff --git a/specs/001-etf-paper-trading-agent/research.md b/specs/001-etf-paper-trading-agent/research.md deleted file mode 100644 index 4eba89a..0000000 --- a/specs/001-etf-paper-trading-agent/research.md +++ /dev/null @@ -1,283 +0,0 @@ -# Research: Alphoryn — Automated Ticker Paper Trading System - -**Phase 0 output** | **Date**: 2026-07-03 (updated 2026-07-03) | **Plan**: [plan.md](plan.md) - ---- - -## Paper Trading API + Market Data - -**Decision**: Alpaca via `alpaca-py` SDK (deterministic components) + Alpaca MCP server (LLM agents) - -**Rationale**: The project owner specified Alpaca for paper trading and the Alpaca MCP server -as a tool provider for agents. Alpaca provides a unified API covering paper trading order -execution, position management, account info, historical bars, and real-time quotes in a -single integration. This replaces both ib-insync and yfinance from the initial plan. - -**Market scope note**: Alpaca covers **US equities markets** (NYSE, NASDAQ, AMEX). The -original design doc referenced European exchanges (XETRA, Euronext, LSE) and EUR currency. -With Alpaca as the execution and data provider, supported tickers are US-listed (e.g., SPY, -QQQ, EEM). The `exchange` config field is now optional and informational only (no longer -required); market hours come from Alpaca's market calendar API regardless of its value. -Currency is USD for Alpaca paper accounts. - -**Alternatives considered**: -- IBKR / ib-insync: Covers European exchanges but requires TWS running locally, more complex - setup. Eliminated per project owner decision. -- yfinance: Free market data but no trading. No longer needed — Alpaca provides both. - -**Integration pattern** (two layers): - -*Deterministic components* (`alpaca-py` Python SDK, no MCP): -- `market_data/client.py` — fetches 1H and 1-min OHLCV bars at candle close to build - the frozen `SignalSnapshot`; polls latest 1-min bar for stop-loss monitor -- `execution/agent.py` — places market orders; checks account/budget; cancels on failure -- `monitor/monitor.py` — polls position P&L and price; closes positions deterministically - -*LLM agents* (Alpaca MCP server configured as tool provider in Google ADK): -- `agents/main_agent.py` — has `build_snapshot` as an ADK tool; calls it during - pre-investigation to receive a frozen `SignalSnapshot`. Raw data fetching is internal to - `market_data/client.py` — the agent never sees OHLCV bars. Once `build_snapshot` returns, - market data tool calls are prohibited for the rest of investigation (system prompt + - integration test enforcement; see §Snapshot Isolation). -- `agents/feedback_agent.py` — uses MCP `get_bars` to fetch the 1H candle close at - evaluation time (design doc §Data Access Pattern: "1H candle close at evaluation time") - -**Alpaca MCP server tool categories** (from https://github.com/alpacahq/alpaca-mcp-server): - -Note: deterministic components (`execution/agent.py`, `monitor/monitor.py`, `scheduler/scheduler.py`, -`market_data/client.py`) use `alpaca-py` SDK directly — NOT the MCP server. The MCP server -is used only by LLM agents (main_agent, feedback_agent) via ADK tool integration. - -| Category | Tools | Used via MCP by | Used via SDK by | -|---|---|---|---| -| Account & Portfolio | account info, portfolio history, activity | `agents/main_agent.py` (account context) | `execution/agent.py` (budget check) | -| Order Management | place order (market/limit/trailing-stop), cancel | — | `execution/agent.py` | -| Position Management | get positions, close position | — | `monitor/monitor.py`, `execution/agent.py` | -| Market Data | historical bars, real-time quotes, snapshots | `agents/feedback_agent.py` (evaluation-time bars) | `market_data/client.py` (signal computation + price polling) | -| Market Calendar | market clock, trading hours | — | `scheduler/scheduler.py` | -| Asset Information | asset lookup, market status | — | `config/loader.py` (ticker validation) | - -**Order type**: Market order for all Buy/Sell executions at v0.0.1 (simplest; adequate for -paper trading). Trailing-stop order type available for Momentum strategy profit target in v0.1.0. - -**Execution failure mode**: `alpaca-py` raises `APIError` or connection timeout → log intended -action, hold position, retry next session (design doc §Failure Handling; spec FR-017). - -**Alpaca paper trading setup**: Free account at alpaca.markets; no TWS installation required. -Paper trading is the default mode (`ALPACA_PAPER_TRADE=true` in MCP config). - ---- - -## Real-Time Price Feed (Stop-Loss Monitor) - -**Decision**: Alpaca `alpaca-py` SDK — latest bar polling (≤30-second interval) - -**Rationale**: `alpaca-py` provides `StockLatestBarRequest` for real-time price polling. -Same integration already required for execution — no additional dependency. Adequate for -paper trading stop-loss resolution (spec SC-005: trigger within one 1-minute candle). - -**Alternative considered**: Alpaca WebSocket streaming — more precise but adds event loop -complexity. Deferred to v0.1.0. - ---- - -## CLI Framework - -**Decision**: `typer` - -**Rationale**: Integrates natively with Pydantic models, generates rich help text -automatically, supports JSON config file + CLI override pattern cleanly. - -**Alternatives considered**: `click` (workable, more boilerplate), `argparse` (eliminated). - ---- - -## Configuration Loading - -**Decision**: Pydantic `BaseSettings` with JSON file source + Typer CLI overrides - -**Pattern**: -``` -1. Load config.json (or --config path) → AlphorynConfig instance -2. Apply non-None CLI option values as overrides -3. Validate merged config → field-level error if invalid -4. Config object passed to all components; no global state -``` - ---- - -## Memory Bank Storage - -**Decision**: SQLite via SQLAlchemy (`~/.alphoryn/memory.db` by default) - -**Rationale**: Local database requirement. Zero-server, file-based, ACID compliant. -SQLAlchemy provides ORM and migration path. Single-process — no concurrency concerns. - -**Alternatives considered**: TinyDB (weak query support), JSON files (no ACID). Both eliminated. - -**Corruption / inaccessibility**: SQLAlchemy connection failure at startup → `MemoryBankError` -→ CLI prints error + exits with code 2 (spec FR-019; Clarification Q3). - ---- - -## HTML Report Generation - -**Decision**: Jinja2 with a single unified session template in `templates/reports/` - -**Template file**: `session.html.j2` - renders every ticker's decision in one report. -(An earlier per-strategy-template design was dropped; those templates are deleted.) - ---- - -## API Key Management - -**Decision**: Google Secret Manager (specified by project owner) - -**Secrets required**: -| Secret name (GCP) | Contents | -|---|---| -| `alphoryn-alpaca-api-key` | Alpaca paper trading API key | -| `alphoryn-alpaca-secret-key` | Alpaca paper trading secret key | - -The Alpaca MCP server reads these from environment variables (`ALPACA_API_KEY`, -`ALPACA_SECRET_KEY`). `secrets/client.py` fetches them from Secret Manager at startup -and injects them as env vars before the MCP server connection is established. - -GCP auth: Application Default Credentials (`gcloud auth application-default login`). - ---- - -## Testing ADK Agents - -**Decision**: Stub Google ADK responses using recorded fixtures; stub Alpaca MCP tool responses separately - -**Pattern**: -- Record real ADK + MCP responses for known scenarios as JSON fixtures in `tests/fixtures/`. -- `StubGeminiClient` returns fixture for given prompt hash. -- `StubMCPClient` returns fixture for given tool name + arguments hash. -- Tests assert agent tool-calling logic and output parsing without hitting Gemini or Alpaca APIs. - -**Deterministic component tests**: `execution/agent.py` and `monitor/monitor.py` tested with -`StubAlpacaClient` that returns fixed API responses for known order/position inputs. - ---- - -## build_snapshot (Tool Architecture) - -`build_snapshot` is the single ADK tool registered on the main agent for pre-investigation -data access. The agent calls it once; `market_data/client.py` handles all raw data fetching -internally via `alpaca-py` — the agent never sees OHLCV bars. - -`build_snapshot` returns a frozen `SignalSnapshot` containing a `signals: dict[str, AssetSignals]` -keyed by ticker — one entry per configured ticker, not fixed to two. The agent works -entirely from these signals during investigation. - -`data_fetch` is an internal function within `market_data/client.py`, not an ADK tool. -The agent has no direct access to it. - ---- - -## Snapshot Isolation with MCP Tools (Architecture Note) - -Constitution Principle V requires investigation to use only a frozen snapshot. Because the -main agent has Alpaca MCP tools available, the system prompt MUST explicitly prohibit calling -market data tools after `build_snapshot` has returned. The enforcement strategy: - -1. The system prompt includes: "You have called build_snapshot and received a SignalSnapshot. - Do not call any further market data tools during investigation. Use only the signal - fields in the snapshot." -2. Skills (md files) reference snapshot fields by name, not MCP tool calls. -3. Integration tests assert that the main agent makes zero MCP market data tool calls during - investigation (stubbed MCP client tracks call counts per phase). - ---- - -## Execution Agent Architecture - -**Decision**: ADK `BaseAgent` subclass with no LLM model configured - -**Rationale**: The execution agent is purely deterministic — given a Buy/Sell/Hold decision -from the main agent, it performs a fixed sequence of Alpaca API calls (budget check, -order placement, confirmation). No reasoning or language model is required. ADK `BaseAgent` -provides the same ADK infrastructure (event bus, tool integration, lifecycle hooks) without -attaching a model. - -**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). - ---- - -## Telemetry - -**Decision**: System-wide structured event log emitted to Cloud Logging. Every meaningful -action across all components — LLM agents, deterministic execution agent, and stop-loss -monitor — emits a structured JSON event. - -**Rationale**: Full observability across the entire pipeline, not just LLM decisions. -Cloud Logging provides queryable centralized storage via GCP Logs Explorer. Events include -`latency_ms` and `session_id` on every record, enabling timing analysis and session -correlation without a separate tracing backend. - -**Event log schema** — common fields on every event emitted by `telemetry/logger.py`: - -| Field | Type | Description | -|---|---|---| -| `event_type` | `str` | See event types table below | -| `session_id` | `str \| None` | Parent session (`run-N/session-X`); null for run-level events | -| `component` | `str` | Emitting component (e.g., `"main_agent"`, `"monitor"`, `"scheduler"`, `"feedback_agent"`) | -| `etf` | `str \| None` | Ticker symbol where applicable. Field name intentionally kept as `etf` (not renamed to `ticker`) for log schema stability — renaming would break existing Cloud Logging queries. See `telemetry/logger.py::emit`. | -| `timestamp` | `datetime` | UTC event time | -| `latency_ms` | `int \| None` | Duration where applicable | -| `payload` | `dict` | Event-specific fields (see below) | - -**Event types and their payload fields** (as actually implemented in `telemetry/logger.py::EVENT_TYPES` and each emit call site): - -| Event type | Component | Key payload fields | -|---|---|---| -| `AGENT_DECISION` | `main_agent` | `decisions` (dict: ticker → action) | -| `AGENT_DECISION` | `feedback_agent` | `position_id`, `ticker`, `outcome_judgment`, `attempt` | -| `TOOL_CALL` | `main_agent` | `tool`, `args` | -| `TOOL_CALL` | `feedback_agent` | `attempt` | -| `SIGNAL_SNAPSHOT_BUILT` | `main_agent` | `snapshot` (stringified tool response) | -| `STOP_LOSS_TRIGGERED` / `PROFIT_TARGET_TRIGGERED` / `WINDOW_EXPIRY_TRIGGERED` | `monitor` | `ticker`, `exit_price`, `exit_reason` | -| `POSITION_CLOSED` | `monitor` | `ticker`, `status` | -| `SESSION_START` | `scheduler` | (empty payload; `session_id` carries the identity) | -| `SESSION_END` | `scheduler` | (empty payload; `latency_ms` carries duration) | -| `MARKET_CLOSED` | `scheduler` | `session_ordinal` | -| `BUDGET_TIMEOUT` | `scheduler` | `phase` (`"investigation"` or `"execute"`), `budget_secs` | -| `TICKER_BLOCKED` | `scheduler` | `reason` (`"FEEDBACK_UNEVALUATED"`); `etf` carries the ticker | -| `MONITOR_STARTED` / `MONITOR_STOPPED` | `scheduler` | (empty payload) | -| `EVALUATION_FAILED` | `feedback_agent` | `position_id`, `ticker`, `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. - -**Dependencies**: -- `google-cloud-logging` — upload structured JSON events to Cloud Logging - ---- - -## Open Items - -All design artifacts previously marked TBD have been authored. Subject to user refinement. - -| Item | Path | -|---|---| -| Mean Reversion strategy | `alphoryn/strategies/mean_reversion.md` | -| Momentum strategy | `alphoryn/strategies/momentum.md` | -| Skills (5 files) | `alphoryn/skills/` | -| HTML report templates | `templates/reports/` | -| Report context contract | `contracts/report-context.md` | -| Feedback window timing | Mean Reversion: +4 sessions; Momentum: +2 sessions | diff --git a/specs/001-etf-paper-trading-agent/spec.md b/specs/001-etf-paper-trading-agent/spec.md deleted file mode 100644 index bae8eae..0000000 --- a/specs/001-etf-paper-trading-agent/spec.md +++ /dev/null @@ -1,204 +0,0 @@ -# Feature Specification: Alphoryn — Automated Ticker Paper Trading System - -**Feature Branch**: `001-etf-paper-trading-agent` - -**Created**: 2026-07-03 - -**Status**: Implemented - -**Input**: User description: "Alphoryn V0.0.1 — an agentic system for automated ETF paper trading using LLM-assisted discretionary decision-making." - ---- - -## Clarifications - -### Session 2026-07-03 - -- Q: How are stop-loss and profit target thresholds defined? → A: Stop-loss is a hard config percentage (risk control, e.g., −2% from entry). Profit target is agent-determined per trade at entry: Mean Reversion targets the mean price level; Momentum uses a trailing stop. Neither is a fixed config value for profit. -- Q: When multiple tickers trigger Buy in the same session, how is the session money budget allocated? → A: Ticker orders execute sequentially; each order is validated against the full remaining budget at the time of execution (first-come-first-served). No pre-split or conviction-based allocation. -- Q: What happens if the memory bank is inaccessible or corrupted at run startup? → A: Abort with a clear error message; the run must not start. The user must resolve the memory bank before proceeding. -- Q: How are sessions uniquely identified? → A: Sequential run number combined with a zero-padded sequential session number within that run (e.g., `run-3/session-0001`). Run number increments across runs; session number increments within each run. -- Q: What does the user see during the investigation window? → A: Periodic heartbeat lines at a fixed interval (e.g., "investigating… 12 min elapsed") — enough to confirm the system is alive without cluttering the output. - -### Session 2026-07-07 - -- Q: How many tickers does the system support? → A: Minimum two tickers required; the system supports any number of US-listed tickers configured in `tickers: list[str]`. The original two-ETF constraint is generalised — tickers are evaluated independently in every session. -- Q: How are market hours and exchange determined? → A: Market hours are sourced from Alpaca's market calendar API. No exchange configuration is required; the system is scoped to US equities (NYSE, NASDAQ, AMEX) only. -- Q: What configuration parameters have been added since the original spec? → A: `extended_hours: bool` (allow pre/post-market execution; testing affordance), `memory_db_path: str` (path to local SQLite memory bank, default `~/.alphoryn/memory.db`). `exchange` has been removed. -- Q: How are the four agents architecturally separated? → A: Two are LLM-assisted (Investigation Agent, Feedback Agent); two are fully deterministic (Execution Agent, Position Monitor). Deterministic agents contain no LLM calls — this is verified by tests asserting zero model calls. Agents communicate via structured typed records, not natural language. The HTML report is the only cross-agent artifact. - ---- - -## User Scenarios & Testing *(mandatory)* - -### User Story 1 — Configure and Launch a Trading Session (Priority: P1) - -A user provides configuration (a list of tickers, candle timeframe, run duration, an optional session money budget, and an optional extended-hours flag) and starts the system. The system validates the configuration, calculates the total number of sessions, warns if the session count is fractional, and waits in an idle countdown until the next candle boundary before beginning. - -**Why this priority**: This is the entry point for the entire system. Nothing can function without a valid, started session. - -**Independent Test**: Can be fully tested by supplying a valid configuration and confirming the system reaches the "waiting for candle close" idle state with correct session count displayed, without executing any trades. - -**Acceptance Scenarios**: - -1. **Given** a valid config with a 24H run duration and 1H candle timeframe, **When** the user starts the system, **Then** the system displays "24 sessions planned" and begins counting down to the next candle boundary. -2. **Given** a run duration that does not divide cleanly into the candle timeframe, **When** the user starts the system, **Then** the system warns about fractional sessions, rounds down, and suggests a config adjustment. -3. **Given** an invalid or missing configuration field, **When** the user starts the system, **Then** the system surfaces a clear error and does not proceed. - ---- - -### User Story 2 — Autonomous Per-Session Decision Cycle (Priority: P1) - -At each candle close, the system wakes, checks whether the run is still active and the market is open, investigates market data for all configured tickers, performs independent regime recognition per ticker, selects a strategy per ticker (each ticker may end up on a different strategy in the same session), decides Buy/Sell/Hold per ticker, executes the decisions via the paper trading interface, and produces a unified session HTML report covering all tickers. This cycle repeats until the run completes. - -**Why this priority**: This is the core value of the system — autonomous decision-making over a configured run period. - -**Independent Test**: Can be tested by running a single-session scenario: the system receives one candle close, completes the full investigate-decide-execute cycle, generates a unified HTML report, and writes a memory bank entry — without a second candle close being required. - -**Acceptance Scenarios**: - -1. **Given** a candle has closed and the market is open, **When** the session begins, **Then** the system takes a frozen data snapshot for all tickers, investigates within the budget window, produces a Buy/Sell/Hold decision per ticker, and executes it. -2. **Given** investigation does not complete within the session investigation budget, **When** the budget expires, **Then** the system forces a Hold on all tickers for that session and logs a timeout warning. -3. **Given** the session money budget is set and an order would exceed the remaining budget, **When** execution is attempted, **Then** the order is skipped, the skip is logged, and the position is held. -4. **Given** the market is closed at the time of a candle close, **When** the session check runs, **Then** the system logs the closure, displays a countdown to market open, and waits without consuming session budget. - ---- - -### User Story 3 — Position Lifecycle and Risk Management (Priority: P2) - -After a trade is placed, the system monitors the open position continuously and exits it automatically when a profit target, stop-loss, or evaluation window expiry is reached — without LLM involvement. The system also prevents the main decision agent from opening a new position on a ticker until the feedback agent has evaluated and closed the prior trade on that ticker. - -**Why this priority**: Unclosed positions and unguarded risk are the primary financial failure modes. This must be in place before any live trading sessions run. - -**Independent Test**: Can be tested by opening a simulated position and verifying that a price reaching the stop-loss threshold triggers an automatic exit, while the main agent is correctly blocked from opening a new position on the same ticker. - -**Acceptance Scenarios**: - -1. **Given** an open position where the price hits the configured stop-loss, **When** the monitoring loop detects this, **Then** the position is closed automatically and logged without any LLM call. -2. **Given** an open position on ticker-A whose feedback has not been evaluated, **When** a session begins, **Then** ticker-A is excluded from the investigation step entirely (no LLM investigation call is made for it) and its session outcome is recorded as Hold. -3. **Given** an open position on ticker-A and no open position on ticker-B, **When** a session runs, **Then** the main agent can freely decide on ticker-B while being forced to Hold on ticker-A. -4. **Given** a position whose evaluation window has expired without hitting profit target or stop-loss, **When** the expiry is detected, **Then** the position is closed and the fact is logged. *** Added by me, Let's clarify wht evaluation window is? *** - ---- - -### User Story 4 — Feedback Evaluation and Memory Learning (Priority: P2) - -At a strategy-defined point after the entry session (1–2 sessions for Momentum, 3–6 for Mean Reversion), a feedback agent evaluates whether the original trading thesis was correct by comparing what was decided against what actually happened. The evaluation result is written to the memory bank, closing the learning loop for that trade. - -**Why this priority**: Without feedback evaluation, the system cannot improve regime recognition over time and cannot unblock the main agent from trading the affected ticker. - -**Independent Test**: Can be tested by simulating a completed trade entry and fast-forwarding to the evaluation window, then confirming the feedback agent produces a structured judgment record in the memory bank and marks the trade as evaluated. - -**Acceptance Scenarios**: - -1. **Given** a Momentum trade entry session, **When** 1–2 candle sessions have elapsed, **Then** the feedback agent reads the entry HTML report, calls the same market data tool the Investigation Agent uses to fetch the candle close price at evaluation time, and writes a judgment to the memory bank. -2. **Given** the feedback agent has written its evaluation, **When** the main agent next considers that ticker, **Then** it is free to Buy, Sell, or Hold on that ticker, and its investigation input includes the ticker's recent feedback judgments from the memory bank so the decision can account for whether the prior thesis was correct. -3. **Given** the feedback agent encounters an error at evaluation time, **When** the attempt fails, **Then** the feedback agent retries immediately up to 3 times. If all 3 attempts fail, the evaluation is marked failed, the ticker is unblocked, and a warning is logged — the position is considered closed from the main agent's perspective. - ---- - -### Edge Cases - -- What happens if multiple tickers all have open, feedback-unevaluated positions simultaneously? (System should Hold on all affected tickers until at least one feedback evaluation completes; unaffected tickers remain free.) -- What happens if the real-time price feed becomes unavailable mid-session while a stop-loss is active? (System suspends trading, alerts the user, and resumes when feed is restored.) -- What happens if a candle close occurs while the system is still in the execution phase of the previous session? (The new session is skipped and does not count against the run total.) -- What happens if the paper trading API is unavailable at execution time? (System logs the intended action, holds the position, and retries at the next session.) -- What happens if the run completes while a position is still open? (Position remains open under continuous stop-loss monitoring; run completion does not force-close positions.) -- What happens if a new run starts and carry-over positions from a previous run exist in the memory bank? (System loads them, applies position-blocking immediately for any with unevaluated feedback, and resumes stop-loss monitoring at market open — the new run is never a clean slate.) -- What happens if the memory bank is inaccessible or corrupted at run startup? (System aborts with a clear error message; the run must not start in a degraded state. User must restore the memory bank before retrying.) - ---- - -## Requirements *(mandatory)* - -### Functional Requirements - -- **FR-001**: System MUST accept a configuration specifying a list of tickers (minimum two, US-listed), candle timeframe, run duration, an optional per-session money budget, a stop-loss percentage applied as a hard risk control at trade entry, an extended-hours flag (`extended_hours`), and a memory bank path (`memory_db_path`). No exchange configuration is required; market hours are sourced from the trading platform's market calendar. -- **FR-002**: System MUST calculate total session count as `run_duration / candle_timeframe` (rounded down) at startup and display it to the user. -- **FR-003**: System MUST warn the user if the session count is fractional and suggest a configuration adjustment. -- **FR-004**: System MUST align to the next candle boundary (not system start time) before triggering the first session, and display the wait time. -- **FR-005**: System MUST check, at each session start, whether the run is complete, the market is open, and whether any ticker has a feedback-blocked position (a closed position with no `FeedbackEvaluation` yet, per FR-014). Feedback-blocked tickers MUST be excluded from the investigation step entirely for that session — no Investigation Agent call is made for them — and their session outcome is recorded as Hold. At the start of a new run, the system MUST additionally load all open positions from the memory bank and immediately apply position-blocking rules for any unevaluated carry-over positions (positions still `OPEN`, or closed but unevaluated, at the time a new run starts, per FR-019). -- **FR-006**: System MUST take a frozen market data snapshot at each candle close and reason exclusively over that snapshot during investigation; no live data may be fetched during investigation. -- **FR-007**: System MUST enforce a session investigation budget of ≤87% of the candle timeframe and a decide+execute budget of ≤13% of the candle timeframe (for a 1H candle: 52 min / 7 min). Overruns MUST force a Hold decision on all tickers and emit a warning log. During investigation, the system MUST emit periodic heartbeat lines at a fixed interval indicating elapsed time (e.g., "investigating… 12 min elapsed"), so the user can confirm the system is active. -- **FR-008**: System MUST perform regime recognition independently for each ticker within a session and select a strategy (Mean Reversion or Momentum) per ticker. One ticker may be assigned Mean Reversion while another is assigned Momentum in the same session. The investigation step covers all tickers but produces independent strategy selection and decision outputs for each. -- **FR-008a**: Before investigating a ticker, the system MUST supply the Investigation Agent with that ticker's recent feedback judgments and strategy performance history from the memory bank, so the decision (including whether to continue, reverse, or exit an unblocked position) can account for prior evaluation outcomes. -- **FR-009**: System MUST produce one of three actions per ticker per session: Buy, Sell, or Hold. -- **FR-010**: System MUST enforce the session money budget at execution time: each ticker order is checked against the full remaining budget at the moment of execution, in sequence. If an order would exceed the remaining budget, that order is skipped and logged; other tickers' orders are unaffected. No pre-session budget split between tickers is performed. This execution-time check is a hard, deterministic backstop regardless of what the Investigation Agent reasoned about. -- **FR-010a**: The Investigation Agent MUST be given the session money budget as part of its input, and when it produces Buy decisions with lot sizes for more than one ticker in the same session, it MUST reason about them jointly against that shared budget (e.g., not size every ticker's order as if it alone had the full budget), since execution consumes the budget sequentially, ticker by ticker. This is advisory sizing guidance only — FR-010's execution-time check remains authoritative and may still skip an order the agent sized optimistically. -- **FR-011**: System MUST generate a unified HTML report after each session covering all tickers, recording per-ticker strategy, action, reasoning, execution result, and any warnings. The report is stored under a composite session identifier (e.g., `run-3/session-0001`). -- **FR-012**: System MUST write a memory bank entry after each session recording strategy selected, regime context, and decision per ticker. -- **FR-013**: System MUST continuously monitor open positions using real-time price data and trigger deterministic exits (no LLM involvement) on three conditions: (1) price breaches the configured stop-loss percentage from entry price; (2) price reaches the agent-set exit target recorded at trade entry (mean-reversion price level or trailing stop for Momentum); (3) evaluation window expires without either prior exit triggering. -- **FR-014**: System MUST block the main agent from opening a new position on a ticker while that ticker has an open, feedback-unevaluated position. Each ticker is independent. -- **FR-015**: System MUST trigger the feedback agent at the strategy-defined evaluation window (1–2 sessions post-entry for Momentum; 3–6 sessions for Mean Reversion). -- **FR-016**: Feedback agent MUST write a structured evaluation (thesis vs. outcome judgment) to the memory bank and mark the position as evaluated. It MUST use the same market data tool as the Investigation Agent to fetch the price at the evaluation timestamp, querying that specific past candle close rather than the latest one. -- **FR-016a**: If a feedback evaluation attempt fails, the feedback agent MUST retry immediately up to 3 times. After 3 consecutive failures, the evaluation MUST be marked as failed, the ticker MUST be unblocked for new trades, and a warning MUST be logged. The position is treated as closed from the main agent's perspective. -- **FR-017**: System MUST handle all failure conditions (API unavailable, market closed, budget exceeded, skill unavailable) with a Hold action and a structured log entry; no failure condition may leave a position in an ambiguous state. -- **FR-018**: Timed-out sessions and data-unavailability skips MUST NOT count against the derived session total. -- **FR-019**: When a new run starts, the system MUST load all open positions from the memory bank, apply position-blocking rules for any with unevaluated feedback, and resume stop-loss monitoring for all carry-over positions at market open. Every run begins position-aware; no run starts as a clean slate. If the memory bank is inaccessible or corrupted at startup, the system MUST abort with a clear error message — the run must not proceed in a degraded state. - -### Agent Architecture - -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, 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. - -**Position Monitor** (deterministic, continuous) -Runs concurrently with the session loop as a background process. Continuously polls real-time price data and closes positions when any of three exit conditions is met: stop-loss breach, profit-target reached, or evaluation window expired. Makes no LLM calls. Thread-safe with respect to the session loop. - -**Feedback Agent** (LLM-assisted, reasoning) -Triggered once per closed position at the strategy-defined evaluation window. Reads the original session HTML report to extract the entry reasoning, then calls the same market data tool the Investigation Agent uses (`market_data/client.py`) to fetch the actual price outcome at the evaluation timestamp, and writes a structured judgment (Correct / Incorrect / Neutral) to the memory bank. Unlike the Investigation Agent's snapshot-isolated call, the Feedback Agent's tool call targets a specific past candle close rather than the latest one. Unblocks the ticker for future trades after evaluation (or after exhausting its retry policy). Invoked by the session loop before investigation begins. - -**Interaction flow:** - -``` -[candle close] - Investigation Agent → structured decision per ticker - Execution Agent → market orders + open Position records - Position Monitor → continuous price polling → close Position on exit trigger -[evaluation window] - Feedback Agent → reads HTML report → writes FeedbackEvaluation → unblocks ticker -``` - -Each agent communicates via structured records written to the memory bank or passed as typed data — no agent reads or interprets another agent's natural language output directly. The HTML report is the only cross-agent artifact, and only the feedback agent reads it. - -### Key Entities - -- **Configuration**: List of tickers (min 2, US-listed), candle timeframe, run duration, optional money budget, extended-hours flag, stop-loss percentage, memory bank path. The single source of truth for all session parameters. -- **Session**: One atomic decision unit triggered by a candle close. Identified by a composite key of sequential run number and a zero-padded sequential session number (e.g., `run-3/session-0001`). Contains a frozen data snapshot and per-ticker investigation outputs — each ticker produces an independent strategy selection, action, and execution result. The unified session HTML report captures all tickers and is stored under the session's composite ID. -- **Position**: An open paper trade on one ticker. Tracks entry price, strategy, status (open/closed/evaluated), a hard stop-loss level (derived from the configured stop-loss percentage applied at entry), and a strategy-determined exit target (price level for Mean Reversion; trailing stop for Momentum — both set by the investigation agent at trade entry). -- **Memory Bank**: A structured local store accumulating per-ticker strategy performance, regime context summaries, and feedback evaluations across all sessions and runs. Persists across runs. -- **HTML Report**: A unified session record generated after each session covering all configured tickers. The primary artifact shared between the session loop and the feedback agent. -- **Feedback Evaluation**: A structured record written by the feedback agent after comparing the entry thesis to the actual price outcome for a single ticker position. - ---- - -## Success Criteria *(mandatory)* - -- **SC-001**: A user can start a configured trading session and reach the "waiting for candle close" idle state within 2 minutes of invoking the system. -- **SC-002**: The system aligns to the next candle boundary at startup. If alignment is delayed, the system logs a warning and proceeds — it never blocks or silently skips a session due to startup latency alone. -- **SC-003**: Every session produces either a completed decision record (Buy/Sell/Hold with unified HTML report) or a logged skip entry with reason — no session ends silently. -- **SC-004**: Every failure condition results in a log entry with sufficient detail for the user to identify the cause without additional instrumentation. -- **SC-005**: Stop-loss exits trigger within one 1-minute candle of the threshold being breached. -- **SC-006**: Feedback evaluations are triggered within one session of the strategy-defined evaluation window. -- **SC-007**: The memory bank accurately reflects cumulative per-ticker strategy performance and all feedback evaluations across the full run. -- **SC-008**: The system accepts any list of two or more US-listed tickers supplied by the user at configuration time; no specific pre-defined tickers are required. All tickers are evaluated using the same strategy rules regardless of which symbols are chosen. - ---- - -## Assumptions - -- Paper trading is the only mode in scope for V0.0.1; live trading is explicitly out of scope. -- The system runs as a single-process, single-machine application; distributed or cloud-hosted execution is out of scope. -- Market data, real-time price feeds, and market calendar are sourced from Alpaca's paper trading platform. US equities (NYSE, NASDAQ, AMEX) are the only supported market. -- The memory bank and HTML reports are stored locally on the host machine. -- "Session money budget" applies per session, not as a cumulative portfolio limit across the full run. -- All configured tickers operate independently throughout the system; no cross-ticker correlation logic is required in V0.0.1. -- Mean Reversion and Momentum are the only two strategies. Each ticker undergoes independent regime recognition per session and receives its own strategy assignment; multiple tickers may run different strategies in the same session. -- The agent determines lot size as part of its Buy decision, constrained by the session money budget communicated to it before investigation begins, and reasons jointly across all tickers it decides to Buy in the same session so it doesn't size each order as if it alone had the full budget (FR-010a). The execution workflow validates each order's value against the remaining budget at execution time, in sequence, and skips if exceeded (FR-010) — this hard check is authoritative regardless of the agent's sizing. -- The feedback agent evaluates a trade only once; re-evaluation is out of scope. -- Run completion does not force-close open positions; positions remain under stop-loss monitoring after the run ends and are persisted in the memory bank so they are carried into subsequent runs. -- `extended_hours: true` is a testing affordance; production runs should use standard market hours. diff --git a/specs/001-etf-paper-trading-agent/tasks.md b/specs/001-etf-paper-trading-agent/tasks.md deleted file mode 100644 index fa359c0..0000000 --- a/specs/001-etf-paper-trading-agent/tasks.md +++ /dev/null @@ -1,280 +0,0 @@ -# Tasks: Alphoryn — Automated ETF Paper Trading System - -**Input**: Design documents from `/specs/001-etf-paper-trading-agent/` - -**Prerequisites**: plan.md ✅ | spec.md ✅ | research.md ✅ | data-model.md ✅ | contracts/ ✅ | constitution.md ✅ - -**Tests**: Included — constitution Principle II mandates 100% pytest coverage (CI gate); no `pragma: no cover`. - -**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. - -## Format: `[ID] [P?] [Story] Description` - -- **[P]**: Can run in parallel (different files, no dependencies) -- **[Story]**: Which user story this task belongs to (US1–US4) -- Exact file paths included in all descriptions - ---- - -## 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. - -- [x] T001 Create project directory structure: `alphoryn/cli/`, `alphoryn/config/`, `alphoryn/agents/`, `alphoryn/execution/`, `alphoryn/monitor/`, `alphoryn/scheduler/`, `alphoryn/market_data/`, `alphoryn/memory/`, `alphoryn/reports/`, `alphoryn/secrets/`, `alphoryn/telemetry/`, `alphoryn/strategies/`, `alphoryn/skills/`, `tests/unit/`, `tests/integration/`, `tests/contract/`, `tests/fixtures/`, `templates/reports/`; add `__init__.py` to each Python package -- [x] T002 [P] Write `pyproject.toml` with all dependencies (`google-adk`, `alpaca-py`, `alpaca-mcp-server`, `typer`, `pydantic`, `pydantic-settings`, `sqlalchemy`, `google-cloud-secret-manager`, `google-cloud-logging`, `jinja2`, `ruff`, `pytest`, `pytest-cov`), Ruff config (zero violations gate), and pytest-cov config (100% threshold, `--cov=alphoryn`) -- [x] T003 [P] Create `config.json` example at repo root (non-secret fields only: SPY/QQQ, `1H` timeframe, `24H` duration, USD, 2% stop-loss, no budget per `contracts/config-schema.md`) - ---- - -## Phase 2: Foundational (Blocking Prerequisites) - -**Purpose**: Stage 1 + Stage 2 modules from `plan.md §Build Order`. All user stories depend on these completing first. - -**⚠️ CRITICAL**: No user story work can begin until this phase is complete. - -### Stage 1 — No dependencies (implement in parallel) - -- [x] T004 [P] Implement `AlphorynConfig` Pydantic `BaseSettings` model in `alphoryn/config/models.py` (all fields from `data-model.md §Config Model`: `tickers: list[str]` (min 2), `candle_timeframe`, `run_duration`, `extended_hours`, `session_money_budget`, `stop_loss_pct`, `max_startup_latency_seconds`, `currency`, `memory_db_path`; derived fields `session_count`, `alpaca_paper_mode`) -- [x] T005 [P] Implement `alphoryn/secrets/client.py` (fetch `alpaca-api-key` and `alpaca-api-secret` from GCP Secret Manager via Application Default Credentials; inject as `ALPACA_API_KEY` and `ALPACA_SECRET_KEY` env vars; raise `SecretsError` on failure per `research.md §API Key Management`) -- [x] T006 [P] Implement `alphoryn/telemetry/logger.py` (system-wide structured event emitter; all 14 event types from `research.md §Telemetry`; common fields: `event_type`, `session_id`, `component`, `etf`, `timestamp`, `latency_ms`, `payload`; Cloud Logging upload via `google-cloud-logging`; on Cloud Logging unavailable: write to stderr and continue — never block execution per constitution Principle IV) -- [x] T044 [P] Implement `alphoryn/telemetry/otel.py` (OTel trace exporter setup; wire Google ADK's built-in GCP exporters via `google.adk.telemetry.google_cloud.get_gcp_exporters(enable_cloud_logging=True)`; set `OTEL_SERVICE_NAME=alphoryn`; called once at CLI startup before any agent init; fail silently if `opentelemetry-exporter-gcp-*` packages missing — never block execution; no dependencies) - -### Stage 2 — Depends on Stage 1 - -- [x] T007 Implement `alphoryn/config/loader.py` (layered config resolution: load JSON file at `--config` path or `./config.json`, apply non-None CLI overrides, validate merged result into `AlphorynConfig`; field-level error on invalid input; depends on T004) -- [x] T008 [P] Implement `alphoryn/memory/schema.py` (SQLAlchemy ORM models for all five entities per `data-model.md §Database Entities`: `Run`, `Session`, `Position`, `FeedbackEvaluation`, `MemoryEntry`; all columns, types, FK constraints, and `Position.status` enum values; depends on T004) -- [x] T009 Implement `alphoryn/memory/bank.py` (startup load query for all `status=OPEN` positions across all runs; raise `MemoryBankError` on inaccessible/corrupt DB; per-session writes for `Session`, `Position`, `MemoryEntry`; `FeedbackEvaluation` write + `Position.status` update; carry-over position blocking query; depends on T008) - -### Unit Tests for Stage 1 + 2 (parallel after modules above) - -- [x] T010 [P] Unit tests for `alphoryn/config/` in `tests/unit/test_config.py` (`AlphorynConfig` field validation, required fields, defaults, `session_count` derivation, loader JSON→CLI override resolution, invalid config raises `ValidationError`) -- [x] T011 [P] Unit tests for `alphoryn/secrets/client.py` in `tests/unit/test_secrets.py` (mock `google-cloud-secret-manager`; successful fetch injects env vars; fetch failure raises `SecretsError`) -- [x] T012 [P] Unit tests for `alphoryn/telemetry/logger.py` in `tests/unit/test_telemetry.py` (all 14 event types emit correct schema; Cloud Logging failure → stderr output, execution continues; `latency_ms` and `session_id` present on every event) -- [x] T013 Unit tests for `alphoryn/memory/` in `tests/unit/test_memory.py` (in-memory SQLite; schema integrity for all five entities; startup load returns OPEN positions from multiple runs; corrupt DB raises `MemoryBankError`; per-session write ordering; `FeedbackEvaluation.attempt_count ≤ 3` invariant) - -**Checkpoint**: Foundation ready — user story implementation can begin - ---- - -## Phase 3: User Story 1 — Configure and Launch a Trading Session (Priority: P1) 🎯 MVP - -**Goal**: User runs `alphoryn run`, config is validated, session count is displayed, memory bank is loaded, and the system counts down to the next candle boundary. - -**Independent Test**: Supply a valid config → confirm system reaches "waiting for candle close" state with correct session count displayed, without executing any trades (spec US1 Acceptance Scenario 1). - -### Tests for User Story 1 - -> **NOTE: Write these tests FIRST, ensure they FAIL before implementation** - -- [x] T014 [P] [US1] Contract test: CLI startup output and exit codes in `tests/contract/test_cli.py` (startup banner, "N sessions planned", memory bank line, countdown line; exit code 1 on invalid config, exit code 2 on inaccessible memory bank, exit code 3 on Secret Manager unreachable; `alphoryn status` and `alphoryn history` output format — all per `contracts/cli.md`) -- [x] T015 [P] [US1] Contract test: config schema in `tests/contract/test_config_schema.py` (all required fields present; optional `session_money_budget` = None means no limit; fractional session count triggers warning; `candle_timeframe` restricted to `10min`, `30min`, `1H`, `4H` (`10min` is a testing affordance); `tickers` must be list with min 2 items; validates against `contracts/config-schema.md`) - -### Implementation for User Story 1 - -- [x] T016 [US1] Implement candle boundary alignment and market hours check in `alphoryn/scheduler/scheduler.py` (query Alpaca market calendar API via `alpaca-py` to get next market open and candle close timestamps; compute wait time; if wait exceeds `max_startup_latency_seconds` emit warning and proceed; display countdown to stdout) -- [x] T017 [US1] Implement `alphoryn run` startup path in `alphoryn/cli/main.py` (Typer app; load config via `config/loader.py`; fetch secrets via `secrets/client.py`; load memory bank open positions via `memory/bank.py`; compute `session_count`; warn on fractional sessions; display startup output per `contracts/cli.md`; pass control to scheduler) -- [x] T018 [US1] Implement `alphoryn status` command in `alphoryn/cli/main.py` (query memory bank for current run + open positions; display format per `contracts/cli.md`; `--db` path option) -- [x] T019 [US1] Implement `alphoryn history` command in `alphoryn/cli/main.py` (query memory bank sessions by run; `--run` filter; `--db` path option; display table most-recent-first per `contracts/cli.md`) -- [x] T020 [US1] Integration test: full startup cycle in `tests/integration/test_startup.py` (stub `alpaca-py` calendar API + stub Secret Manager; valid config → reaches waiting state, session count matches; fractional session count → warning emitted; invalid config → exit code 1; missing memory bank → exit code 2; Secret Manager unreachable → exit code 3) - -**Checkpoint**: User Story 1 fully functional — `alphoryn run/status/history` all respond correctly - ---- - -## Phase 4: User Story 2 — Autonomous Per-Session Decision Cycle (Priority: P1) 🎯 MVP - -**Goal**: At each candle close, the system investigates all configured tickers, selects a strategy per ticker, decides Buy/Sell/Hold per ticker, executes via Alpaca, generates a unified HTML report covering all tickers, and writes a memory bank entry. - -**Independent Test**: Trigger one candle close → system completes the full investigate-decide-execute cycle → session record + HTML report + memory bank entry written, without requiring a second candle close (spec US2 acceptance scenarios). - -### Tests for User Story 2 - -> **NOTE: Write these tests FIRST, ensure they FAIL before implementation** - -- [x] T021 [P] [US2] Unit tests for `alphoryn/market_data/client.py` in `tests/unit/test_market_data.py` (stub `alpaca-py` bars; verify all 15 `ETFSignals` fields computed correctly from fixture OHLCV data: RSI-14, ADX-14, EMA-20, EMA-50, SMA-20, Bollinger bands and %B, MACD line/signal/histogram, volume ratio, price vs EMA/SMA pct; `build_snapshot` returns frozen `SignalSnapshot`; `data_fetch` not exposed as ADK tool) -- [x] T022 [P] [US2] Unit tests for `alphoryn/execution/agent.py` in `tests/unit/test_execution.py` (stub `alpaca-py`; BUY decision → `BUDGET_CHECK` + `ORDER_PLACED` events + `Position` written; HOLD → `AGENT_DECISION` event only; budget exceeded → `ORDER_FAILED`; assert zero LLM model calls — verifies constitution Principle I) -- [x] T023 [P] [US2] Unit tests for `alphoryn/reports/generator.py` in `tests/unit/test_reports.py` (unified `session.html.j2` template renders `
` with per-ticker decisions table and reasoning sections; path format `reports/run-{id}/session-{seq}.html`; context includes `tickers: list[str]` and `ticker_details: list[dict]` per `contracts/report-context.md`) - -### Implementation for User Story 2 - -- [x] T024 [US2] Implement `alphoryn/market_data/client.py` (internal `data_fetch` using `alpaca-py` `StockBarsRequest` for 1H bars; compute all 15 `ETFSignals` fields from OHLCV bars; `build_snapshot` registered as ADK tool returning frozen `SignalSnapshot` for all configured tickers; separate 1-min bar polling method for stop-loss monitor; `data_fetch` not exposed to agents — internal only per `research.md §build_snapshot`) -- [x] T025 [US2] Implement `alphoryn/agents/prompts.py` main agent system prompt (regime recognition instructions; strategy selection rules referencing `alphoryn/strategies/mean_reversion.md` and `alphoryn/strategies/momentum.md`; snapshot isolation enforcement clause: "Do not call any further market data tools after build_snapshot returns"; memory bank context format; output schema for `SessionDecision` per `contracts/agents.md §Decision Handoff`) -- [x] T026 [US2] Implement `alphoryn/agents/main_agent.py` (Google ADK `LlmAgent` with Gemini model; `build_snapshot` registered as sole ADK tool; regime recognition per ticker using `alphoryn/skills/` (identify_regime, mean_reversion_entry, momentum_entry, size_position, read_memory); outputs `SessionDecision` dataclass per `contracts/agents.md`; emits `AGENT_DECISION` + `TOOL_CALL` + `SIGNAL_SNAPSHOT_BUILT` telemetry events) -- [x] T027 [US2] Implement `alphoryn/execution/agent.py` (ADK `BaseAgent` subclass — no LLM model configured; `_run_async_impl` processes `SessionDecision` sequentially per ticker per `contracts/agents.md §Decision Handoff`: HOLD → log; BUY/SELL → budget check via `alpaca-py` → `BUDGET_CHECK` event → market order via `alpaca-py` → `ORDER_PLACED`/`ORDER_FAILED` event → write `Position` to memory bank with `status=OPEN`; existing OPEN position blocks new BUY per FR-014) -- [x] T028 [US2] Implement `alphoryn/reports/generator.py` (Jinja2 template rendering using `templates/reports/session.html.j2` — unified template covering all tickers in one report with per-ticker decisions table and reasoning sections; context includes `tickers: list[str]` and `ticker_details: list[dict]` per `contracts/report-context.md`; output path: `reports/run-{run_id}/session-{session_seq}.html`; store path in `Session.html_report_path`; `
` present per ticker for feedback agent extraction) -- [x] T029 [US2] Implement session budget enforcement and heartbeat in `alphoryn/scheduler/scheduler.py` (`asyncio.wait_for` with 87% of candle timeframe for investigation (e.g., 52 min for 1H) and 13% for decide+execute (e.g., 7 min for 1H); overrun → force Hold on all tickers + emit `BUDGET_TIMEOUT` telemetry; 5-min heartbeat stdout lines during investigation: `[session-id] investigating... N min elapsed`; emit `SESSION_START` + `SESSION_END` telemetry) -- [x] T030 [US2] Implement full session loop in `alphoryn/scheduler/scheduler.py` (outer run loop: check run complete + market open; inner session: wait for candle close → invoke main_agent → pass `SessionDecision` to execution_agent → generate HTML report → write `Session` + `MemoryEntry` records; skipped sessions not counted against total per FR-018; emit `MARKET_CLOSED` telemetry on closed market) -- [x] T031 [US2] Integration test: single session cycle in `tests/integration/test_session_cycle.py` (StubGeminiClient returns fixture `SessionDecision`; StubMCPClient returns fixture signal data; StubAlpacaClient returns fixture bars + order confirmation; assert zero MCP market data calls during investigation phase; assert `Session` record written + `html_report_path` non-null + `MemoryEntry` written; investigation timeout → Hold forced + `BUDGET_TIMEOUT` event emitted) - -**Checkpoint**: User Stories 1 AND 2 both work independently — full investigation-to-report cycle functional - ---- - -## Phase 5: User Story 3 — Position Lifecycle and Risk Management (Priority: P2) - -**Goal**: Open positions are continuously monitored; stop-loss, profit-target, and evaluation-window exits trigger automatically without LLM involvement; main agent is blocked from opening new positions on an ETF with an open unevaluated trade. - -**Independent Test**: Open a simulated position → price hits stop-loss threshold → position closes automatically and is logged → main agent attempt to BUY same ETF is forced to Hold (spec US3 acceptance scenarios). - -### Tests for User Story 3 - -> **NOTE: Write these tests FIRST, ensure they FAIL before implementation** - -- [x] T032 [P] [US3] Unit tests for `alphoryn/monitor/monitor.py` in `tests/unit/test_monitor.py` (StubAlpacaClient; stop-loss price breach → `STOP_LOSS_TRIGGERED` + `POSITION_CLOSED` events + `Position.status = CLOSED_STOP_LOSS`; price reaches `exit_target` price level → `PROFIT_TARGET_TRIGGERED`; `evaluation_window_session` reached → `WINDOW_EXPIRY_TRIGGERED`; Momentum trailing stop: new price high updates `trailing_stop_high_watermark`; stop_price = watermark × (1 − trail_pct); zero LLM calls asserted) - -### Implementation for User Story 3 - -- [x] T033 [US3] Implement `alphoryn/monitor/monitor.py` (subclass `threading.Thread`; polls latest 1-min bar via `alpaca-py` every ≤30 seconds per SC-005; for each OPEN position: check stop-loss breach (`current_price ≤ stop_loss_price`), profit-target reach (price level or trailing stop), window expiry (`evaluation_window_session == current_session_ordinal`); on exit: call `alpaca-py` `close_position` → on success write `Position.exit_price/exit_time/exit_reason/status`; update `trailing_stop_high_watermark` for Momentum positions on new price highs; emit `STOP_LOSS_TRIGGERED`/`PROFIT_TARGET_TRIGGERED`/`WINDOW_EXPIRY_TRIGGERED` + `POSITION_CLOSED` telemetry; on `close_position` failure retry next poll; stopped via `threading.Event`; thread stays alive while any position is OPEN after run ends per `contracts/agents.md §Monitor Lifecycle`) -- [x] T034 [US3] Implement OPEN-position blocking in `alphoryn/execution/agent.py` (before executing BUY for a ticker: query memory bank for OPEN position on same ticker; if found emit `AGENT_DECISION` HOLD with reason "position-blocked" and skip order; other tickers unaffected when one ticker is blocked per US3 Acceptance Scenario 3) -- [x] T035 [US3] Integration test: position lifecycle in `tests/integration/test_position_lifecycle.py` (open BUY position via StubAlpacaClient; simulate price drop to stop-loss → monitor closes position, status = `CLOSED_STOP_LOSS`; assert same ticker BUY in next session blocked; simulate Momentum price rise → trailing stop watermark updated; window expiry → `CLOSED_WINDOW_EXPIRY`) - -**Checkpoint**: User Stories 1, 2, AND 3 all independently functional — risk management in place - ---- - -## Phase 6: User Story 4 — Feedback Evaluation and Memory Learning (Priority: P2) - -**Goal**: At the strategy-defined evaluation window, the feedback agent compares the original thesis to the actual outcome, writes a structured judgment to the memory bank, and unblocks the ETF for new trades. - -**Independent Test**: Simulate a completed trade entry, fast-forward to the evaluation session, confirm feedback agent produces a `FeedbackEvaluation` record in memory bank and marks position `EVALUATED` (spec US4 acceptance scenarios). - -### Tests for User Story 4 - -> **NOTE: Write these tests FIRST, ensure they FAIL before implementation** - -- [x] T036 [P] [US4] Unit tests for `alphoryn/agents/feedback_agent.py` in `tests/unit/test_feedback_agent.py` (StubGeminiClient; thesis extraction from HTML `
`; `CORRECT`/`INCORRECT`/`NEUTRAL` judgment written to `FeedbackEvaluation`; `Position.status = EVALUATED`; 3-retry policy: first two fail → retry; third fail → `EVALUATION_FAILED`, ETF unblocked; `MemoryEntry.outcome_judgment` populated after evaluation) - -### Implementation for User Story 4 - -- [x] T037 [US4] Implement `alphoryn/agents/prompts.py` feedback agent system prompt (thesis extraction instructions: parse `
` from HTML; judgment rubric: `CORRECT` if outcome aligns with thesis, `INCORRECT` if contradicts, `NEUTRAL` if insufficient evidence; output schema for `FeedbackEvaluation` record; retry instructions) -- [x] T038 [US4] Implement `alphoryn/agents/feedback_agent.py` (Google ADK `LlmAgent` with Gemini model; receives `FeedbackInput` from scheduler per `contracts/agents.md §Feedback Trigger`; step 1: read HTML report at `html_report_path` → extract thesis from `
`; step 2: fetch 1H candle close at evaluation time via Alpaca MCP `get_bars`; step 3: produce `CORRECT`/`INCORRECT`/`NEUTRAL` judgment; step 4: write `FeedbackEvaluation` + update `Position.status = EVALUATED` + write `MemoryEntry.outcome_judgment`; retry policy: up to 3 attempts; on 3rd failure: write partial `FeedbackEvaluation`, set `Position.status = EVALUATION_FAILED`, unblock ticker, emit warning telemetry; emit `AGENT_DECISION` telemetry with reasoning) -- [x] T039 [US4] Implement feedback trigger in `alphoryn/scheduler/scheduler.py` (at start of each session, before investigation: query memory bank for positions where `status IN (CLOSED_STOP_LOSS, CLOSED_PROFIT_TARGET, CLOSED_WINDOW_EXPIRY)` AND `evaluation_window_session == current_session_ordinal` AND no `FeedbackEvaluation` exists; build `FeedbackInput` from Position + Session records; invoke `feedback_agent` sequentially for each due position; evaluation runs before investigation in same session per `contracts/agents.md §Feedback Trigger`) -- [x] T040 [US4] Integration test: feedback evaluation cycle in `tests/integration/test_feedback.py` (StubGeminiClient + StubMCPClient; open position → position closes → evaluation window session arrives → scheduler triggers feedback_agent → `FeedbackEvaluation` written with `outcome_judgment` + `Position.status = EVALUATED` → ETF unblocked for new BUY; simulate 3 consecutive feedback failures → `EVALUATION_FAILED` + ETF unblocked) - -**Checkpoint**: All four user stories independently functional — full learning loop operational - ---- - -## Phase 7: Polish & Cross-Cutting Concerns - -**Purpose**: CI gates, coverage verification, and quickstart validation. - -- [x] T041 [P] Ruff linting pass: run `ruff check alphoryn/ tests/` and fix until zero violations; verify `ruff.toml` or `pyproject.toml [tool.ruff]` config is consistent with CI gate -- [x] T042 [P] pytest coverage pass: run `pytest --cov=alphoryn --cov-report=term-missing` and verify 100% coverage on all modules; fix any uncovered lines (no `pragma: no cover` allowed per constitution Principle II) -- [x] T043 Quickstart validation: follow `quickstart.md` end-to-end; verify `alphoryn --help`, `alphoryn run --help`, `alphoryn status --help`, `alphoryn history --help` all respond; verify config.json example loads without error; confirm memory bank initializes at `~/.alphoryn/memory.db` - ---- - -## Dependencies & Execution Order - -### Phase Dependencies - -- **Setup (Phase 1)**: No dependencies — start immediately -- **Foundational (Phase 2)**: Depends on Phase 1 completion — **BLOCKS all user stories** -- **User Story 1 (Phase 3)**: Depends on Phase 2 completion — no dependency on US2/US3/US4 -- **User Story 2 (Phase 4)**: Depends on Phase 2 completion — no dependency on US1 (except CLI integration which can be wired last) -- **User Story 3 (Phase 5)**: Depends on Phase 2 completion — `monitor/monitor.py` also needs `market_data/client.py` from Phase 4 (T024) -- **User Story 4 (Phase 6)**: Depends on Phase 2 + `reports/generator.py` (T028 from Phase 4) -- **Polish (Phase 7)**: Depends on all desired user stories being complete - -### User Story Dependencies - -| Story | Depends on | Blocking | -|---|---|---| -| US1 (P1) | Phase 2 | Nothing blocked on US1 | -| US2 (P1) | Phase 2 | US3 needs `market_data/client.py` (T024); US4 needs `reports/generator.py` (T028) | -| US3 (P2) | Phase 2 + T024 | US4 benefit from US3 (positions close before feedback) | -| US4 (P2) | Phase 2 + T028 | Nothing blocked on US4 | - -### Within Each User Story - -- Tests → Models → Services → Integration (write tests first, verify they fail) -- `agents/prompts.py` (T025 for US2, T037 for US4) before the agent implementation it describes -- Foundation unit tests (T010–T013) can run in parallel once corresponding modules exist -- Models before services, services before integration tests - -### Parallel Opportunities - -```bash -# Phase 2 Stage 1 — full parallel: -Task T004: AlphorynConfig in alphoryn/config/models.py -Task T005: secrets/client.py -Task T006: telemetry/logger.py -Task T044: telemetry/otel.py - -# Phase 2 unit tests — full parallel (after Stage 2 complete): -Task T010: tests/unit/test_config.py -Task T011: tests/unit/test_secrets.py -Task T012: tests/unit/test_telemetry.py -Task T013: tests/unit/test_memory.py - -# US2 tests — full parallel (before implementation): -Task T021: tests/unit/test_market_data.py -Task T022: tests/unit/test_execution.py -Task T023: tests/unit/test_reports.py - -# Polish — full parallel: -Task T041: ruff check alphoryn/ tests/ -Task T042: pytest --cov=alphoryn -``` - ---- - -## Implementation Strategy - -### MVP First (User Stories 1 + 2 Only) - -1. Complete Phase 1: Setup -2. Complete Phase 2: Foundational (**CRITICAL — blocks all stories**) -3. Complete Phase 3: User Story 1 (configure + launch) -4. **STOP and VALIDATE**: `alphoryn run` reaches "waiting for candle close" state -5. Complete Phase 4: User Story 2 (session decision cycle) -6. **STOP and VALIDATE**: single candle close → full session cycle → HTML report generated -7. Deploy/demo the MVP - -### Incremental Delivery - -1. Phase 1 + Phase 2 → Foundation ready -2. Phase 3 (US1) → `alphoryn run/status/history` all functional -3. Phase 4 (US2) → full session cycle with investigation + execution + report -4. Phase 5 (US3) → position monitoring + risk management active -5. Phase 6 (US4) → feedback evaluation closes the learning loop -6. Phase 7 → CI green (100% coverage + zero ruff violations) - -### Parallel Team Strategy - -Once Phase 2 is complete: -- **Developer A**: Phase 3 (US1) — config, CLI, scheduler candle alignment -- **Developer B**: Phase 4 (US2) — market_data, agents, execution, reports -- **Developer C**: Phase 5 (US3) — monitor, position blocking -- Developer A resumes Phase 6 (US4) after Phase 3 is done - ---- - -## Notes - -- `[P]` = tasks touch different files with no incomplete-task dependencies; safe to run in parallel -- `[Story]` label maps to spec.md user story for traceability -- Tests MUST be written and verified to FAIL before corresponding implementation -- Constitution Principle II: 100% coverage is a CI hard gate — no `pragma: no cover` -- Constitution Principle I: `execution/agent.py` and `monitor/monitor.py` must contain zero LLM model calls; assert in unit tests -- Constitution Principle V: main agent must make zero MCP market data calls after `build_snapshot` returns; assert in integration tests (T031) via stubbed MCP call-count tracking -- Commit after each task or logical group; run `ruff check` + `pytest` before each commit -- Stop at Phase 3 and Phase 4 checkpoints to validate stories independently before proceeding