Snapshot before the DuckDB->PostgreSQL migration. data/, archive/, the 232MB rebuild zip, the Windows .venv, and .env are excluded via .gitignore. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
488 lines
15 KiB
Markdown
488 lines
15 KiB
Markdown
# Phase 2 Preview: What You'll Be Coding
|
|
|
|
This preview shows **exactly** what Phase 2 implementation looks like, so you can see the work upfront.
|
|
|
|
---
|
|
|
|
## Example 1: Instrumenting `ingest_live_minute_bars()`
|
|
|
|
### Before (Phase 1)
|
|
```python
|
|
def ingest_live_minute_bars(alpaca_client, research_db, symbols):
|
|
"""Ingest latest 1-minute bars from Alpaca."""
|
|
for symbol in symbols:
|
|
bars = alpaca_client.get_bars(symbol, lookback_minutes=120)
|
|
# Insert into DB silently...
|
|
research_db.insert_bars(symbol, bars)
|
|
# No telemetry, no validation, silent success/failure
|
|
```
|
|
|
|
### After (Phase 2)
|
|
```python
|
|
from core.telemetry import TelemetryCollector, emit_module_tick, emit_data_validation
|
|
from core.validation import BarDataValidator
|
|
|
|
def ingest_live_minute_bars(alpaca_client, research_db, symbols):
|
|
"""Ingest latest 1-minute bars from Alpaca with full telemetry."""
|
|
|
|
collector = TelemetryCollector("ingestion")
|
|
validation_failures = []
|
|
|
|
try:
|
|
for symbol in symbols:
|
|
try:
|
|
bars = alpaca_client.get_bars(symbol, lookback_minutes=120)
|
|
collector.record_input(len(bars))
|
|
|
|
# Validate each bar before insertion
|
|
for i, bar in enumerate(bars):
|
|
is_valid, failures = BarDataValidator.validate_bar(bar)
|
|
if not is_valid:
|
|
collector.record_error()
|
|
validation_failures.extend(failures)
|
|
|
|
# Insert valid bars
|
|
if not validation_failures:
|
|
research_db.insert_bars(symbol, bars)
|
|
collector.record_output(len(bars))
|
|
collector.record_metric(f"{symbol}_volume_avg",
|
|
sum(b['volume'] for b in bars) / len(bars))
|
|
|
|
except Exception as e:
|
|
collector.record_error()
|
|
collector.record_warning(f"{symbol} failed: {e}")
|
|
|
|
# Emit validation checkpoint
|
|
emit_data_validation(
|
|
module_name="ingestion",
|
|
validation_type="schema",
|
|
valid=len([f for f in validation_failures if f['severity'] == 'error']) == 0,
|
|
details={
|
|
"rows_validated": collector.input_count,
|
|
"validation_failures": validation_failures,
|
|
"symbols_processed": len(symbols)
|
|
}
|
|
)
|
|
|
|
# Emit completion telemetry
|
|
metrics = collector.finalize()
|
|
emit_module_tick(
|
|
module_name="ingestion",
|
|
stage="completed",
|
|
metrics=metrics
|
|
)
|
|
|
|
except Exception as e:
|
|
collector.record_error()
|
|
metrics = collector.finalize()
|
|
emit_module_tick(
|
|
module_name="ingestion",
|
|
stage="failed",
|
|
metrics=metrics,
|
|
errors=[{"message": str(e)}]
|
|
)
|
|
raise
|
|
```
|
|
|
|
**What changed:**
|
|
- ✅ Wrapped with `TelemetryCollector`
|
|
- ✅ Validate bars before insertion
|
|
- ✅ Record input/output counts
|
|
- ✅ Track custom metrics (volume averages)
|
|
- ✅ Emit validation + completion events
|
|
- ✅ Error handling with telemetry
|
|
|
|
**You'll see in event store:**
|
|
```json
|
|
{
|
|
"type": "module_tick",
|
|
"module": "ingestion",
|
|
"stage": "completed",
|
|
"duration_ms": 245,
|
|
"input_count": 500,
|
|
"output_count": 500,
|
|
"metrics": {
|
|
"AAPL_volume_avg": 125000,
|
|
"TSLA_volume_avg": 98000,
|
|
"AMD_volume_avg": 75000
|
|
},
|
|
"errors": []
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Example 2: Instrumenting `evaluate_setup()`
|
|
|
|
### Before (Phase 1)
|
|
```python
|
|
def evaluate_setup(bars, symbol, previous_close, avg_daily_volume):
|
|
"""Evaluate setup criteria."""
|
|
|
|
gap_pct = ((bars[-1]['open'] - previous_close) / previous_close) * 100
|
|
rvol = bars[-1]['volume'] / avg_daily_volume
|
|
|
|
# ... evaluate 7 more criteria silently ...
|
|
|
|
criteria_passed = sum([gap_pct >= 2.0, rvol >= 1.5, ...])
|
|
score = (criteria_passed / 9) * 100
|
|
|
|
return {
|
|
'status': 'ready' if score >= 60 else 'blocked',
|
|
'score': score,
|
|
# ... hidden metrics ...
|
|
}
|
|
# User never sees WHY it passed/failed
|
|
```
|
|
|
|
### After (Phase 2)
|
|
```python
|
|
from core.telemetry import (
|
|
TelemetryCollector,
|
|
emit_module_tick,
|
|
emit_symbol_evaluation_detail
|
|
)
|
|
from core.validation import RangeValidator
|
|
|
|
def evaluate_setup(bars, symbol, previous_close, avg_daily_volume):
|
|
"""Evaluate setup criteria with full telemetry."""
|
|
|
|
collector = TelemetryCollector("evaluation")
|
|
criteria_results = []
|
|
|
|
try:
|
|
collector.record_input(1) # 1 symbol evaluated
|
|
|
|
# STAGE 1: Gap check
|
|
gap_pct = ((bars[-1]['open'] - previous_close) / previous_close) * 100
|
|
gap_passed = gap_pct >= 2.0
|
|
|
|
emit_symbol_evaluation_detail(
|
|
symbol=symbol,
|
|
stage="gap_checked",
|
|
metrics={"gap_pct": gap_pct, "threshold": 2.0},
|
|
passed=gap_passed,
|
|
blocking_reason=None if gap_passed else f"Gap {gap_pct:.1f}% < 2.0%"
|
|
)
|
|
collector.record_metric("gap_pct", gap_pct)
|
|
criteria_results.append(("gap", gap_passed, gap_pct))
|
|
|
|
# STAGE 2: RVOL check
|
|
current_5min_vol = bars[-1]['volume']
|
|
rvol = current_5min_vol / avg_daily_volume
|
|
rvol_passed = rvol >= 1.5
|
|
|
|
emit_symbol_evaluation_detail(
|
|
symbol=symbol,
|
|
stage="rvol_calculated",
|
|
metrics={
|
|
"rvol": rvol,
|
|
"threshold": 1.5,
|
|
"current_5min_vol": current_5min_vol,
|
|
"avg_20day_vol": avg_daily_volume
|
|
},
|
|
passed=rvol_passed,
|
|
blocking_reason=None if rvol_passed else f"RVOL {rvol:.2f}x < 1.5x"
|
|
)
|
|
collector.record_metric("rvol", rvol)
|
|
criteria_results.append(("rvol", rvol_passed, rvol))
|
|
|
|
# STAGE 3: Bull flag detection
|
|
bull_flag_score, bull_flag_details = detect_bull_flag(bars)
|
|
bull_flag_passed = bull_flag_score >= 0.5
|
|
|
|
emit_symbol_evaluation_detail(
|
|
symbol=symbol,
|
|
stage="bull_flag_detected",
|
|
metrics={
|
|
"confidence": bull_flag_score,
|
|
"threshold": 0.5,
|
|
"details": bull_flag_details
|
|
},
|
|
passed=bull_flag_passed,
|
|
blocking_reason=None if bull_flag_passed else f"Bull flag confidence {bull_flag_score:.2f} < 0.5"
|
|
)
|
|
collector.record_metric("bull_flag_confidence", bull_flag_score)
|
|
criteria_results.append(("bull_flag", bull_flag_passed, bull_flag_score))
|
|
|
|
# ... evaluate 6 more criteria, each with emit_symbol_evaluation_detail ...
|
|
|
|
# Calculate final score
|
|
criteria_passed = sum(1 for _, passed, _ in criteria_results if passed)
|
|
total_criteria = len(criteria_results)
|
|
score = (criteria_passed / total_criteria) * 100
|
|
quality = bull_flag_score * (rvol / 1.5) # Custom quality metric
|
|
|
|
emit_symbol_evaluation_detail(
|
|
symbol=symbol,
|
|
stage="score_calculated",
|
|
metrics={
|
|
"score": score,
|
|
"criteria_passed": criteria_passed,
|
|
"total_criteria": total_criteria,
|
|
"quality": quality,
|
|
"threshold": 60
|
|
},
|
|
passed=score >= 60,
|
|
blocking_reason=None if score >= 60 else f"Score {score:.1f}% < 60%"
|
|
)
|
|
collector.record_metric("score", score)
|
|
collector.record_metric("quality", quality)
|
|
|
|
# Determine status
|
|
status = "ready" if score >= 60 else "blocked"
|
|
if status == "ready":
|
|
collector.record_output(1)
|
|
|
|
# Entry/stop/target calculation
|
|
entry_price = bars[-1]['close']
|
|
stop_price = bars.min()['low'] * 0.99
|
|
target_price = entry_price + (entry_price - stop_price) * 2.0
|
|
|
|
emit_symbol_evaluation_detail(
|
|
symbol=symbol,
|
|
stage="levels_calculated",
|
|
metrics={
|
|
"entry": entry_price,
|
|
"stop": stop_price,
|
|
"target": target_price,
|
|
"risk": entry_price - stop_price,
|
|
"reward": target_price - entry_price,
|
|
"reward_ratio": (target_price - entry_price) / (entry_price - stop_price)
|
|
},
|
|
passed=True
|
|
)
|
|
|
|
# Finalize telemetry
|
|
metrics = collector.finalize()
|
|
emit_module_tick(
|
|
module_name="evaluation",
|
|
stage="completed",
|
|
metrics=metrics
|
|
)
|
|
|
|
return {
|
|
'status': status,
|
|
'score': score,
|
|
'criteria': [
|
|
{'name': name, 'passed': passed, 'value': value}
|
|
for name, passed, value in criteria_results
|
|
],
|
|
'entry': entry_price,
|
|
'stop': stop_price,
|
|
'target': target_price,
|
|
'quality': quality
|
|
}
|
|
|
|
except Exception as e:
|
|
collector.record_error()
|
|
metrics = collector.finalize()
|
|
emit_module_tick(
|
|
module_name="evaluation",
|
|
stage="failed",
|
|
metrics=metrics,
|
|
errors=[{"symbol": symbol, "message": str(e)}]
|
|
)
|
|
raise
|
|
```
|
|
|
|
**What you'll see in event store:**
|
|
|
|
```json
|
|
[
|
|
{
|
|
"type": "symbol_evaluation_detail",
|
|
"symbol": "AAPL",
|
|
"stage": "gap_checked",
|
|
"metrics": {"gap_pct": 2.1, "threshold": 2.0},
|
|
"passed": true
|
|
},
|
|
{
|
|
"type": "symbol_evaluation_detail",
|
|
"symbol": "AAPL",
|
|
"stage": "rvol_calculated",
|
|
"metrics": {"rvol": 1.2, "threshold": 1.5, "current_5min_vol": 85000},
|
|
"passed": false,
|
|
"blocking_reason": "RVOL 1.20x < 1.5x"
|
|
},
|
|
{
|
|
"type": "symbol_evaluation_detail",
|
|
"symbol": "AAPL",
|
|
"stage": "bull_flag_detected",
|
|
"metrics": {"confidence": 0.75, "threshold": 0.5},
|
|
"passed": true
|
|
},
|
|
{
|
|
"type": "symbol_evaluation_detail",
|
|
"symbol": "AAPL",
|
|
"stage": "score_calculated",
|
|
"metrics": {"score": 48.0, "criteria_passed": 4, "total_criteria": 9, "threshold": 60},
|
|
"passed": false,
|
|
"blocking_reason": "Score 48.0% < 60%"
|
|
},
|
|
{
|
|
"type": "module_tick",
|
|
"module": "evaluation",
|
|
"stage": "completed",
|
|
"duration_ms": 1823,
|
|
"input_count": 5,
|
|
"output_count": 2,
|
|
"metrics": {
|
|
"avg_score": 58.3,
|
|
"ready_count": 2,
|
|
"blocked_count": 3
|
|
}
|
|
}
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## Example 3: Updating `run_live_paper.py`
|
|
|
|
### Small changes needed
|
|
```python
|
|
from core.telemetry import TelemetryEmitter, set_telemetry_emitter
|
|
from storage.event_store import EventStore
|
|
|
|
def build_runtime(args: argparse.Namespace) -> dict:
|
|
"""Wire up every component; safe without API keys."""
|
|
|
|
# ... existing code ...
|
|
|
|
event_store = EventStore()
|
|
|
|
# NEW: Initialize telemetry emitter
|
|
emitter = TelemetryEmitter(event_store=event_store)
|
|
set_telemetry_emitter(emitter) # Make it global
|
|
|
|
# ... rest of existing code ...
|
|
|
|
return {
|
|
'event_store': event_store,
|
|
'emitter': emitter, # NEW
|
|
# ... other components ...
|
|
}
|
|
```
|
|
|
|
That's it! The modules will use the global emitter automatically.
|
|
|
|
---
|
|
|
|
## Example 4: Instrumenting `TradingExecutionService`
|
|
|
|
```python
|
|
def tick(self):
|
|
"""Execute one tick with telemetry."""
|
|
|
|
collector = TelemetryCollector("execution")
|
|
|
|
# Check for new signal_ready events
|
|
ready_signals = query_ready_signals_snapshot(self.store)
|
|
collector.record_input(len(ready_signals))
|
|
|
|
for signal in ready_signals:
|
|
try:
|
|
# Risk checks
|
|
open_pos = self._open_position_count()
|
|
if open_pos >= self.settings.max_concurrent_positions:
|
|
emit_symbol_evaluation_detail(
|
|
symbol=signal.symbol,
|
|
stage="risk_check_max_positions",
|
|
metrics={"open_positions": open_pos, "max": self.settings.max_concurrent_positions},
|
|
passed=False,
|
|
blocking_reason=f"Max positions {open_pos} >= limit {self.settings.max_concurrent_positions}"
|
|
)
|
|
continue
|
|
|
|
# Position sizing
|
|
shares = calculate_position_size(
|
|
entry=signal.entry,
|
|
stop=signal.stop,
|
|
equity=self.equity,
|
|
risk_pct=self.settings.risk_per_trade_pct
|
|
)
|
|
|
|
# Emit approval request
|
|
self.store.emit(OrderApprovalRequestedEvent(...))
|
|
collector.record_output(1)
|
|
|
|
except Exception as e:
|
|
collector.record_error()
|
|
|
|
metrics = collector.finalize()
|
|
emit_module_tick(
|
|
module_name="execution",
|
|
stage="completed",
|
|
metrics=metrics
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Time Breakdown for Phase 2
|
|
|
|
| Task | Time |
|
|
|------|------|
|
|
| Instrument ingestion module | 30 mins |
|
|
| Instrument research module | 30 mins |
|
|
| Instrument evaluation module | 60 mins (most complex) |
|
|
| Instrument execution module | 45 mins |
|
|
| Instrument broker sync module | 30 mins |
|
|
| Update run_live_paper.py | 15 mins |
|
|
| Test & verify | 45 mins |
|
|
| **Total** | **~4 hours** |
|
|
|
|
---
|
|
|
|
## Verification Checklist for Phase 2
|
|
|
|
After completing Phase 2, you should be able to:
|
|
|
|
```bash
|
|
# Run the system
|
|
python run_live_paper.py --once
|
|
|
|
# Check event store has new events
|
|
sqlite3 data/momentum_events.duckdb "SELECT COUNT(*), event_type FROM events GROUP BY event_type;"
|
|
|
|
# Expected output:
|
|
# 5 | module_tick
|
|
# 3 | data_validation
|
|
# 15 | symbol_evaluation_detail
|
|
# ... (plus existing event types)
|
|
|
|
# View a symbol's complete journey
|
|
sqlite3 data/momentum_events.duckdb \
|
|
"SELECT event_type, symbol, stage, passed FROM events \
|
|
WHERE symbol='AAPL' AND event_type IN ('symbol_evaluation_detail', 'data_validation') \
|
|
ORDER BY timestamp;"
|
|
|
|
# Expected output:
|
|
# symbol_evaluation_detail | AAPL | gap_checked | 1
|
|
# symbol_evaluation_detail | AAPL | rvol_calculated | 0
|
|
# symbol_evaluation_detail | AAPL | bull_flag_detected | 1
|
|
# ...
|
|
```
|
|
|
|
---
|
|
|
|
## Key Points
|
|
|
|
1. **Minimal changes to existing code** — mostly wrapping with telemetry
|
|
2. **No breaking changes** — all existing functionality preserved
|
|
3. **Backward compatible** — code still works without telemetry
|
|
4. **Testable** — each instrumented module still works alone
|
|
5. **Progressive** — instrument one module at a time, test, then move to next
|
|
|
|
---
|
|
|
|
## What Phase 3 Needs
|
|
|
|
Once Phase 2 is done, Phase 3 will:
|
|
- Query the `symbol_evaluation_detail` events for symbol inspector
|
|
- Query `module_tick` events for module performance grid
|
|
- Query `data_validation` events for quality report
|
|
|
|
Everything needed will be in the event store already!
|