diff options
| -rw-r--r-- | cli/src/trading_cli/main.py | 2 | ||||
| -rw-r--r-- | docs/TODO.md | 296 | ||||
| -rw-r--r-- | pyproject.toml | 2 | ||||
| -rwxr-xr-x | scripts/backtest_moc.py | 246 | ||||
| -rw-r--r-- | services/api/tests/test_portfolio_router.py | 4 | ||||
| -rw-r--r-- | services/backtester/src/backtester/config.py | 2 | ||||
| -rw-r--r-- | services/backtester/tests/test_walk_forward.py | 2 | ||||
| -rw-r--r-- | services/data-collector/tests/test_storage.py | 6 | ||||
| -rw-r--r-- | services/order-executor/tests/test_risk_manager.py | 46 | ||||
| -rw-r--r-- | services/portfolio-manager/tests/test_portfolio.py | 24 | ||||
| -rw-r--r-- | services/portfolio-manager/tests/test_snapshot.py | 2 | ||||
| -rw-r--r-- | services/strategy-engine/src/strategy_engine/config.py | 2 | ||||
| -rw-r--r-- | shared/tests/test_broker.py | 6 | ||||
| -rw-r--r-- | shared/tests/test_notifier.py | 12 |
14 files changed, 422 insertions, 230 deletions
diff --git a/cli/src/trading_cli/main.py b/cli/src/trading_cli/main.py index db3c282..1129bdd 100644 --- a/cli/src/trading_cli/main.py +++ b/cli/src/trading_cli/main.py @@ -10,7 +10,7 @@ from trading_cli.commands.service import service @click.group() @click.version_option(version="0.1.0") def cli(): - """Trading Platform CLI — Binance spot crypto trading""" + """Trading Platform CLI — US stock trading""" pass diff --git a/docs/TODO.md b/docs/TODO.md index d737b0f..fd4f7eb 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,201 +1,147 @@ -# Trading Platform — TODO +# US Stock Trading Platform — TODO -> Last updated: 2026-04-01 +> Last updated: 2026-04-02 ## Current State -- **298 tests**, lint clean, production-ready 인프라 -- 8 strategies, 6 services, full monitoring/CI/CD stack -- **트레이딩 전략 업그레이드 필요** — 아래 상세 +- **375 tests**, lint clean +- **US 주식 전용** (Alpaca API, 수수료 0%) +- 6 microservices + CLI + shared library +- MOC (Market on Close) 전략 + 기술적 전략 7개 +- Prometheus/Grafana/Loki 모니터링, CI/CD, Telegram 알림 +- Claude 기반 종목 스크리너 --- -## Trading Strategy Upgrade Plan - -### Phase 1: 백테스터 현실화 (최우선) - -현재 백테스트 결과는 슬리피지/수수료 미포함으로 **실제 수익과 큰 차이**가 있음. 이것부터 수정해야 전략 개선의 효과를 정확히 측정 가능. - -#### 1-1. 슬리피지 + 수수료 모델링 -- **파일:** `backtester/simulator.py` -- **현재:** 시그널 가격 그대로 체결, 수수료 0 -- **수정:** 매수 시 `price * (1 + slippage_pct)`, 매도 시 `price * (1 - slippage_pct)` -- 수수료: `cost = price * quantity * fee_pct` (maker: 0.05%, taker: 0.1%) -- 슬리피지를 주문 크기에 비례하게 (대형 주문 → 더 큰 슬리피지) -- **설정:** `BacktestConfig`에 `slippage_pct`, `maker_fee_pct`, `taker_fee_pct` 추가 - -#### 1-2. 손절/익절 자동 실행 -- **파일:** `backtester/simulator.py` -- **현재:** 시그널로만 매매, 스탑 없음 -- **수정:** 각 포지션에 stop_loss, take_profit 가격 추적 -- 매 캔들마다 `high >= take_profit` 또는 `low <= stop_loss` 체크 → 자동 청산 -- `engine.py`에서 캔들 처리 시 시뮬레이터에 현재 가격 전달 - -#### 1-3. 공매도 지원 -- **파일:** `backtester/simulator.py` -- **현재:** 매도는 보유 수량 내에서만 가능 -- **수정:** `allow_short: bool` 설정, 공매도 시 음수 포지션 허용 -- 공매도 수수료 (borrow fee) 추가 - -#### 1-4. Walk-Forward Analysis -- **파일:** `backtester/engine.py` (신규 클래스) -- **현재:** 전체 데이터로 백테스트 → 과적합 위험 -- **수정:** `WalkForwardEngine` 클래스 - - 데이터를 N개 구간으로 분할 - - 각 구간: in-sample (파라미터 최적화) → out-of-sample (검증) - - 최종 결과는 out-of-sample 구간만 합산 -- 파라미터 최적화: grid search 또는 random search - -#### 1-5. 메트릭 정확도 개선 -- **파일:** `backtester/metrics.py` -- Sharpe/Sortino를 per-trade가 아닌 **일별 수익률 기반**으로 계산 -- Risk-free rate 설정 추가 (기본 5%) -- Recovery Factor (총수익 / 최대 drawdown) 추가 -- 최대 연속 손실 횟수 추가 -- 인트라 트레이드 drawdown (진입 후 최저점) 계산 +## Architecture ---- +``` +Alpaca API → data-collector (REST polling) + → Redis Streams → strategy-engine (MOC + 기술전략) + → signals → order-executor (Alpaca 주문) + → orders → portfolio-manager (포지션 추적) -### Phase 2: 전략 공통 인프라 - -모든 전략에 적용할 공통 기능. 개별 전략 개선 전에 인프라를 먼저 구축. - -#### 2-1. ATR 기반 동적 손절/익절 -- **파일:** `strategies/base.py` + 각 전략 -- **현재:** 어떤 전략도 손절/익절을 설정하지 않음 -- **수정:** `BaseStrategy`에 `calculate_stop_loss(candle, atr)` 메서드 추가 -- ATR (Average True Range) 유틸리티 함수 (`shared/` 또는 `strategies/indicators/`) -- 손절: entry - ATR * multiplier, 익절: entry + ATR * reward_ratio -- Signal에 `stop_loss`, `take_profit` 필드 추가 (`shared/models.py`) - -#### 2-2. 추세/횡보 레짐 필터 (ADX) -- **파일:** `strategies/indicators/adx.py` (신규) -- ADX (Average Directional Index) 계산 유틸리티 -- ADX > 25 = 추세장, ADX < 20 = 횡보장 -- 각 전략이 레짐에 따라 동작 변경: - - 추세 추종 전략 (MACD, EMA): ADX < 20이면 시그널 무시 - - 평균 회귀 전략 (RSI, Bollinger, Grid): ADX > 30이면 시그널 무시 - -#### 2-3. 볼륨 확인 필터 -- **파일:** 각 전략 -- **현재:** 모든 전략이 볼륨을 무시 (RSI, MACD, EMA 등) -- **수정:** 시그널 발생 시 해당 캔들의 볼륨이 최근 N개 평균 대비 일정 비율 이상인지 확인 -- 볼륨 < 평균의 50%면 시그널 무시 (유동성 부족) -- 볼륨 > 평균의 200%면 시그널 가중치 증가 - -#### 2-4. 시그널 강도 (Conviction Score) -- **파일:** `shared/models.py` Signal + 각 전략 -- **현재:** 시그널은 BUY/SELL/quantity만 있음 -- **수정:** Signal에 `conviction: float` (0.0~1.0) 필드 추가 - - RSI 5 → conviction 0.9, RSI 28 → conviction 0.3 - - MACD 히스토그램이 0에서 먼 크로스 → 높은 conviction -- Combined 전략에서 conviction 기반 가중치 사용 -- RiskManager에서 conviction 기반 포지션 사이징 - -#### 2-5. 지표 라이브러리 -- **디렉토리:** `strategies/indicators/` (신규) -- 재사용 가능한 기술 지표 함수: - - `atr(highs, lows, closes, period)` — Average True Range - - `adx(highs, lows, closes, period)` — Average Directional Index - - `ema(series, period)` — Exponential Moving Average - - `sma(series, period)` — Simple Moving Average - - `rsi(closes, period)` — RSI - - `bollinger_bands(closes, period, num_std)` — Bollinger - - `macd(closes, fast, slow, signal)` — MACD - - `volume_sma(volumes, period)` — Volume SMA -- 각 전략에서 직접 계산하지 않고 공통 라이브러리 사용 -- 테스트: 각 지표에 대한 unit test +Claude API → stock_screener.py (종목 분석/추천) +FastAPI → REST API (/api/v1/portfolio, orders, strategies) +``` ---- +## 핵심 매매 전략: MOC (Market on Close) -### Phase 3: 개별 전략 고도화 - -Phase 1-2 완료 후 각 전략을 전문가 수준으로 업그레이드. - -#### 3-1. RSI 전략 개선 -- [ ] RSI 다이버전스 감지 (가격 신고가 + RSI 하락 = 약세 다이버전스) -- [ ] ADX 레짐 필터 적용 (추세장에서는 RSI 매수 신호 무시) -- [ ] RSI 강도별 conviction score (RSI 5 vs RSI 28) -- [ ] ATR 기반 손절/익절 -- [ ] 볼륨 확인 필터 - -#### 3-2. MACD 전략 개선 -- [ ] 히스토그램 크로스오버 + MACD 제로라인 크로스오버 구분 -- [ ] MACD 다이버전스 감지 -- [ ] ADX 추세 확인 (ADX < 20이면 시그널 무시) -- [ ] 제로라인으로부터 거리 기반 시그널 강도 -- [ ] ATR 기반 손절 - -#### 3-3. Grid 전략 개선 -- [ ] ADX 기반 레짐 필터 (추세장 진입 차단) -- [ ] 동적 그리드 재설정 (실현 변동성 기반 범위 조정) -- [ ] 그리드 외 이탈 시 전 포지션 청산 + 알림 -- [ ] 볼륨 프로파일 기반 비균등 그리드 간격 - -#### 3-4. Bollinger Bands 전략 개선 -- [ ] 스퀴즈 감지 (밴드 압축 → 브레이크아웃 대비) -- [ ] %B 지표 활용 (밴드 내 위치 0~1) -- [ ] RSI 확인 (하단 밴드 터치 + RSI < 30 = 강한 매수) -- [ ] 볼륨 스파이크 확인 - -#### 3-5. EMA Crossover 전략 개선 -- [ ] ADX > 25 필터 (강한 추세만 진입) -- [ ] 풀백 진입 (크로스 후 단기 EMA로 되돌림 시 진입) -- [ ] 50 SMA 위/아래 필터 (장기 추세 방향 확인) -- [ ] 볼륨 확인 - -#### 3-6. VWAP 전략 개선 -- [ ] 일중 리셋 (매일 00:00 UTC에 VWAP 재계산) -- [ ] VWAP 표준편차 밴드 추가 (1σ, 2σ) -- [ ] ATR 기반 deviation threshold (고정값 대신 변동성 적응형) -- [ ] 세션 필터 (저유동성 시간대 진입 차단) - -#### 3-7. Volume Profile 전략 개선 -- [ ] HVN/LVN (고/저볼륨 노드) 식별 -- [ ] 세션 기반 프로파일 리셋 -- [ ] POC를 동적 지지/저항선으로 활용 -- [ ] 볼륨 델타 (매수량 - 매도량) 추적 - -#### 3-8. Combined 전략 개선 -- [ ] Sub-strategy conviction score 반영 -- [ ] Sub-strategy 간 상관관계 행렬 계산 → 중복 시그널 감쇄 -- [ ] 적응형 가중치 (최근 win rate 기반 동적 가중치 조정) -- [ ] 포트폴리오 집중도 제한 +``` +[매일 ET 15:50] Claude 종목 분석 → 매수 종목 선정 +[ET 15:50~16:00] 장 마감 직전 매수 (MOC 주문) +[다음날 ET 9:35~10:00] 시가 매도 + +조건: 양봉 + 볼륨 > 평균 + RSI 30~60 + EMA 위 + 모멘텀 양호 +손절: -2%, 포지션당 자본금 20%, 최대 5종목 +``` --- -### Phase 4: 리스크 관리 고도화 +## Remaining Work + +### 즉시 해야 할 것 -#### 4-1. 포트폴리오 레벨 리스크 -- [ ] 전체 노출도 제한 (총 포지션 가치 / 잔고 비율) -- [ ] 포지션 간 상관관계 계산 → 실효 리스크 산출 -- [ ] VaR (Value at Risk) 계산 — 95% 신뢰 구간 +#### 1. MOC 백테스트 스크립트 +- [ ] `scripts/backtest_moc.py` — 합성 데이터로 MOC 전략 파라미터 최적화 +- [ ] 5개 주식 (AAPL, MSFT, TSLA, NVDA, AMZN) 대상 90일 백테스트 +- [ ] RSI 범위, 손절률, EMA period 그리드 서치 +- [ ] Makefile target: `make backtest-moc` -#### 4-2. 동적 포지션 축소 -- [ ] Drawdown이 일정 수준 넘으면 포지션 크기 자동 축소 -- [ ] 연속 손실 N회 시 거래 일시 중단 → Telegram 알림 -- [ ] 시간대별 리스크 조정 (주말, 공휴일 축소) +#### 2. 실제 데이터 백테스트 +- [ ] Alpaca API로 과거 데이터 다운로드 → DB 저장 +- [ ] 실제 데이터 기반 MOC 전략 검증 +- [ ] Walk-forward analysis로 과적합 확인 -#### 4-3. 시나리오 분석 -- [ ] 과거 극단 이벤트 (FTX 사태, Luna 등)에 대한 포트폴리오 영향 시뮬레이션 -- [ ] 유동성 리스크 체크 (주문 크기 vs 호가창 깊이) +#### 3. Paper Trading 배포 +- [ ] `.env` 설정 (ALPACA_PAPER=true) +- [ ] `make up` → 전 서비스 실행 +- [ ] 2-4주 모의 매매 → 실제 성과 확인 +- [ ] Telegram 알림으로 매일 결과 수신 --- -## Priority & Effort +### 개선 사항 -| Phase | 내용 | 예상 작업량 | 영향 | -|-------|------|------------|------| -| **Phase 1** | 백테스터 현실화 | 1-2일 | **최대** — 이것 없이는 전략 평가 불가 | -| **Phase 2** | 전략 공통 인프라 | 1-2일 | **높음** — 모든 전략의 기반 | -| **Phase 3** | 개별 전략 고도화 | 3-5일 | 중간 — 수익률 직접 개선 | -| **Phase 4** | 리스크 관리 고도화 | 2-3일 | 높음 — 손실 방지 | +#### 4. Claude 스크리너 고도화 +- [ ] SEC 공시 분석 (10-K, 10-Q, 8-K) +- [ ] 실적 서프라이즈 감지 (EPS beat/miss) +- [ ] 섹터 로테이션 분석 +- [ ] 뉴스 감성 분석 (Yahoo Finance, MarketWatch) -**권장 순서: Phase 1 → Phase 2 → Phase 4 → Phase 3** -(전략 개선보다 리스크 관리가 더 중요 — 돈을 벌기 전에 잃지 않는 게 먼저) +#### 5. 주문 유형 확장 +- [ ] Limit order 지원 +- [ ] 프리마켓/애프터마켓 주문 +- [ ] 분할 매수/매도 + +#### 6. 추가 전략 +- [ ] ORB (Opening Range Breakout) — 장 시작 30분 전략 +- [ ] Gap & Go — 갭 상승 종목 추격 전략 +- [ ] Earnings Play — 실적 발표 전후 전략 + +#### 7. 리스크 관리 개선 +- [ ] 섹터 집중도 제한 (같은 섹터 3개 이상 금지) +- [ ] 실적 발표일 매매 회피 +- [ ] 시장 전체 하락 시 매매 중단 (SPY RSI 기반) --- -## Previously Completed (Infrastructure) +## Quick Start + +```bash +# 1. 환경 설정 +cp .env.example .env +# ALPACA_API_KEY, ALPACA_API_SECRET 입력 +# ANTHROPIC_API_KEY 입력 (Claude 스크리너용) + +# 2. 의존성 설치 +pip install -e shared/ + +# 3. 인프라 실행 +make infra # Redis + PostgreSQL +make migrate # DB 마이그레이션 + +# 4. 테스트 +make test # 375 tests + +# 5. 종목 스크리닝 +make screen # Claude가 33개 종목 분석 → Top 5 추천 + +# 6. 서비스 실행 +make up # 전 서비스 시작 (paper trading) + +# 7. 모니터링 +docker compose --profile monitoring up -d +# Grafana: http://localhost:3000 +# API: http://localhost:8000/api/v1/strategies + +# 8. CLI +trading strategy list +trading backtest run --strategy moc --symbol AAPL --timeframe 1Day +trading portfolio show +``` + +--- -모든 인프라 항목 완료 (27개): SQLAlchemy, Alembic, structlog, Telegram, Prometheus/Grafana/Loki, CI/CD, FastAPI, multi-exchange, Redis consumer groups, realized PnL, bearer auth, 298 tests. +## Completed (Infrastructure) + +- [x] Alpaca API 클라이언트 (paper + live) +- [x] MOC 전략 (종가 매수 / 시가 매도) +- [x] Claude 종목 스크리너 (33개 유니버스) +- [x] Data collector (Alpaca REST polling) +- [x] Order executor (Alpaca submit_order) +- [x] SQLAlchemy ORM + Alembic 마이그레이션 +- [x] structlog 구조화 로깅 +- [x] Telegram 알림 +- [x] Retry + Circuit Breaker +- [x] Prometheus + Grafana + Loki +- [x] Redis consumer groups +- [x] Portfolio snapshots + realized PnL +- [x] Bearer token auth +- [x] CI/CD (Gitea Actions) +- [x] E2E test script +- [x] FastAPI REST API +- [x] 백테스터 (슬리피지, 수수료, SL/TP, 공매도, walk-forward) +- [x] 기술 지표 라이브러리 (ATR, ADX, RSI, MACD, Bollinger, Stochastic, OBV) +- [x] 포트폴리오 VaR, 상관관계, drawdown 기반 리스크 +- [x] 375 tests, lint clean diff --git a/pyproject.toml b/pyproject.toml index 545eae1..6938778 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "trading-platform" version = "0.1.0" -description = "Binance spot crypto trading platform" +description = "US stock trading platform" requires-python = ">=3.12" [tool.pytest.ini_options] diff --git a/scripts/backtest_moc.py b/scripts/backtest_moc.py new file mode 100755 index 0000000..92b426b --- /dev/null +++ b/scripts/backtest_moc.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +"""Backtest and optimize MOC strategy on synthetic US stock data. + +Usage: python scripts/backtest_moc.py +""" + +import sys +import random +from pathlib import Path +from decimal import Decimal +from datetime import datetime, timedelta, timezone + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "services" / "strategy-engine" / "src")) +sys.path.insert(0, str(ROOT / "services" / "strategy-engine")) +sys.path.insert(0, str(ROOT / "services" / "backtester" / "src")) +sys.path.insert(0, str(ROOT / "shared" / "src")) + +from shared.models import Candle # noqa: E402 +from backtester.engine import BacktestEngine # noqa: E402 +from strategies.moc_strategy import MocStrategy # noqa: E402 + + +def generate_stock_candles( + symbol: str = "AAPL", + days: int = 90, + base_price: float = 180.0, + daily_drift: float = 0.0003, # Slight upward bias + daily_vol: float = 0.015, # 1.5% daily vol +) -> list[Candle]: + """Generate realistic US stock intraday candles (5-min bars). + + Simulates: + - Market hours only (14:30-21:00 UTC = 9:30-16:00 ET) + - Opening gaps (overnight news effect) + - Intraday volatility pattern (higher at open/close) + - Volume pattern (U-shaped: high at open, low midday, high at close) + """ + candles = [] + price = base_price + start_date = datetime(2025, 1, 2, tzinfo=timezone.utc) # Start on a Thursday + + trading_day = 0 + current_date = start_date + + while trading_day < days: + # Skip weekends + if current_date.weekday() >= 5: + current_date += timedelta(days=1) + continue + + # Opening gap: overnight news effect + gap_pct = random.gauss(daily_drift, daily_vol * 0.5) # Gap is ~50% of daily vol + price *= 1 + gap_pct + + # Generate 78 5-minute bars (9:30-16:00 = 6.5 hours = 78 bars) + intraday_bars = 78 + for bar in range(intraday_bars): + # Time: 14:30 UTC + bar * 5 minutes + dt = current_date.replace(hour=14, minute=30) + timedelta(minutes=bar * 5) + + # Intraday volatility pattern (U-shaped) + hour_of_day = bar / intraday_bars + if hour_of_day < 0.1: # First 10% of day (opening) + vol = daily_vol * 0.003 + elif hour_of_day > 0.9: # Last 10% (closing) + vol = daily_vol * 0.0025 + else: # Middle of day + vol = daily_vol * 0.001 + + # Add daily trend component + intraday_drift = daily_drift / intraday_bars + change = random.gauss(intraday_drift, vol) + + open_p = price + close_p = price * (1 + change) + high_p = max(open_p, close_p) * (1 + abs(random.gauss(0, vol * 0.3))) + low_p = min(open_p, close_p) * (1 - abs(random.gauss(0, vol * 0.3))) + + # Volume pattern (U-shaped) + if hour_of_day < 0.1 or hour_of_day > 0.85: + volume = random.uniform(500000, 2000000) + else: + volume = random.uniform(100000, 500000) + + candles.append( + Candle( + symbol=symbol, + timeframe="5Min", + open_time=dt, + open=Decimal(str(round(open_p, 2))), + high=Decimal(str(round(high_p, 2))), + low=Decimal(str(round(low_p, 2))), + close=Decimal(str(round(close_p, 2))), + volume=Decimal(str(int(volume))), + ) + ) + + price = close_p + + trading_day += 1 + current_date += timedelta(days=1) + + return candles + + +def run_backtest(candles, params, balance=750.0): + """Run a single backtest.""" + strategy = MocStrategy() + strategy.configure(params) + + engine = BacktestEngine( + strategy=strategy, + initial_balance=Decimal(str(balance)), + slippage_pct=0.0005, # 0.05% slippage (stocks have tighter spreads) + taker_fee_pct=0.0, # Alpaca = 0% commission + ) + return engine.run(candles) + + +def main(): + random.seed(42) + + print("=" * 60) + print("MOC Strategy Backtest — US Stocks") + print("Capital: $750 (~100만원)") + print("=" * 60) + + # Test across multiple stocks + stocks = [ + ("AAPL", 180.0, 0.0003, 0.015), + ("MSFT", 420.0, 0.0004, 0.014), + ("TSLA", 250.0, 0.0001, 0.030), + ("NVDA", 800.0, 0.0005, 0.025), + ("AMZN", 185.0, 0.0003, 0.018), + ] + + # Parameter grid + param_sets = [] + for rsi_min in [25, 30, 35]: + for rsi_max in [55, 60, 65]: + for sl in [1.5, 2.0, 3.0]: + for ema in [10, 20]: + param_sets.append( + { + "quantity_pct": 0.2, + "stop_loss_pct": sl, + "rsi_min": rsi_min, + "rsi_max": rsi_max, + "ema_period": ema, + "volume_avg_period": 20, + "min_volume_ratio": 0.8, + "buy_start_utc": 19, + "buy_end_utc": 21, + "sell_start_utc": 14, + "sell_end_utc": 15, + "max_positions": 5, + } + ) + + print(f"\nParameter combinations: {len(param_sets)}") + print(f"Stocks: {[s[0] for s in stocks]}") + print("Generating 90 days of 5-min data per stock...\n") + + # Generate data for each stock + all_candles = {} + for symbol, base, drift, vol in stocks: + all_candles[symbol] = generate_stock_candles( + symbol, days=90, base_price=base, daily_drift=drift, daily_vol=vol + ) + print(f" {symbol}: {len(all_candles[symbol])} candles") + + # Test each parameter set across all stocks + print( + f"\nRunning {len(param_sets)} x {len(stocks)} = {len(param_sets) * len(stocks)} backtests..." + ) + + param_results = [] + for i, params in enumerate(param_sets): + total_profit = Decimal("0") + total_trades = 0 + total_sharpe = 0.0 + stock_details = [] + + for symbol, _, _, _ in stocks: + result = run_backtest(all_candles[symbol], params) + total_profit += result.profit + total_trades += result.total_trades + if result.detailed: + total_sharpe += result.detailed.sharpe_ratio + stock_details.append((symbol, result)) + + avg_sharpe = total_sharpe / len(stocks) if stocks else 0 + param_results.append((params, total_profit, total_trades, avg_sharpe, stock_details)) + + if (i + 1) % 18 == 0: + print(f" Progress: {i + 1}/{len(param_sets)}") + + # Sort by average Sharpe + param_results.sort(key=lambda x: x[3], reverse=True) + + print("\n" + "=" * 60) + print("TOP 5 PARAMETER SETS (by avg Sharpe across all stocks)") + print("=" * 60) + + for rank, (params, profit, trades, sharpe, details) in enumerate(param_results[:5], 1): + print(f"\n#{rank}:") + print( + f" RSI: {params['rsi_min']}-{params['rsi_max']}," + f" SL: {params['stop_loss_pct']}%, EMA: {params['ema_period']}" + ) + print(f" Total Profit: ${float(profit):.2f}, Trades: {trades}, Avg Sharpe: {sharpe:.3f}") + print(" Per stock:") + for symbol, result in details: + pct = float(result.profit_pct) + dd = result.detailed.max_drawdown if result.detailed else 0 + print(f" {symbol}: {pct:+.2f}% ({result.total_trades} trades, DD: {dd:.1f}%)") + + # Best params + best = param_results[0] + print("\n" + "=" * 60) + print("RECOMMENDED PARAMETERS") + print("=" * 60) + bp = best[0] + print(f" rsi_min: {bp['rsi_min']}") + print(f" rsi_max: {bp['rsi_max']}") + print(f" stop_loss_pct: {bp['stop_loss_pct']}") + print(f" ema_period: {bp['ema_period']}") + print(f" min_volume_ratio: {bp['min_volume_ratio']}") + print(f"\n Avg Sharpe: {best[3]:.3f}") + print(f" Total Profit: ${float(best[1]):.2f} across 5 stocks over 90 days") + + # Worst for comparison + print("\n" + "=" * 60) + print("WORST 3 PARAMETER SETS") + print("=" * 60) + for _rank, (params, profit, trades, sharpe, _) in enumerate(param_results[-3:], 1): + print( + f" RSI({params['rsi_min']}-{params['rsi_max']})," + f" SL={params['stop_loss_pct']}%, EMA={params['ema_period']}" + ) + print(f" Profit: ${float(profit):.2f}, Sharpe: {sharpe:.3f}") + + +if __name__ == "__main__": + main() diff --git a/services/api/tests/test_portfolio_router.py b/services/api/tests/test_portfolio_router.py index f2584ea..3bd1b2c 100644 --- a/services/api/tests/test_portfolio_router.py +++ b/services/api/tests/test_portfolio_router.py @@ -45,7 +45,7 @@ def test_get_positions_with_data(app, mock_db): app.state.db = db mock_row = MagicMock() - mock_row.symbol = "BTCUSDT" + mock_row.symbol = "AAPL" mock_row.quantity = Decimal("0.1") mock_row.avg_entry_price = Decimal("50000") mock_row.current_price = Decimal("55000") @@ -59,7 +59,7 @@ def test_get_positions_with_data(app, mock_db): assert response.status_code == 200 data = response.json() assert len(data) == 1 - assert data[0]["symbol"] == "BTCUSDT" + assert data[0]["symbol"] == "AAPL" def test_get_snapshots_empty(app, mock_db): diff --git a/services/backtester/src/backtester/config.py b/services/backtester/src/backtester/config.py index f7897da..57ee1fb 100644 --- a/services/backtester/src/backtester/config.py +++ b/services/backtester/src/backtester/config.py @@ -5,7 +5,7 @@ from shared.config import Settings class BacktestConfig(Settings): backtest_initial_balance: float = 10000.0 - symbol: str = "BTCUSDT" + symbol: str = "AAPL" timeframe: str = "1h" strategy_name: str = "rsi_strategy" candle_limit: int = 500 diff --git a/services/backtester/tests/test_walk_forward.py b/services/backtester/tests/test_walk_forward.py index 5ab2e7b..96abb6e 100644 --- a/services/backtester/tests/test_walk_forward.py +++ b/services/backtester/tests/test_walk_forward.py @@ -21,7 +21,7 @@ def _generate_candles(n=100, base_price=100.0): price = base_price + (i % 20) - 10 candles.append( Candle( - symbol="BTCUSDT", + symbol="AAPL", timeframe="1h", open_time=datetime(2025, 1, 1, tzinfo=timezone.utc) + timedelta(hours=i), open=Decimal(str(price)), diff --git a/services/data-collector/tests/test_storage.py b/services/data-collector/tests/test_storage.py index be85578..ffffa40 100644 --- a/services/data-collector/tests/test_storage.py +++ b/services/data-collector/tests/test_storage.py @@ -9,7 +9,7 @@ from shared.models import Candle from data_collector.storage import CandleStorage -def _make_candle(symbol: str = "BTCUSDT") -> Candle: +def _make_candle(symbol: str = "AAPL") -> Candle: return Candle( symbol=symbol, timeframe="1m", @@ -39,11 +39,11 @@ async def test_storage_saves_to_db_and_publishes(): mock_broker.publish.assert_called_once() stream_arg = mock_broker.publish.call_args[0][0] - assert stream_arg == "candles.BTCUSDT" + assert stream_arg == "candles.AAPL" data_arg = mock_broker.publish.call_args[0][1] assert data_arg["type"] == "CANDLE" - assert data_arg["data"]["symbol"] == "BTCUSDT" + assert data_arg["data"]["symbol"] == "AAPL" @pytest.mark.asyncio diff --git a/services/order-executor/tests/test_risk_manager.py b/services/order-executor/tests/test_risk_manager.py index 00a9ab4..3d5175b 100644 --- a/services/order-executor/tests/test_risk_manager.py +++ b/services/order-executor/tests/test_risk_manager.py @@ -7,7 +7,7 @@ from shared.models import OrderSide, Position, Signal from order_executor.risk_manager import RiskManager -def make_signal(side: OrderSide, price: str, quantity: str, symbol: str = "BTC/USDT") -> Signal: +def make_signal(side: OrderSide, price: str, quantity: str, symbol: str = "AAPL") -> Signal: return Signal( strategy="test", symbol=symbol, @@ -93,7 +93,7 @@ def test_risk_check_rejects_insufficient_balance(): def test_trailing_stop_set_and_trigger(): """Trailing stop should trigger when price drops below stop level.""" rm = make_risk_manager(trailing_stop_pct="5") - rm.set_trailing_stop("BTC/USDT", Decimal("100")) + rm.set_trailing_stop("AAPL", Decimal("100")) signal = make_signal(side=OrderSide.BUY, price="94", quantity="0.01") result = rm.check(signal, balance=Decimal("10000"), positions={}, daily_pnl=Decimal("0")) @@ -104,10 +104,10 @@ def test_trailing_stop_set_and_trigger(): def test_trailing_stop_updates_highest_price(): """Trailing stop should track the highest price seen.""" rm = make_risk_manager(trailing_stop_pct="5") - rm.set_trailing_stop("BTC/USDT", Decimal("100")) + rm.set_trailing_stop("AAPL", Decimal("100")) # Price rises to 120 => stop at 114 - rm.update_price("BTC/USDT", Decimal("120")) + rm.update_price("AAPL", Decimal("120")) # Price at 115 is above stop (114), should be allowed signal = make_signal(side=OrderSide.BUY, price="115", quantity="0.01") @@ -124,7 +124,7 @@ def test_trailing_stop_updates_highest_price(): def test_trailing_stop_not_triggered_above_stop(): """Trailing stop should not trigger when price is above stop level.""" rm = make_risk_manager(trailing_stop_pct="5") - rm.set_trailing_stop("BTC/USDT", Decimal("100")) + rm.set_trailing_stop("AAPL", Decimal("100")) # Price at 96 is above stop (95), should be allowed signal = make_signal(side=OrderSide.BUY, price="96", quantity="0.01") @@ -140,11 +140,11 @@ def test_max_open_positions_check(): rm = make_risk_manager(max_open_positions=2) positions = { - "BTC/USDT": make_position("BTC/USDT", "1", "100", "100"), - "ETH/USDT": make_position("ETH/USDT", "10", "50", "50"), + "AAPL": make_position("AAPL", "1", "100", "100"), + "MSFT": make_position("MSFT", "10", "50", "50"), } - signal = make_signal(side=OrderSide.BUY, price="10", quantity="1", symbol="SOL/USDT") + signal = make_signal(side=OrderSide.BUY, price="10", quantity="1", symbol="TSLA") result = rm.check(signal, balance=Decimal("10000"), positions=positions, daily_pnl=Decimal("0")) assert result.allowed is False assert result.reason == "Max open positions reached" @@ -158,14 +158,14 @@ def test_volatility_calculation(): rm = make_risk_manager(volatility_lookback=5) # No history yet - assert rm.get_volatility("BTC/USDT") is None + assert rm.get_volatility("AAPL") is None # Feed prices prices = [100, 102, 98, 105, 101] for p in prices: - rm.update_price("BTC/USDT", Decimal(str(p))) + rm.update_price("AAPL", Decimal(str(p))) - vol = rm.get_volatility("BTC/USDT") + vol = rm.get_volatility("AAPL") assert vol is not None assert vol > 0 @@ -177,9 +177,9 @@ def test_position_size_with_volatility_scaling(): # Feed volatile prices prices = [100, 120, 80, 130, 70] for p in prices: - rm.update_price("BTC/USDT", Decimal(str(p))) + rm.update_price("AAPL", Decimal(str(p))) - size = rm.calculate_position_size("BTC/USDT", Decimal("10000")) + size = rm.calculate_position_size("AAPL", Decimal("10000")) base = Decimal("10000") * Decimal("0.1") # High volatility should reduce size below base @@ -192,9 +192,9 @@ def test_position_size_without_scaling(): prices = [100, 120, 80, 130, 70] for p in prices: - rm.update_price("BTC/USDT", Decimal(str(p))) + rm.update_price("AAPL", Decimal(str(p))) - size = rm.calculate_position_size("BTC/USDT", Decimal("10000")) + size = rm.calculate_position_size("AAPL", Decimal("10000")) base = Decimal("10000") * Decimal("0.1") assert size == base @@ -211,8 +211,8 @@ def test_portfolio_exposure_check_passes(): max_portfolio_exposure=0.8, ) positions = { - "BTCUSDT": Position( - symbol="BTCUSDT", + "AAPL": Position( + symbol="AAPL", quantity=Decimal("0.01"), avg_entry_price=Decimal("50000"), current_price=Decimal("50000"), @@ -230,8 +230,8 @@ def test_portfolio_exposure_check_rejects(): max_portfolio_exposure=0.3, ) positions = { - "BTCUSDT": Position( - symbol="BTCUSDT", + "AAPL": Position( + symbol="AAPL", quantity=Decimal("1"), avg_entry_price=Decimal("50000"), current_price=Decimal("50000"), @@ -263,10 +263,10 @@ def test_var_calculation(): daily_loss_limit_pct=Decimal("10"), ) for i in range(30): - rm.update_price("BTCUSDT", Decimal(str(100 + (i % 5) - 2))) + rm.update_price("AAPL", Decimal(str(100 + (i % 5) - 2))) positions = { - "BTCUSDT": Position( - symbol="BTCUSDT", + "AAPL": Position( + symbol="AAPL", quantity=Decimal("1"), avg_entry_price=Decimal("100"), current_price=Decimal("100"), @@ -357,7 +357,7 @@ def test_drawdown_check_rejects_in_check(): rm.update_balance(Decimal("10000")) signal = Signal( strategy="test", - symbol="BTC/USDT", + symbol="AAPL", side=OrderSide.BUY, price=Decimal("50000"), quantity=Decimal("0.01"), diff --git a/services/portfolio-manager/tests/test_portfolio.py b/services/portfolio-manager/tests/test_portfolio.py index 768e071..365dc1a 100644 --- a/services/portfolio-manager/tests/test_portfolio.py +++ b/services/portfolio-manager/tests/test_portfolio.py @@ -10,7 +10,7 @@ def make_order(side: OrderSide, price: str, quantity: str) -> Order: """Helper to create a filled Order.""" return Order( signal_id="test-signal", - symbol="BTC/USDT", + symbol="AAPL", side=side, type=OrderType.MARKET, price=Decimal(price), @@ -24,7 +24,7 @@ def test_portfolio_add_buy_order() -> None: order = make_order(OrderSide.BUY, "50000", "0.1") tracker.apply_order(order) - position = tracker.get_position("BTC/USDT") + position = tracker.get_position("AAPL") assert position is not None assert position.quantity == Decimal("0.1") assert position.avg_entry_price == Decimal("50000") @@ -35,7 +35,7 @@ def test_portfolio_add_multiple_buys() -> None: tracker.apply_order(make_order(OrderSide.BUY, "50000", "0.1")) tracker.apply_order(make_order(OrderSide.BUY, "52000", "0.1")) - position = tracker.get_position("BTC/USDT") + position = tracker.get_position("AAPL") assert position is not None assert position.quantity == Decimal("0.2") assert position.avg_entry_price == Decimal("51000") @@ -46,7 +46,7 @@ def test_portfolio_sell_reduces_position() -> None: tracker.apply_order(make_order(OrderSide.BUY, "50000", "0.2")) tracker.apply_order(make_order(OrderSide.SELL, "55000", "0.1")) - position = tracker.get_position("BTC/USDT") + position = tracker.get_position("AAPL") assert position is not None assert position.quantity == Decimal("0.1") assert position.avg_entry_price == Decimal("50000") @@ -54,7 +54,7 @@ def test_portfolio_sell_reduces_position() -> None: def test_portfolio_no_position_returns_none() -> None: tracker = PortfolioTracker() - position = tracker.get_position("ETH/USDT") + position = tracker.get_position("MSFT") assert position is None @@ -66,7 +66,7 @@ def test_realized_pnl_on_sell() -> None: tracker.apply_order( Order( signal_id="s1", - symbol="BTCUSDT", + symbol="AAPL", side=OrderSide.BUY, type=OrderType.MARKET, price=Decimal("50000"), @@ -80,7 +80,7 @@ def test_realized_pnl_on_sell() -> None: tracker.apply_order( Order( signal_id="s2", - symbol="BTCUSDT", + symbol="AAPL", side=OrderSide.SELL, type=OrderType.MARKET, price=Decimal("55000"), @@ -98,7 +98,7 @@ def test_realized_pnl_on_loss() -> None: tracker.apply_order( Order( signal_id="s1", - symbol="BTCUSDT", + symbol="AAPL", side=OrderSide.BUY, type=OrderType.MARKET, price=Decimal("50000"), @@ -109,7 +109,7 @@ def test_realized_pnl_on_loss() -> None: tracker.apply_order( Order( signal_id="s2", - symbol="BTCUSDT", + symbol="AAPL", side=OrderSide.SELL, type=OrderType.MARKET, price=Decimal("45000"), @@ -128,7 +128,7 @@ def test_realized_pnl_accumulates() -> None: tracker.apply_order( Order( signal_id="s1", - symbol="BTCUSDT", + symbol="AAPL", side=OrderSide.BUY, type=OrderType.MARKET, price=Decimal("50000"), @@ -141,7 +141,7 @@ def test_realized_pnl_accumulates() -> None: tracker.apply_order( Order( signal_id="s2", - symbol="BTCUSDT", + symbol="AAPL", side=OrderSide.SELL, type=OrderType.MARKET, price=Decimal("55000"), @@ -154,7 +154,7 @@ def test_realized_pnl_accumulates() -> None: tracker.apply_order( Order( signal_id="s3", - symbol="BTCUSDT", + symbol="AAPL", side=OrderSide.SELL, type=OrderType.MARKET, price=Decimal("60000"), diff --git a/services/portfolio-manager/tests/test_snapshot.py b/services/portfolio-manager/tests/test_snapshot.py index a464599..ec5e92d 100644 --- a/services/portfolio-manager/tests/test_snapshot.py +++ b/services/portfolio-manager/tests/test_snapshot.py @@ -13,7 +13,7 @@ class TestSaveSnapshot: from portfolio_manager.main import save_snapshot pos = Position( - symbol="BTCUSDT", + symbol="AAPL", quantity=Decimal("0.5"), avg_entry_price=Decimal("50000"), current_price=Decimal("52000"), diff --git a/services/strategy-engine/src/strategy_engine/config.py b/services/strategy-engine/src/strategy_engine/config.py index e3a49c2..9fd9c49 100644 --- a/services/strategy-engine/src/strategy_engine/config.py +++ b/services/strategy-engine/src/strategy_engine/config.py @@ -4,6 +4,6 @@ from shared.config import Settings class StrategyConfig(Settings): - symbols: list[str] = ["BTC/USDT"] + symbols: list[str] = ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA"] timeframes: list[str] = ["1m"] strategy_params: dict = {} diff --git a/shared/tests/test_broker.py b/shared/tests/test_broker.py index 9be84b0..eb1582d 100644 --- a/shared/tests/test_broker.py +++ b/shared/tests/test_broker.py @@ -16,7 +16,7 @@ async def test_broker_publish(): from shared.broker import RedisBroker broker = RedisBroker("redis://localhost:6379") - data = {"type": "CANDLE", "symbol": "BTCUSDT"} + data = {"type": "CANDLE", "symbol": "AAPL"} await broker.publish("candles", data) mock_redis.xadd.assert_called_once() @@ -35,7 +35,7 @@ async def test_broker_subscribe_returns_messages(): mock_redis = AsyncMock() mock_from_url.return_value = mock_redis - payload_data = {"type": "CANDLE", "symbol": "ETHUSDT"} + payload_data = {"type": "CANDLE", "symbol": "MSFT"} mock_redis.xread.return_value = [ [ b"candles", @@ -53,7 +53,7 @@ async def test_broker_subscribe_returns_messages(): mock_redis.xread.assert_called_once() assert len(messages) == 1 assert messages[0]["type"] == "CANDLE" - assert messages[0]["symbol"] == "ETHUSDT" + assert messages[0]["symbol"] == "MSFT" @pytest.mark.asyncio diff --git a/shared/tests/test_notifier.py b/shared/tests/test_notifier.py index 3d29830..6c81369 100644 --- a/shared/tests/test_notifier.py +++ b/shared/tests/test_notifier.py @@ -86,7 +86,7 @@ class TestTelegramNotifierFormatters: notifier = TelegramNotifier(bot_token="fake-token", chat_id="123") signal = Signal( strategy="rsi_strategy", - symbol="BTCUSDT", + symbol="AAPL", side=OrderSide.BUY, price=Decimal("50000.00"), quantity=Decimal("0.01"), @@ -99,7 +99,7 @@ class TestTelegramNotifierFormatters: msg = mock_send.call_args[0][0] assert "BUY" in msg assert "rsi_strategy" in msg - assert "BTCUSDT" in msg + assert "AAPL" in msg assert "50000.00" in msg assert "0.01" in msg assert "RSI oversold" in msg @@ -109,7 +109,7 @@ class TestTelegramNotifierFormatters: notifier = TelegramNotifier(bot_token="fake-token", chat_id="123") order = Order( signal_id=str(uuid.uuid4()), - symbol="ETHUSDT", + symbol="MSFT", side=OrderSide.SELL, type=OrderType.LIMIT, price=Decimal("3000.50"), @@ -122,7 +122,7 @@ class TestTelegramNotifierFormatters: mock_send.assert_called_once() msg = mock_send.call_args[0][0] assert "FILLED" in msg - assert "ETHUSDT" in msg + assert "MSFT" in msg assert "SELL" in msg assert "3000.50" in msg assert "1.5" in msg @@ -143,7 +143,7 @@ class TestTelegramNotifierFormatters: notifier = TelegramNotifier(bot_token="fake-token", chat_id="123") positions = [ Position( - symbol="BTCUSDT", + symbol="AAPL", quantity=Decimal("0.1"), avg_entry_price=Decimal("50000"), current_price=Decimal("51000"), @@ -158,7 +158,7 @@ class TestTelegramNotifierFormatters: ) mock_send.assert_called_once() msg = mock_send.call_args[0][0] - assert "BTCUSDT" in msg + assert "AAPL" in msg assert "5100.00" in msg assert "100.00" in msg |
