Skip to content

Commit 195b51e

Browse files
committed
Phase 4
1 parent 98ac6cf commit 195b51e

11 files changed

Lines changed: 1259 additions & 33 deletions

File tree

.env.example

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
# Anthropic API key (required for all LLM agent calls)
22
ANTHROPIC_API_KEY=
33

4-
# Binance testnet API credentials (for order execution in Phase 3)
4+
# Trading mode: paper (Binance testnet, default) | live (Binance mainnet)
5+
TRADING_MODE=paper
6+
# Required when TRADING_MODE=live — safety gate to prevent accidental live trading
7+
# COUNCIL_LIVE_CONFIRMED=1
8+
9+
# Binance testnet API credentials (paper trading)
510
BINANCE_TESTNET_API_KEY=
611
BINANCE_TESTNET_SECRET=
712

@@ -20,6 +25,15 @@ CRYPTOQUANT_API_KEY=
2025
TELEGRAM_BOT_TOKEN=
2126
TELEGRAM_CHAT_ID=
2227

23-
# Trading mode: paper | live
24-
TRADING_MODE=paper
28+
# Binance mainnet credentials (live trading only — set TRADING_MODE=live)
29+
# BINANCE_API_KEY=
30+
# BINANCE_SECRET=
31+
32+
# Starting capital in USD (used to seed portfolio_state on first run)
2533
INITIAL_CAPITAL=10000
34+
35+
# SQLite database path (default: ./council.db)
36+
# COUNCIL_DB_PATH=council.db
37+
38+
# Set to 1 to skip order execution and log only (useful for debugging cycles)
39+
# DRY_RUN=0

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## Phase 4 — 2026-04-05
4+
5+
### Added
6+
- `src/db/schema.py` — added `weekly_summaries` table (`window_start`, `window_end`, `summary`, `metrics_json`)
7+
- `src/db/store.py` — added `get_closed_trades_in_window`, `get_reflections_for_trades`, `get_cycles_in_window`, `save_weekly_summary`, `get_weekly_summaries`
8+
- `src/reporting/__init__.py` — new reporting package
9+
- `src/reporting/metrics.py``compute_sharpe_ratio` (annualised, sqrt(252)), `compute_max_drawdown` (equity-curve walk), `compute_expectancy` (avg_win × win_rate − avg_loss × loss_rate), `compute_council_consistency` (unanimous rate, confidence spread, veto rate), `compute_all_metrics` (all four, with target-met flags)
10+
- `src/reporting/weekly.py``run_weekly_summary`: loads week's trades + reflections + cycles, computes metrics, calls claude-sonnet-4-6 with `weekly_summary_v1.txt`, persists summary to DB; runnable as `python -m src.reporting.weekly`
11+
- `prompts/weekly_summary_v1.txt` — system prompt for the weekly summary agent (5 structured sections: performance verdict, pattern analysis, agent accuracy ranking, prompt refinement recommendations, next-period watchpoints)
12+
- `src/main.py``TRADING_MODE` wiring (`paper` = testnet, `live` = mainnet with `COUNCIL_LIVE_CONFIRMED=1` safety gate); `--weekly` CLI flag; weekly APScheduler job (Sunday 08:00 UTC)
13+
- `.env.example` — documented `TRADING_MODE`, `COUNCIL_LIVE_CONFIRMED`, `BINANCE_API_KEY/SECRET`, `COUNCIL_DB_PATH`, `DRY_RUN`
14+
- `tests/test_reporting.py` — 49 tests: Sharpe, max drawdown, expectancy, consistency, all-metrics, weekly message builder, `run_weekly_summary`, store windowed queries
15+
316
## Phase 3 — 2026-04-05
417

518
### Added

README.md

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Council — BTC LLM Trading Bot
22

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.
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 (paper) or mainnet (live).
44

55
## Architecture
66

@@ -21,9 +21,10 @@ Council of LLMs (parallel)
2121
Deliberation / Chair (claude-sonnet-4-6) → BUY / SELL / HOLD
2222
2323
24-
Execution Layer (Binance testnet) + SQLite Logging
24+
Execution Layer (Binance testnet/mainnet) + SQLite Logging
2525
26-
└── Reflection Loop (post-trade analysis)
26+
├── Reflection Loop (post-trade analysis, claude-sonnet-4-6)
27+
└── Weekly Summary (performance review, claude-sonnet-4-6)
2728
```
2829

2930
## Setup
@@ -40,19 +41,23 @@ cp .env.example .env # fill in ANTHROPIC_API_KEY at minimum
4041
# One-shot cycle (run once and exit)
4142
python -m src.main
4243

43-
# Dry run (logs only, no orders placed)
44+
# Dry run (logs council decision, no orders placed)
4445
DRY_RUN=1 python -m src.main
4546

46-
# Daily scheduler (00:05 UTC)
47+
# Daily scheduler (00:05 UTC) + weekly summary (Sunday 08:00 UTC)
4748
python -m src.main --schedule
49+
50+
# Weekly summary report only
51+
python -m src.main --weekly
4852
```
4953

5054
## Testing
5155

