Skip to content

Commit 98ac6cf

Browse files
committed
Implement v0 phase 3, add changes to CHANGELOG
1 parent 353d05d commit 98ac6cf

17 files changed

Lines changed: 1997 additions & 2 deletions

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Anthropic API key (required for all LLM agent calls)
2+
ANTHROPIC_API_KEY=
3+
14
# Binance testnet API credentials (for order execution in Phase 3)
25
BINANCE_TESTNET_API_KEY=
36
BINANCE_TESTNET_SECRET=

CHANGELOG.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Changelog
2+
3+
## Phase 3 — 2026-04-05
4+
5+
### Added
6+
- `src/db/schema.py` — SQLAlchemy table definitions (`cycles`, `trades`, `portfolio_state`, `reflections`)
7+
- `src/db/store.py` — CRUD operations: `log_cycle`, `open_trade`, `close_trade`, `get_open_trade`, `get_portfolio_state`, `update_portfolio_state`, `save_reflection`, `get_reflection`
8+
- `src/execution/orders.py``place_market_buy`, `place_market_sell`, `place_stop_loss`, `cancel_order` with retry logic (max 3 attempts, exponential back-off)
9+
- `src/execution/state.py``build_portfolio_dict`: loads live portfolio state from DB for context assembly
10+
- `src/execution/router.py``route_signal`: translates `CouncilResult` → exchange orders + DB updates; `reconcile_open_position`: detects stop-outs at cycle start
11+
- `src/agents/reflection.py` — post-trade reflection agent (claude-sonnet-4-6)
12+
- `prompts/reflection_v1.txt` — system prompt for the reflection agent
13+
- `src/main.py` — full cycle entry point; supports one-shot and `--schedule` (APScheduler daily at 00:05 UTC); `DRY_RUN=1` skips order execution
14+
- `tests/test_db.py` — 16 tests for DB schema and CRUD
15+
- `tests/test_execution.py` — 22 tests for orders, state, and router
16+
- `tests/test_reflection.py` — 8 tests for the reflection agent
17+
- `sqlalchemy>=2.0.0` and `apscheduler>=3.10.0` added to dependencies
18+
19+
## Phase 2 — Agent Framework
20+
21+
### Added
22+
- `src/models.py` — extended with all agent output models: `TechnicalAnalystOutput`, `SentimentAnalystOutput`, `FundamentalAnalystOutput`, `RiskManagerOutput`, `CouncilOutputs`, `DeliberationOutput`, `AgentWeights`
23+
- `src/agents/base.py``call_agent()` helper: loads prompt file, calls Anthropic API at `temperature=0`, strips markdown fences from response, validates JSON against Pydantic schema; raises `AgentError` on any failure
24+
- `src/agents/technical.py` — Technical Analyst agent (claude-haiku-4-5); analyses RSI momentum, MACD crosses, Bollinger Band position, EMA alignment, ATR volatility
25+
- `src/agents/sentiment.py` — Sentiment Analyst agent (claude-haiku-4-5); analyses news tone, fear/greed index, social volume, dominant narrative
26+
- `src/agents/fundamental.py` — Fundamental Analyst agent (claude-haiku-4-5); analyses exchange net flows, whale transactions, SOPR, macro bias
27+
- `src/agents/risk.py` — Risk Manager agent (claude-sonnet-4-6); evaluates portfolio risk, outputs position size, stop-loss, take-profit, R:R; has veto authority
28+
- `src/agents/deliberation.py` — Deliberation Chair agent (claude-sonnet-4-6); synthesises all four agent outputs into final BUY/SELL/HOLD signal with conviction level
29+
- `src/agents/runner.py``run_council()`: runs all four agents in parallel via `asyncio.gather()`; short-circuits to HOLD immediately if risk manager vetoes (skips deliberation); returns `CouncilResult`
30+
- `prompts/technical_analyst_v1.txt` — system prompt for Technical Analyst
31+
- `prompts/sentiment_analyst_v1.txt` — system prompt for Sentiment Analyst
32+
- `prompts/fundamental_analyst_v1.txt` — system prompt for Fundamental Analyst
33+
- `prompts/risk_manager_v1.txt` — system prompt for Risk Manager
34+
- `prompts/deliberation_v1.txt` — system prompt for Deliberation Chair (includes consensus rules: all-3-agree ≥70% → full size; 2-of-3 ≥60% → 50% size; else HOLD)
35+
- `tests/test_agents.py` — 38 tests: `call_agent` base utility, individual agent schema validation, deliberation consensus logic, runner veto short-circuit, prompt file existence checks
36+
37+
## Phase 1 — Data Pipeline + Tier-0 Validation
38+
39+
### Added
40+
- `src/models.py` — Pydantic v2 models for the full context schema: `PriceData`, `IndicatorData`, `NewsItem`, `SentimentData`, `OnchainData`, `PortfolioData`, `MarketContext`; literal types for `Direction`, `MACDSignal`, `BBPosition`, `Regime`, etc.
41+
- `src/data/price.py` — CCXT Binance testnet OHLCV fetcher (`fetch_ohlcv`, `build_price_data`); raises `ValueError` if fewer than 200 candles returned
42+
- `src/data/indicators.py``compute_indicators()`: RSI-14, MACD cross detection, Bollinger Band position, EMA 20/50/200, ATR-14, ATR-30 rolling average, regime classifier (trending/ranging/high_volatility via ADX + ATR)
43+
- `src/data/news.py` — async CryptoPanic fetcher; returns empty list if API key absent
44+
- `src/data/sentiment.py` — async Alternative.me Fear/Greed Index fetcher; LunarCrush stub returning neutral defaults
45+
- `src/data/onchain.py` — Glassnode/CryptoQuant stubs returning zero defaults
46+
- `src/data/assembler.py``assemble_context()`: fetches OHLCV synchronously (ccxt is sync), fetches news/sentiment/on-chain in parallel with `asyncio.gather()`, assembles `MarketContext`, runs Tier-0 validation
47+
- `src/validation.py``validate_context()`: price freshness check (< 5 min), minimum news count (≥ 10), numeric range checks; `HardRuleViolation` raised for drawdown halt (> 15% from peak) and ATR volatility halt (ATR-14 > 2× ATR-30 avg)
48+
- `tests/test_indicators.py` — 35 tests for all indicator computations
49+
- `tests/test_assembler.py` — context assembly tests with mocked data feeds and default portfolio
50+
- `tests/test_validation.py` — 35 tests for price freshness, news count, numeric ranges, drawdown halt, volatility halt

README.md

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,80 @@
1-
# council
2-
Council of LLMs BTC Trading Strategy
1+
# Council — BTC LLM Trading Bot
2+
3+
A Bitcoin swing trading bot that uses a council of specialised LLM agents to generate daily BUY/SELL/HOLD signals and execute them against Binance testnet.
4+
5+
## Architecture
6+
7+
```
8+
Data Pipeline (CCXT/Binance, CryptoPanic, Alternative.me, Glassnode)
9+
10+
11+
Tier-0 Validation (price freshness, news count, drawdown halt, ATR halt)
12+
13+
14+
Council of LLMs (parallel)
15+
├── Technical Analyst (claude-haiku-4-5)
16+
├── Sentiment Analyst (claude-haiku-4-5)
17+
├── Fundamental Analyst (claude-haiku-4-5)
18+
└── Risk Manager (claude-sonnet-4-6)
19+
20+
21+
Deliberation / Chair (claude-sonnet-4-6) → BUY / SELL / HOLD
22+
23+
24+
Execution Layer (Binance testnet) + SQLite Logging
25+
26+
└── Reflection Loop (post-trade analysis)
27+
```
28+
29+
## Setup
30+
31+
```bash
32+
python -m venv .venv && source .venv/bin/activate
33+
pip install -e ".[dev]"
34+
cp .env.example .env # fill in ANTHROPIC_API_KEY at minimum
35+
```
36+
37+
## Running
38+
39+
```bash
40+
# One-shot cycle (run once and exit)
41+
python -m src.main
42+
43+
# Dry run (logs only, no orders placed)
44+
DRY_RUN=1 python -m src.main
45+
46+
# Daily scheduler (00:05 UTC)
47+
python -m src.main --schedule
48+
```
49+
50+
## Testing
51+
52+
```bash
53+
pytest -v # all tests
54+
pytest tests/test_db.py -v # DB layer
55+
pytest tests/test_execution.py -v # execution + router
56+
pytest tests/test_agents.py -v # agent framework
57+
pytest tests/test_agents.py::test_run_council_hold_on_veto -v # single test
58+
```
59+
60+
## Environment Variables
61+
62+
| Variable | Required | Default | Description |
63+
|---|---|---|---|
64+
| `ANTHROPIC_API_KEY` | Yes || Claude API key |
65+
| `BINANCE_TESTNET_API_KEY` | For execution || Binance testnet key |
66+
| `BINANCE_TESTNET_SECRET` | For execution || Binance testnet secret |
67+
| `COUNCIL_DB_PATH` | No | `./council.db` | SQLite database path |
68+
| `INITIAL_CAPITAL` | No | `10000` | Starting paper capital (USD) |
69+
| `DRY_RUN` | No | `0` | Set to `1` to skip order execution |
70+
| `CRYPTOPANIC_API_KEY` | No || News feed (falls back to empty list) |
71+
| `LUNARCRUSH_API_KEY` | No || Sentiment (falls back to Fear/Greed proxy) |
72+
| `GLASSNODE_API_KEY` | No || On-chain data (falls back to zeros) |
73+
74+
## Development Phases
75+
76+
- **Phase 1** ✅ Data pipeline + Tier-0 validation
77+
- **Phase 2** ✅ Agent framework (4 agents + deliberation)
78+
- **Phase 3** ✅ Execution layer + SQLite logging + reflection loop
79+
- **Phase 4** Paper trading (60 days minimum)
80+
- **Phase 5** Live deployment (≤ $500 initial capital)