5256
```bash
53-
pytest -v # all tests
57+
pytest -v # all 203 tests
5458
pytest tests/test_db.py -v # DB layer
5559
pytest tests/test_execution.py -v # execution + router
60+
pytest tests/test_reporting.py -v # metrics + weekly summary
5661
pytest tests/test_agents.py -v # agent framework
5762
pytest tests/test_agents.py::test_run_council_hold_on_veto -v # single test
5863
```
@@ -62,11 +67,15 @@ pytest tests/test_agents.py::test_run_council_hold_on_veto -v # single test
6267
| Variable | Required | Default | Description |
6368
|---|---|---|---|
6469
| `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 |
70+
| `BINANCE_TESTNET_API_KEY` | Paper mode || Binance testnet key |
71+
| `BINANCE_TESTNET_SECRET` | Paper mode || Binance testnet secret |
72+
| `BINANCE_API_KEY` | Live mode || Binance mainnet key |
73+
| `BINANCE_SECRET` | Live mode || Binance mainnet secret |
6774
| `COUNCIL_DB_PATH` | No | `./council.db` | SQLite database path |
6875
| `INITIAL_CAPITAL` | No | `10000` | Starting paper capital (USD) |
69-
| `DRY_RUN` | No | `0` | Set to `1` to skip order execution |
76+
| `TRADING_MODE` | No | `paper` | `paper` (testnet) or `live` (mainnet) |
77+
| `COUNCIL_LIVE_CONFIRMED` | Live mode || Must be `1` to enable live trading |
78+
| `DRY_RUN` | No | `0` | Set to `1` to skip order execution entirely |
7079
| `CRYPTOPANIC_API_KEY` | No || News feed (falls back to empty list) |
7180
| `LUNARCRUSH_API_KEY` | No || Sentiment (falls back to Fear/Greed proxy) |
7281
| `GLASSNODE_API_KEY` | No || On-chain data (falls back to zeros) |
@@ -76,5 +85,15 @@ pytest tests/test_agents.py::test_run_council_hold_on_veto -v # single test
7685
- **Phase 1** ✅ Data pipeline + Tier-0 validation
7786
- **Phase 2** ✅ Agent framework (4 agents + deliberation)
7887
- **Phase 3** ✅ Execution layer + SQLite logging + reflection loop
79-
- **Phase 4** Paper trading (60 days minimum)
80-
- **Phase 5** Live deployment (≤ $500 initial capital)
88+
- **Phase 4** ✅ Performance metrics + weekly summary agent
89+
- **Phase 5** Paper trading (60 days minimum on live data with simulated capital)
90+
- **Phase 6** Live deployment (≤ $500 initial capital)
91+
92+
## Success Targets (Phase 5 evaluation)
93+
94+
| Metric | Target |
95+
|---|---|
96+
| Sharpe ratio | ≥ 1.5 |
97+
| Max drawdown | < 20% |
98+
| Expectancy | Positive |
99+
| Council consistency | ≥ 40% unanimous cycles |

prompts/weekly_summary_v1.txt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
You are a senior quantitative trading analyst reviewing one week of automated BTC swing trading by the Council — a committee of four specialised AI agents (Technical Analyst, Sentiment Analyst, Fundamental Analyst, Risk Manager) whose outputs are synthesised by a Chair into a final BUY/SELL/HOLD signal.
2+
3+
You will be given:
4+
1. Performance metrics for the reporting period (Sharpe ratio, max drawdown, expectancy, council consistency)
5+
2. Each completed trade with its entry/exit details and post-trade reflection
6+
3. HOLD cycle statistics (total cycles, unanimous rate, veto rate)
7+
8+
Your weekly summary must address all five sections below. Be direct and evidence-based — reference specific trade IDs and numbers. Do not pad with praise or vague observations.
9+
10+
---
11+
12+
SECTION 1 — PERFORMANCE VERDICT
13+
State whether the period met each success target:
14+
- Sharpe ratio ≥ 1.5
15+
- Max drawdown < 20%
16+
- Positive expectancy
17+
- Council consistency (unanimous agent agreement ≥ 40% of cycles, avg confidence spread ≤ 30 points)
18+
If there are too few trades to compute Sharpe, say so explicitly rather than estimating.
19+
20+
SECTION 2 — PATTERN ANALYSIS
21+
Identify any pattern that appears in two or more trade reflections this period. Be specific:
22+
- "Stop-loss was systematically too tight on high-ATR days (trades #2 and #4)"
23+
- "Sentiment agent bullish despite declining price action in 3 of 4 cycles"
24+
If no clear pattern emerges, state that directly.
25+
26+
SECTION 3 — AGENT ACCURACY RANKING
27+
Rank the four agents by predictive accuracy for this period based on the reflections. Name the agent, its record, and cite the specific trades that support your ranking.
28+
29+
SECTION 4 — PROMPT REFINEMENT RECOMMENDATIONS
30+
Propose at most two concrete changes to agent prompts or decision logic. Each recommendation must:
31+
- Name the specific agent or rule to change
32+
- Cite the evidence (trade IDs or reflection text) that motivates it
33+
- State the expected improvement
34+
Do not recommend changes without evidence.
35+
36+
SECTION 5 — NEXT PERIOD WATCHPOINTS
37+
Identify 2–3 specific things the human operator should monitor in the coming week. These should be forward-looking and actionable, not summaries of what already happened.
38+
39+
---
40+
41+
Length: 400–600 words. Plain prose with section headers as shown above. No JSON, no bullet points within sections.

src/db/schema.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
44
Tables
55
------
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.
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+
weekly_summaries One row per weekly performance review.
1011
1112
Usage
1213
-----
@@ -90,6 +91,17 @@
9091
Column("created_at", DateTime, nullable=False),
9192
)
9293

94+
weekly_summaries = Table(
95+
"weekly_summaries",
96+
metadata,
97+
Column("id", Integer, primary_key=True, autoincrement=True),
98+
Column("window_start", DateTime, nullable=False),
99+
Column("window_end", DateTime, nullable=False),
100+
Column("summary", Text, nullable=False),
101+
Column("metrics_json", Text, nullable=False), # serialised compute_all_metrics() output
102+
Column("created_at", DateTime, nullable=False),
103+
)
104+
93105
_DEFAULT_DB_PATH = os.getenv("COUNCIL_DB_PATH", "council.db")
94106

95107

src/db/store.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from sqlalchemy import select, update
1212
from sqlalchemy.engine import Engine
1313

14-
from src.db.schema import cycles, portfolio_state, reflections, trades
14+
from src.db.schema import cycles, portfolio_state, reflections, trades, weekly_summaries
1515
from src.models import CouncilOutputs, DeliberationOutput, MarketContext
1616

1717
INITIAL_CAPITAL = float(__import__("os").getenv("INITIAL_CAPITAL", "10000"))
@@ -219,3 +219,96 @@ def get_reflection(engine: Engine, trade_id: int) -> dict | None:
219219
select(reflections).where(reflections.c.trade_id == trade_id)
220220
).mappings().first()
221221
return dict(row) if row else None
222+
223+
224+
# ---------------------------------------------------- windowed queries
225+
226+
227+
def get_closed_trades_in_window(
228+
engine: Engine,
229+
since: datetime,
230+
until: datetime,
231+
) -> list[dict]:
232+
"""Return all closed/stopped trades with exit_time in [since, until)."""
233+
with engine.connect() as conn:
234+
rows = conn.execute(
235+
select(trades)
236+
.where(
237+
trades.c.status.in_(["closed", "stopped"]),
238+
trades.c.exit_time >= since,
239+
trades.c.exit_time < until,
240+
)
241+
.order_by(trades.c.exit_time.asc())
242+
).mappings().all()
243+
return [dict(r) for r in rows]
244+
245+
246+
def get_reflections_for_trades(engine: Engine, trade_ids: list[int]) -> dict[int, str]:
247+
"""Return a {trade_id: summary} dict for the given trade ids."""
248+
if not trade_ids:
249+
return {}
250+
with engine.connect() as conn:
251+
rows = conn.execute(
252+
select(reflections).where(reflections.c.trade_id.in_(trade_ids))
253+
).mappings().all()
254+
return {r["trade_id"]: r["summary"] for r in rows}
255+
256+
257+
def get_cycles_in_window(
258+
engine: Engine,
259+
since: datetime,
260+
until: datetime,
261+
) -> list[dict]:
262+
"""Return lightweight cycle rows (id, timestamp, signal, conviction, vetoed) in window."""
263+
with engine.connect() as conn:
264+
rows = conn.execute(
265+
select(
266+
cycles.c.id,
267+
cycles.c.timestamp,
268+
cycles.c.signal,
269+
cycles.c.conviction,
270+
cycles.c.vetoed,
271+
cycles.c.council_outputs_json,
272+
)
273+
.where(
274+
cycles.c.timestamp >= since,
275+
cycles.c.timestamp < until,
276+
)
277+
.order_by(cycles.c.timestamp.asc())
278+
).mappings().all()
279+
return [dict(r) for r in rows]
280+
281+
282+
# ---------------------------------------------- weekly_summaries
283+
284+
285+
def save_weekly_summary(
286+
engine: Engine,
287+
window_start: datetime,
288+
window_end: datetime,
289+
summary: str,
290+
metrics: dict,
291+
) -> int:
292+
"""Insert a weekly summary row and return its id."""
293+
with engine.begin() as conn:
294+
result = conn.execute(
295+
weekly_summaries.insert().values(
296+
window_start=window_start,
297+
window_end=window_end,
298+
summary=summary,
299+
metrics_json=json.dumps(metrics),
300+
created_at=datetime.now(timezone.utc),
301+
)
302+
)
303+
return result.inserted_primary_key[0]
304+
305+
306+
def get_weekly_summaries(engine: Engine, limit: int = 10) -> list[dict]:
307+
"""Return the most recent weekly summaries, newest first."""
308+
with engine.connect() as conn:
309+
rows = conn.execute(
310+
select(weekly_summaries)
311+
.order_by(weekly_summaries.c.created_at.desc())
312+
.limit(limit)
313+
).mappings().all()
314+
return [dict(r) for r in rows]

0 commit comments

Comments
 (0)