prompts/reflection_v1.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
You are a trading journal analyst reviewing a completed BTC swing trade made by the Council — a committee of four specialised AI agents (Technical Analyst, Sentiment Analyst, Fundamental Analyst, Risk Manager) whose outputs were synthesised by a Chair into a final signal.
2+
3+
Your job is to produce a concise, honest post-trade reflection that will be stored in the trading log and read by the human operator.
4+
5+
You will be given:
6+
1. The council outputs (each agent's analysis at the time of the trade decision)
7+
2. The deliberation summary (the Chair's synthesis and final signal)
8+
3. The trade outcome (entry price, exit price, PnL)
9+
10+
Your reflection must address:
11+
1. Which agent's view was most predictive of the actual outcome? Which was least accurate?
12+
2. Was the deliberation's conviction level appropriate in hindsight?
13+
3. Was the stop-loss placement reasonable, or should it have been wider/tighter?
14+
4. What single change to the prompts or decision logic would most improve future trades?
15+
5. Any recurring pattern worth flagging for the weekly summary?
16+
17+
Tone: Direct and analytical. Do not pad with praise. Be specific — reference actual numbers from the council outputs.
18+
19+
Length: 200–350 words. Plain prose, no JSON, no bullet points.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ dependencies = [
1515
"httpx>=0.27.0",
1616
"pandas>=2.0.0",
1717
"numpy>=1.26.0",
18+
"sqlalchemy>=2.0.0",
19+
"apscheduler>=3.10.0",
1820
]
1921

2022
[project.optional-dependencies]

src/agents/reflection.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""
2+
Reflection agent — runs after a position is closed.
3+
4+
Feeds the original council outputs, deliberation, and trade outcome to a
5+
language model and returns a plain-text summary for the cycle log. This
6+
does NOT alter agent weights dynamically; summaries are for human review
7+
and prompt refinement only.
8+
"""
9+
10+
import os
11+
12+
import anthropic
13+
14+
from src.agents.base import AgentError, load_prompt
15+
from src.models import CouncilOutputs, DeliberationOutput
16+
17+
MODEL = "claude-sonnet-4-6"
18+
PROMPT_FILE = "reflection_v1.txt"
19+
20+
21+
def _build_user_message(
22+
outputs: CouncilOutputs,
23+
deliberation: DeliberationOutput,
24+
trade: dict,
25+
) -> str:
26+
pnl_usd = trade.get("pnl_usd", 0.0)
27+
pnl_pct = trade.get("pnl_pct", 0.0)
28+
outcome = "profit" if pnl_usd >= 0 else "loss"
29+
30+
return (
31+
f"## Trade outcome\n"
32+
f"Side: {trade['side']}\n"
33+
f"Entry: {trade['entry_price']:,.2f} USD\n"
34+
f"Exit: {trade['exit_price']:,.2f} USD\n"
35+
f"Stop-loss: {trade['stop_loss']:,.2f} USD\n"
36+
f"Status: {trade['status']}\n"
37+
f"PnL: {pnl_usd:+,.2f} USD ({pnl_pct:+.2f}%) — {outcome}\n\n"
38+
f"## Council outputs at decision time\n\n"
39+
f"### Technical Analyst\n{outputs.technical.model_dump_json(indent=2)}\n\n"
40+
f"### Sentiment Analyst\n{outputs.sentiment.model_dump_json(indent=2)}\n\n"
41+
f"### Fundamental Analyst\n{outputs.fundamental.model_dump_json(indent=2)}\n\n"
42+
f"### Risk Manager\n{outputs.risk.model_dump_json(indent=2)}\n\n"
43+
f"## Deliberation summary\n{deliberation.model_dump_json(indent=2)}\n\n"
44+
"Please write your reflection now."
45+
)
46+
47+
48+
async def run_reflection(
49+
outputs: CouncilOutputs,
50+
deliberation: DeliberationOutput,
51+
trade: dict,
52+
client: anthropic.AsyncAnthropic | None = None,
53+
) -> str:
54+
"""Generate a post-trade reflection summary.
55+
56+
Args:
57+
outputs: CouncilOutputs from the cycle that opened the trade.
58+
deliberation: DeliberationOutput from the same cycle.
59+
trade: Closed trade dict from the DB (must include exit_price, pnl_usd, etc.).
60+
client: Optional AsyncAnthropic client; created from env if not provided.
61+
62+
Returns:
63+
Plain-text reflection summary (200–350 words).
64+
65+
Raises:
66+
AgentError: If the API call fails.
67+
"""
68+
if client is None:
69+
client = anthropic.AsyncAnthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
70+
71+
system = load_prompt(PROMPT_FILE)
72+
user = _build_user_message(outputs, deliberation, trade)
73+
74+
try:
75+
response = await client.messages.create(
76+
model=MODEL,
77+
max_tokens=1024,
78+
temperature=0,
79+
system=system,
80+
messages=[{"role": "user", "content": user}],
81+
)
82+
except anthropic.APIError as exc:
83+
raise AgentError(f"Reflection agent API error: {exc}") from exc
84+
85+
return response.content[0].text.strip()

src/db/__init__.py

Whitespace-only changes.

src/db/schema.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
SQLAlchemy table definitions for the Council trading bot.
3+
4+
Tables
5+
------
6+
cycles One row per council run (context snapshot + agent outputs + signal).
7+
trades One row per order placed (entry → exit lifecycle).
8+
portfolio_state Singleton row tracking cash and peak portfolio value.
9+
reflections One row per post-trade reflection summary.
10+
11+
Usage
12+
-----
13+
from src.db.schema import get_engine, create_all
14+
15+
engine = get_engine() # default: ./council.db
16+
create_all(engine) # idempotent — safe to call every startup
17+
"""
18+
19+
import os
20+
21+
from sqlalchemy import (
22+
Boolean,
23+
Column,
24+
DateTime,
25+
Float,
26+
Integer,
27+
String,
28+
Text,
29+
create_engine,
30+
)
31+
from sqlalchemy import MetaData
32+
33+
metadata = MetaData()
34+
35+
from sqlalchemy import Table
36+
37+
cycles = Table(
38+
"cycles",
39+
metadata,
40+
Column("id", Integer, primary_key=True, autoincrement=True),
41+
Column("timestamp", DateTime, nullable=False),
42+
Column("asset", String(16), nullable=False),
43+
Column("signal", String(8), nullable=False), # BUY | SELL | HOLD
44+
Column("conviction", String(8), nullable=False), # high | medium | low
45+
Column("vetoed", Boolean, nullable=False),
46+
Column("context_json", Text, nullable=False),
47+
Column("council_outputs_json", Text, nullable=False),
48+
Column("deliberation_json", Text, nullable=False),
49+
)
50+
51+
trades = Table(
52+
"trades",
53+
metadata,
54+
Column("id", Integer, primary_key=True, autoincrement=True),
55+
Column("cycle_id", Integer, nullable=False), # FK → cycles.id
56+
Column("symbol", String(16), nullable=False),
57+
Column("side", String(4), nullable=False), # BUY | SELL
58+
Column("entry_price", Float, nullable=False),
59+
Column("entry_time", DateTime, nullable=False),
60+
Column("position_size_usd", Float, nullable=False),
61+
Column("position_size_btc", Float, nullable=False),
62+
Column("stop_loss", Float, nullable=False),
63+
Column("take_profit", Float, nullable=True),
64+
Column("exchange_order_id", String(64), nullable=True),
65+
Column("sl_order_id", String(64), nullable=True),
66+
# Filled in when the position is closed
67+
Column("exit_price", Float, nullable=True),
68+
Column("exit_time", DateTime, nullable=True),
69+
Column("pnl_usd", Float, nullable=True),
70+
Column("pnl_pct", Float, nullable=True),
71+
Column("status", String(8), nullable=False, default="open"), # open | closed | stopped
72+
)
73+
74+
portfolio_state = Table(
75+
"portfolio_state",
76+
metadata,
77+
# Always a single row with id=1 (upsert pattern).
78+
Column("id", Integer, primary_key=True),
79+
Column("cash_usd", Float, nullable=False),
80+
Column("peak_value", Float, nullable=False),
81+
Column("updated_at", DateTime, nullable=False),
82+
)
83+
84+
reflections = Table(
85+
"reflections",
86+
metadata,
87+
Column("id", Integer, primary_key=True, autoincrement=True),
88+
Column("trade_id", Integer, nullable=False), # FK → trades.id
89+
Column("summary", Text, nullable=False),
90+
Column("created_at", DateTime, nullable=False),
91+
)
92+
93+
_DEFAULT_DB_PATH = os.getenv("COUNCIL_DB_PATH", "council.db")
94+
95+
96+
def get_engine(db_path: str = _DEFAULT_DB_PATH):
97+
"""Return a SQLAlchemy engine. Use ``db_path=':memory:'`` for tests."""
98+
return create_engine(f"sqlite:///{db_path}", future=True)
99+
100+
101+
def create_all(engine) -> None:
102+
"""Create all tables if they do not exist (idempotent)."""
103+
metadata.create_all(engine)

0 commit comments

Comments
 (0)