ArifKemall commited on
Commit
7a8d58d
·
0 Parent(s):

Initial commit: Terminal.AI monorepo (PaaS-ready)

Browse files

- Frontend: Next.js 16 + React + TypeScript + Plotly
- Backend: FastAPI + VectorBT + yfinance
- LEAPS Scanner: S&P 100 concurrent anomaly detection
- Options Flow: Per-ticker chain analysis
- Dark theme terminal UI
- CORS configured for Vercel + Render deployment
- Legacy Docker files moved to _archive/

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +10 -0
  2. .gitignore +59 -0
  3. AGENTS.md +156 -0
  4. GEMINI.md +165 -0
  5. README.md +236 -0
  6. _archive/DEPLOY.md +82 -0
  7. _archive/Dockerfile.backend +16 -0
  8. _archive/Dockerfile.frontend +33 -0
  9. _archive/deploy.sh +63 -0
  10. _archive/docker-compose.yml +75 -0
  11. _archive/nginx/nginx.conf +139 -0
  12. _archive/setup-ssl.sh +42 -0
  13. _archive/test_options.py +54 -0
  14. _archive/test_options2.py +60 -0
  15. backend/.dockerignore +8 -0
  16. backend/.gitignore +16 -0
  17. backend/data_fetcher.py +47 -0
  18. backend/encoder.py +85 -0
  19. backend/leaps_scanner.py +182 -0
  20. backend/main.py +530 -0
  21. backend/options_flow.py +268 -0
  22. backend/requirements.txt +9 -0
  23. backend/start.sh +22 -0
  24. backend/strategies.py +277 -0
  25. frontend/.dockerignore +5 -0
  26. frontend/.gitignore +45 -0
  27. frontend/AGENTS.md +5 -0
  28. frontend/CLAUDE.md +1 -0
  29. frontend/README.md +36 -0
  30. frontend/app/age.tsx +0 -0
  31. frontend/app/api.ts +356 -0
  32. frontend/app/components/BacktestForm.tsx +376 -0
  33. frontend/app/components/CandlestickChart.tsx +98 -0
  34. frontend/app/components/ComparePanel.tsx +126 -0
  35. frontend/app/components/DrawdownChart.tsx +100 -0
  36. frontend/app/components/EquityChart.tsx +80 -0
  37. frontend/app/components/FundamentalsPanel.tsx +101 -0
  38. frontend/app/components/IndicatorChart.tsx +148 -0
  39. frontend/app/components/OptionsFlowPanel.tsx +295 -0
  40. frontend/app/components/PriceChart.tsx +164 -0
  41. frontend/app/components/StatsPanel.tsx +122 -0
  42. frontend/app/components/TerminalHeader.tsx +37 -0
  43. frontend/app/components/VolumeChart.tsx +63 -0
  44. frontend/app/favicon.ico +0 -0
  45. frontend/app/globals.css +84 -0
  46. frontend/app/layout.tsx +21 -0
  47. frontend/app/page.tsx +548 -0
  48. frontend/eslint.config.mjs +18 -0
  49. frontend/next.config.js +6 -0
  50. frontend/next.config.ts +7 -0
.env.example ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Terminal.AI Production Environment
2
+ # Domain'ini buraya yaz:
3
+ DOMAIN=yourdomain.com
4
+
5
+ # Backend
6
+ BACKEND_PORT=8000
7
+
8
+ # Frontend
9
+ FRONTEND_PORT=3000
10
+ NEXT_PUBLIC_API_URL=/api
.gitignore ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==========================================
2
+ # Terminal.AI — Monorepo .gitignore
3
+ # ==========================================
4
+
5
+ # --- OS & Editor ---
6
+ .DS_Store
7
+ Thumbs.db
8
+ *.swp
9
+ *.swo
10
+ *~
11
+ .idea/
12
+ .vscode/
13
+ *.sublime-*
14
+
15
+ # --- Environment & Secrets ---
16
+ .env
17
+ .env.local
18
+ .env.*.local
19
+ .env.production
20
+ .env.development
21
+ *.pem
22
+ *.key
23
+
24
+ # --- Python (Backend) ---
25
+ backend/venv/
26
+ backend/.venv/
27
+ backend/__pycache__/
28
+ backend/*.pyc
29
+ backend/*.pyo
30
+ backend/.pytest_cache/
31
+ backend/.mypy_cache/
32
+ backend/*.egg-info/
33
+ backend/dist/
34
+ backend/build/
35
+
36
+ # --- Node (Frontend) ---
37
+ frontend/node_modules/
38
+ frontend/.next/
39
+ frontend/out/
40
+ frontend/.cache/
41
+ frontend/.turbo/
42
+ frontend/coverage/
43
+ frontend/*.log
44
+
45
+ # --- Build & Dist (Root) ---
46
+ dist/
47
+ build/
48
+ *.tar.gz
49
+
50
+ # --- Docker (artık kullanılmıyor, arşivde) ---
51
+ # docker-compose.yml
52
+ # nginx/
53
+ # deploy.sh
54
+ # setup-ssl.sh
55
+
56
+ # --- Misc ---
57
+ *.bak
58
+ *.tmp
59
+ .cache/
AGENTS.md ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Terminal.AI — VectorBT Finansal Backtest Terminali
2
+
3
+ ## Proje Özeti
4
+ VectorBT kütüphanesini temel alan, profesyonel trading terminallerinden ilham alan (dark theme, monospaced estetiği) bir finansal backtest ve analiz platformu. "Terminal.AI" markası altında siyah zemin üzerine beyaz/turuncu aksanlarla kurgulanmıştır. Opsiyon akışı (options flow) modülüyle LEAPS ve anormal opsiyon aktivitelerini izleme desteği mevcuttur.
5
+
6
+ ## Teknik Mimari
7
+
8
+ ### Backend (v1.1.0)
9
+ - **Framework:** FastAPI (Python) — Port 8000
10
+ - **Analiz Motoru:** vectorbt==0.26.2
11
+ - **Veri Kaynağı:** yfinance==1.3.0 (curl_cffi tabanlı)
12
+ - **Stratejiler:** SMA Crossover, EMA Crossover, Bollinger Bands
13
+ - **İndikatörler:** RSI, MACD
14
+ - **Sabitler:** init_cash=10000, fees=0.001 (arka planda)
15
+ - **Cache:** 10dk TTL in-memory cache (fundamentals + ticker range), 5dk options flow cache
16
+
17
+ ### Frontend
18
+ - **Framework:** Next.js 16.2.6 (App Router, Turbopack) — Port 3000
19
+ - **CSS:** TailwindCSS v4
20
+ - **Grafik:** react-plotly.js ^2.6.0 + plotly.js ^3.5.1
21
+ - **Dil:** TypeScript, React 19
22
+
23
+ ## Proje Dizin Yapısı
24
+ ```
25
+ HERMES(deneme)/
26
+ ├── AGENTS.md / GEMINI.md # Proje bağlam dosyaları
27
+ ├── backend/
28
+ │ ├── main.py # FastAPI (v1.1.0) — endpoint'ler + cache
29
+ │ ├── strategies.py # Stratejiler: sma, ema, bollinger + rsi, macd
30
+ │ ├── data_fetcher.py # yfinance ile OHLCV veri çekme
31
+ │ ├── encoder.py # NanEncoder — NaN/Inf/DateTime serializer
32
+ │ ├── options_flow.py # Opsiyon akışı + LEAPS detay (tek hisse)
33
+ │ ├── leaps_scanner.py # S&P 100 concurrent LEAPS anomali tarama
34
+ │ ├── requirements.txt, start.sh, .env
35
+ │ └── venv/ # Sanal ortam
36
+ └── frontend/
37
+ ├── app/
38
+ │ ├── page.tsx # Ana sayfa — backtest + compare + options
39
+ │ ├── layout.tsx # Root layout (suppressHydrationWarning)
40
+ │ ├── globals.css # Dark tema CSS değişkenleri
41
+ │ ├── api.ts # Backend API client (options dahil)
42
+ │ └── components/
43
+ │ ├── BacktestForm.tsx # Strateji + ticker + tarih (boş başlar)
44
+ │ ├── FundamentalsPanel.tsx # Şirket çarpanları (P/E, P/B, vb.)
45
+ │ ├── OptionsFlowPanel.tsx # Opsiyon akışı + LEAPS detay
46
+ │ ├── PriceChart.tsx # Candlestick/Line + overlay + sinyaller
47
+ │ ├── CandlestickChart.tsx, VolumeChart.tsx
48
+ │ ├── EquityChart.tsx, DrawdownChart.tsx
49
+ │ ├── IndicatorChart.tsx # RSI/MACD
50
+ │ ├── ComparePanel.tsx # Strateji karşılaştırma
51
+ │ └── TerminalHeader.tsx
52
+ ├── AGENTS.md, CLAUDE.md
53
+ └── package.json
54
+ ```
55
+
56
+ ## API Endpoint'ler
57
+
58
+ ### Backtest & Analiz
59
+ - `GET /api/health` → `{"status":"ok","version":"1.1.0"}`
60
+ - `GET /api/strategies` → sma, ema, bollinger listesi
61
+ - `POST /api/backtest` → Strateji seçerek backtest. Parametreler: `strategy`, `fast_ma`, `slow_ma`, `bb_window`, `bb_std`, `rsi_window`, `macd_*`. Response: OHLCV + overlay + RSI/MACD + sinyaller + equity + drawdown.
62
+ - `POST /api/compare` → Tüm stratejileri karşılaştırır.
63
+ - `GET /api/ticker/{ticker}` → Ticker bilgisi
64
+ - `GET /api/ticker/{ticker}/range` → `{earliest, latest, data_points}` (10dk cache)
65
+ - `GET /api/ticker/{ticker}/fundamentals` → Şirket çarpanları (10dk cache)
66
+
67
+ ### Opsiyon Akışı (Options Flow)
68
+ - `GET /api/ticker/{ticker}/options-flow` → Tek hisse opsiyon zinciri analizi
69
+ - Per-expiry call/put volumes, OI, volume/OI ratios
70
+ - LEAPS (vade > 365 gün) ayrı işaretlenmiş
71
+ - Anormal hacim/OI spike'ları (vol/OI ≥ 2x)
72
+ - Call/Put oranı
73
+ - Cache: 5 dakika
74
+ - Response: `{ ticker, as_of, has_leaps, leaps_expiries, summary, all_expiries, leaps, unusual_activities }`
75
+
76
+ - `GET /api/options-flow/leaps-board` → 22 ticker taraması, LEAPS aktivitesine göre sıralı (async concurrent)
77
+ - Response: `{ count, scan_tickers, results: [{ ticker, has_leaps, leaps_count, total_call_volume, total_put_volume, call_put_ratio, unusual_count, top_leaps_expiry }] }`
78
+
79
+ - `GET /api/leaps/scanner` → **S&P 100 tabanlı LEAPS anomali tarama** (yeni!)
80
+ - S&P 100 ticker listesini concurrent tarar (~100 hisse, ~10 saniye)
81
+ - LEAPS opsiyonlarda volume/OI ≥ 2x anormal spike yakalar
82
+ - Parametreler: `max_tickers` (1-100, varsayılan 100), `min_spikes` (varsayılan 1)
83
+ - Response: `{ tickers: [{ ticker, total_leaps_call_vol, total_leaps_put_vol, total_leaps_vol, call_put_ratio, unusual_count, top_spike, all_spikes, leaps_expiries_count, nearest_leaps_expiry }], count, scan_info }`
84
+
85
+ ## Mevcut Durum
86
+ - [x] **3 strateji:** SMA, EMA, Bollinger
87
+ - [x] RSI + MACD indikatörleri
88
+ - [x] Candlestick/Line toggle, Volume, Drawdown grafikleri
89
+ - [x] Strateji karşılaştırma (Compare All)
90
+ - [x] **Fundamentals paneli** — Market Cap, P/E, P/B, EV/EBITDA, ROE, Beta, vb.
91
+ - [x] Ticker yazınca direkt fundamentals + tarih aralığı (800ms debounce)
92
+ - [x] "All Time" butonu ile full range seçimi
93
+ - [x] Equity curve, pan/zoom modebar
94
+ - [x] 10dk backend cache (rate limit koruması)
95
+ - [x] Dark tema (Terminal.AI — siyah+turuncu)
96
+ - [x] **Options Flow modülü** — `options_flow.py` (yfinance tabanlı, tek hisse detay)
97
+ - [x] **LEAPS Scanner** — `leaps_scanner.py` (S&P 100 concurrent, ~10s)
98
+ - [x] **Options sekmesi** — Frontend'te LEAPS Scanner tablosu + per-ticker options flow
99
+ - [x] Backend + Frontend birlikte çalışıyor
100
+
101
+ ## Bilinen Sorunlar ve Çözümler
102
+
103
+ ### 1. vectorbt MACD — `histogram` → `hist`
104
+ **Dosya:** `backend/strategies.py` · Çözüm: `macd.histogram` yerine `macd.hist`.
105
+
106
+ ### 2. vectorbt'de EMA yok
107
+ **Dosya:** `backend/strategies.py` → `run_ema_crossover()`
108
+ **Çözüm:** `close.ewm(span=N, adjust=False).mean()` ile pandas EMA, crossover manuel.
109
+
110
+ ### 3. vectorbt BBANDS.run bug'li
111
+ **Dosya:** `backend/strategies.py` → `run_bollinger_bands()`
112
+ **Çözüm:** `close.rolling(w).mean() + std()` ile manuel hesaplama.
113
+
114
+ ### 4. Port cakismasi (Windows/MSYS)
115
+ **Çözüm:** `cmd //c "taskkill /PID <PID> /F"` — start.sh otomatik temizler.
116
+
117
+ ### 5. Dark Reader hydration
118
+ **Dosya:** `frontend/app/layout.tsx` → `<html suppressHydrationWarning>`.
119
+
120
+ ### 6. yfinance eski surum → 0.2.43 → 1.3.0
121
+
122
+ ### 7. Plotly TypeScript tip uyumsuzlukları
123
+ **Sorun:** `react-plotly.js` ile `plotly.js` versiyon mismatch → TS2769 hataları
124
+ **Durum:** Next.js Turbopack bunları ignore eder, runtime'da sorun çıkarmaz. Düşük öncelik.
125
+
126
+ ## Options Flow Modülü Detayları
127
+
128
+ ### `backend/options_flow.py` (tek hisse detay)
129
+ - `fetch_options_flow(ticker)` — Tek hisse tam opsiyon zinciri analizi
130
+ - `rank_single_ticker(sym)` — Tek hisse özet (LEAPS board için)
131
+ - `rank_tickers_by_leaps_activity(tickers)` — Çoklu ticker sıralama (senkron, fallback)
132
+ - `DEFAULT_SCAN_TICKERS` — 22 ticker statik liste
133
+
134
+ ### `backend/leaps_scanner.py` (S&P 100 anomali tarama)
135
+ - `scan_ticker_leaps(sym)` — Tek ticker LEAPS tarama (vol/OI ≥ 2x)
136
+ - `scan_all_sp100_leaps(max_tickers)` — S&P 100 concurrent tarama (ThreadPoolExecutor, 10 worker)
137
+ - `SP100_TICKERS` — S&P 100 ticker listesi (~100 hisse)
138
+ - Sabitler: `LEAPS_MIN_DAYS=365`, `UNUSUAL_VOL_OI_MIN=2.0`, `MAX_WORKERS=10`
139
+
140
+ ### `frontend/app/components/OptionsFlowPanel.tsx`
141
+ - Props: `ticker: string`
142
+ - KPI satırı: Call Vol, Put Vol, C/P Ratio, Expiries, LEAPS count
143
+ - Expiry breakdown: Her vade için açılır/kapanır kart (top calls/puts tablosu)
144
+ - Unusual Activity tablosu: vol/OI ≥ 2x olan sinyaller
145
+
146
+ ### `frontend/app/api.ts` — Yeni tipler
147
+ - `OptionsFlowResponse`, `OptionsChainExpiry`, `OptionStrike`, `UnusualActivity`
148
+ - `LeapsBoardEntry`, `LeapsBoardResponse` (22 ticker board)
149
+ - `LeapsScannerEntry`, `LeapsScannerResponse` (S&P 100 scanner)
150
+ - Fonksiyonlar: `fetchOptionsFlow(ticker)`, `fetchLeapsBoard(limit)`, `fetchLeapsScanner(max_tickers, min_spikes)`
151
+
152
+ ## Hizli Calistirma
153
+ ```bash
154
+ cd backend && bash start.sh # port 8000 (otomatik port temizliği)
155
+ cd frontend && npm run dev # port 3000
156
+ ```
GEMINI.md ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Terminal.AI — VectorBT Finansal Backtest Terminali
2
+
3
+ ## Proje Özeti
4
+ VectorBT kütüphanesini temel alan, profesyonel trading terminallerinden ilham alan (dark theme, monospaced estetiği) bir finansal backtest ve analiz platformu. Kullanıcıların teknik göstergeleri (SMA, RSI, MACD) test etmesine ve sonuçları interaktif grafiklerle görmesine olanak tanır. "Terminal.AI" markası altında siyah zemin üzerine beyaz/turuncu aksanlarla kurgulanmıştır. Opsiyon akışı modülüyle LEAPS ve anormal opsiyon aktivitelerini izleme desteği mevcuttur.
5
+
6
+ ## Teknik Mimari
7
+
8
+ ### Backend (v1.1.0)
9
+ - **Framework:** FastAPI (Python)
10
+ - **Port:** 8000
11
+ - **Analiz Motoru:** vectorbt==0.26.2
12
+ - **Veri Kaynağı:** yfinance==1.3.0 (curl_cffi tabanlı)
13
+ - **Stratejiler:** SMA Crossover, EMA Crossover, Bollinger Bands
14
+ - **İndikatörler:** RSI, MACD
15
+ - **Sabitler:** init_cash=10000, fees=0.001 (arka planda)
16
+ - **Cache:** 10dk TTL in-memory cache (fundamentals + ticker range), 5dk options flow cache
17
+ - **Diğer:** pandas==2.2.3, numpy==1.26.4, pydantic==2.9.2
18
+ - **Çalıştırma:** `cd backend && bash start.sh`
19
+
20
+ ### Frontend
21
+ - **Framework:** Next.js 16.2.6 (App Router, Turbopack)
22
+ - **Port:** 3000
23
+ - **CSS:** TailwindCSS v4
24
+ - **Grafik:** react-plotly.js ^2.6.0 + plotly.js ^3.5.1
25
+ - **Dil:** TypeScript, React 19
26
+ - **Çalıştırma:** `cd frontend && npm run dev`
27
+
28
+ ## Proje Dizin Yapısı
29
+ ```
30
+ HERMES(deneme)/
31
+ ├── AGENTS.md / GEMINI.md # Proje bağlam dosyaları
32
+ ├── backend/
33
+ │ ├── main.py # FastAPI (v1.1.0) — endpoint'ler + cache
34
+ │ ├── strategies.py # Stratejiler: sma, ema, bollinger + rsi, macd
35
+ │ ├── data_fetcher.py # yfinance ile OHLCV veri çekme
36
+ │ ├── encoder.py # NanEncoder — NaN/Inf/DateTime serializer
37
+ │ ├── options_flow.py # Opsiyon akışı (tek hisse detay)
38
+ │ ├── leaps_scanner.py # S&P 100 concurrent LEAPS anomali tarama
39
+ │ ├── requirements.txt # Python bağımlılıkları
40
+ │ ├── start.sh # Başlatma scripti (port temizliği dahil)
41
+ │ └── venv/ # Sanal ortam
42
+ └── frontend/
43
+ ├── app/
44
+ │ ├── page.tsx # Ana sayfa — backtest + compare + options
45
+ │ ├── layout.tsx # Root layout
46
+ │ ├── globals.css # Dark tema CSS değişkenleri
47
+ │ ├── api.ts # Backend API client
48
+ │ └── components/
49
+ │ ├── BacktestForm.tsx # Parametre giriş formu
50
+ │ ├── StatsPanel.tsx # Portföy istatistik paneli
51
+ │ ├── PriceChart.tsx # Fiyat + overlay grafiği
52
+ │ ├── CandlestickChart.tsx # Mum grafiği
53
+ │ ├── VolumeChart.tsx # Hacim grafiği
54
+ │ ├── EquityChart.tsx # Equity curve grafiği
55
+ │ ├── DrawdownChart.tsx # Drawdown grafiği
56
+ │ ├── IndicatorChart.tsx # RSI/MACD indikatör grafiği
57
+ │ ├── ComparePanel.tsx # Strateji karşılaştırma
58
+ │ ├── FundamentalsPanel.tsx # Şirket çarpanları
59
+ │ ├── OptionsFlowPanel.tsx # Opsiyon akışı detay
60
+ │ └── TerminalHeader.tsx # Terminal.AI başlık
61
+ ├── AGENTS.md # Next.js uyarısı
62
+ ├── CLAUDE.md # AGENTS.md referansı
63
+ └── package.json # Node.js bağımlılıkları
64
+ ```
65
+
66
+ ## API Endpoint'ler
67
+
68
+ ### Backtest & Analiz
69
+ - `GET /api/health` → `{"status": "ok", "version": "1.1.0"}`
70
+ - `GET /api/strategies` → Mevcut stratejileri listeler
71
+ - `POST /api/backtest` → SMA Crossover backtest + RSI + MACD indikatörleri çalıştırır
72
+ - `POST /api/compare` → Tüm stratejileri karşılaştırır
73
+ - `GET /api/ticker/{ticker}` → Ticker bilgisi ve son fiyat
74
+ - `GET /api/ticker/{ticker}/range` → Tarih aralığı (10dk cache)
75
+ - `GET /api/ticker/{ticker}/fundamentals` → Şirket çarpanları (10dk cache)
76
+
77
+ ### Opsiyon Akışı
78
+ - `GET /api/ticker/{ticker}/options-flow` → Tek hisse opsiyon zinciri analizi (5dk cache)
79
+ - `GET /api/options-flow/leaps-board` → 22 ticker LEAPS board (async concurrent)
80
+ - `GET /api/leaps/scanner?max_tickers=100&min_spikes=1` → S&P 100 LEAPS anomali tarama (~10s)
81
+
82
+ ## Backtest Request/Response
83
+
84
+ **Request:**
85
+ ```json
86
+ {
87
+ "ticker": "AAPL",
88
+ "start_date": "2024-01-01",
89
+ "end_date": "2024-06-30",
90
+ "interval": "1d",
91
+ "fast_ma": 10,
92
+ "slow_ma": 30,
93
+ "fees": 0.001,
94
+ "init_cash": 10000,
95
+ "rsi_window": 14,
96
+ "macd_fast": 12,
97
+ "macd_slow": 26,
98
+ "macd_signal": 9
99
+ }
100
+ ```
101
+
102
+ **Response:** OHLCV verisi + portföy istatistikleri + trade log + SMA/RSI/MACD indikatör verileri + al/sat sinyalleri + equity curve + drawdown.
103
+
104
+ ## Mevcut Durum
105
+ - [x] SMA Crossover stratejisi çalışıyor
106
+ - [x] EMA Crossover stratejisi çalışıyor
107
+ - [x] Bollinger Bands stratejisi çalışıyor
108
+ - [x] RSI ve MACD indikatörleri backend'de hesaplanıyor
109
+ - [x] Frontend'de tüm grafikler (fiyat, equity, RSI, MACD, volume, drawdown) gösteriliyor
110
+ - [x] Strateji karşılaştırma (Compare All)
111
+ - [x] Fundamentals paneli (Market Cap, P/E, P/B, EV/EBITDA, ROE, Beta)
112
+ - [x] Ticker değişince otomatik fundamentals + tarih aralığı (800ms debounce)
113
+ - [x] Options Flow modülü (tek hisse detay + LEAPS board + S&P 100 scanner)
114
+ - [x] Options sekmesi (LEAPS Scanner tablosu + per-ticker options flow)
115
+ - [x] Dark tema (Terminal.AI markası)
116
+ - [x] yfinance 1.3.0'a yükseltildi (curl_cffi tabanlı)
117
+ - [x] Backend + Frontend birlikte çalışıyor
118
+
119
+ ## Bilinen Sorunlar ve Çözümler
120
+
121
+ ### 1. vectorbt MACD — `histogram` attribute'ı yok
122
+ **Hata:** `'MACD' object has no attribute 'histogram'`
123
+ **Çözüm:** `macd.histogram` yerine `macd.hist` kullanılmalı (vectorbt 0.26.2 API değişikliği).
124
+ **Dosya:** `backend/strategies.py`
125
+
126
+ ### 2. Port çakışması (Windows)
127
+ **Sorun:** Eski process kapatılmazsa port 8000 dolu kalır.
128
+ **Çözüm:** `start.sh` otomatik port temizliği yapar. Manuel: `cmd //c "taskkill /PID <PID> /F"`
129
+
130
+ ### 3. Dark Reader hydration hatası
131
+ **Sorun:** Dark Reader eklentisi `<html>`'e attribute ekler, hydration uyuşmazlığı oluşur.
132
+ **Çözüm:** `<html lang="en" suppressHydrationWarning>` — layout.tsx'te eklendi.
133
+
134
+ ### 4. yfinance eski sürüm
135
+ **Sorun:** yfinance 0.2.43 Yahoo Finance API'den veri çekemiyordu.
136
+ **Çözüm:** yfinance 1.3.0'a yükseltildi (curl_cffi tabanlı, Cloudflare bypass).
137
+
138
+ ### 5. Plotly TypeScript tipleri
139
+ **Sorun:** `react-plotly.js` ile `plotly.js` versiyon mismatch → TS2769
140
+ **Durum:** Turbopack ignore eder, runtime sorun yok.
141
+
142
+ ## LEAPS Scanner Detayı (`backend/leaps_scanner.py`)
143
+ - S&P 100 ticker listesini ~10 saniyede concurrent tarar
144
+ - LEAPS = vade > 365 gün
145
+ - Anomali = volume/openInterest ≥ 2x
146
+ - Sonuçlar toplam LEAPS hacmine göre sıralı
147
+ - Her ticker için en büyük spike + tüm spike'lar döner
148
+
149
+ ## Yapılacaklar / Gelecek Özellikler
150
+ - [ ] Portföy optimizasyonu (çoklu ticker backtest)
151
+ - [ ] LEAPS Scanner sonuçlarını export (CSV)
152
+ - [ ] Canlı veri / WebSocket desteği
153
+ - [ ] Responsive mobil görünüm iyileştirmeleri
154
+ - [ ] Plotly TS tip düzeltmeleri
155
+
156
+ ## Çalıştırma (Hızlı Başlangıç)
157
+ ```bash
158
+ # Backend
159
+ cd backend && bash start.sh
160
+ # -> http://localhost:8000
161
+
162
+ # Frontend (ayrı terminal)
163
+ cd frontend && npm run dev
164
+ # -> http://localhost:3000
165
+ ```
README.md ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+
3
+ # ⚡ Terminal.AI
4
+
5
+ **VectorBT Finansal Backtest + LEAPS Opsiyon Scanner**
6
+
7
+ [![Next.js](https://img.shields.io/badge/Next.js-16-black?logo=nextdotjs)](https://nextjs.org/)
8
+ [![FastAPI](https://img.shields.io/badge/FastAPI-0.115-green?logo=fastapi)](https://fastapi.tiangolo.com/)
9
+ [![Python](https://img.shields.io/badge/Python-3.11-blue?logo=python)](https://python.org/)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5-blue?logo=typescript)](https://typescriptlang.org/)
11
+ [![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE)
12
+
13
+ [Demo](https://terminal-ai.vercel.app) · [Hataları Bildir](https://github.com/username/terminal-ai/issues) · [Özellik İste](https://github.com/username/terminal-ai/issues)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## 📖 Hakkında
20
+
21
+ Terminal.AI, **VectorBT** kütüphanesini temel alan, profesyonel trading terminallerinden ilham alan bir **finansal backtest ve opsiyon analiz platformudur**. Siyah zemin üzerine turuncu aksanlarla kurgulanmış terminal estetiğiyle, teknik göstergeleri test etmenizi ve sonuçları interaktif grafiklerle görmenizi sağlar.
22
+
23
+ ### Temel Özellikler
24
+
25
+ - **3 Strateji:** SMA Crossover, EMA Crossover, Bollinger Bands
26
+ - **İndikatörler:** RSI, MACD
27
+ - **Grafikler:** Candlestick, Volume, Equity Curve, Drawdown, RSI/MACD
28
+ - **Strateji Karşılaştırma:** Tüm stratejileri tek seferde karşılaştır
29
+ - **Fundamentals Paneli:** Market Cap, P/E, P/B, EV/EBITDA, ROE, Beta
30
+ - **Options Flow:** Tek hisse opsiyon zinciri analizi (LEAPS + anormal hacim/OI)
31
+ - **LEAPS Scanner:** S&P 100 concurrent taraması (~10 saniye), tıklanabilir satırlar
32
+
33
+ ---
34
+
35
+ ## 🏗️ Mimari
36
+
37
+ Bu proje **monorepo** yapısında olup **PaaS (Platform as a Service)** mimarisinde deploy edilmiştort.
38
+
39
+ ```
40
+ ┌─────────────────────────────────────────────────────────┐
41
+ │ GitHub (Monorepo) │
42
+ │ ┌───────────────────┐ ┌──────────────────────────┐ │
43
+ │ │ frontend/ │ │ backend/ │ │
44
+ │ │ (Next.js) │ │ (FastAPI) │ │
45
+ │ └────────┬──────────┘ └───────────┬──────────────┘ │
46
+ └───────────┼───────────────────────────┼──────────────────┘
47
+ │ │
48
+ ▼ ▼
49
+ ┌───────────────┐ ┌────────────────┐
50
+ │ Vercel │ │ Render │
51
+ │ (Frontend) │◄────────►│ (Backend) │
52
+ └───────────────┘ └────────────────┘
53
+ ```
54
+
55
+ ### Frontend → Vercel
56
+ - **Root Directory:** `frontend/`
57
+ - **Framework:** Next.js 16 (App Router, Turbopack)
58
+ - **Build Command:** `npm run build`
59
+ - **Output Directory:** `.next/`
60
+ - **Environment:** `NEXT_PUBLIC_API_URL=https://your-backend.onrender.com`
61
+
62
+ ### Backend → Render
63
+ - **Root Directory:** `backend/`
64
+ - **Runtime:** Python 3.11
65
+ - **Build Command:** `pip install -r requirements.txt`
66
+ - **Start Command:** `uvicorn main:app --host 0.0.0.0 --port 8000`
67
+ - **Environment:** `FRONTEND_URL=https://your-frontend.vercel.app`
68
+
69
+ ---
70
+
71
+ ## 📁 Klasör Yapısı
72
+
73
+ ```
74
+ terminal-ai/
75
+ ├── frontend/ # Next.js Frontend
76
+ │ ├── app/
77
+ │ │ ├── page.tsx # Ana sayfa (Backtest / Compare / Options)
78
+ │ │ ├── layout.tsx # Root layout
79
+ │ │ ├── globals.css # Dark tema CSS
80
+ │ │ ├── api.ts # Backend API client
81
+ │ │ └── components/
82
+ │ │ ├── BacktestForm.tsx
83
+ │ │ ├── PriceChart.tsx
84
+ │ │ ├── CandlestickChart.tsx
85
+ │ │ ├── VolumeChart.tsx
86
+ │ │ ├── EquityChart.tsx
87
+ │ │ ├── DrawdownChart.tsx
88
+ │ │ ├── IndicatorChart.tsx
89
+ │ │ ├── ComparePanel.tsx
90
+ │ │ ├── FundamentalsPanel.tsx
91
+ │ │ ├── OptionsFlowPanel.tsx
92
+ │ │ └── TerminalHeader.tsx
93
+ │ ├── public/
94
+ │ └── package.json
95
+
96
+ ├── backend/ # FastAPI Backend
97
+ │ ├── main.py # Endpoint'ler + CORS + Cache
98
+ │ ├── strategies.py # SMA, EMA, Bollinger + RSI, MACD
99
+ │ ├── data_fetcher.py # yfinance OHLCV
100
+ │ ├── encoder.py # NaN/Inf/DateTime serializer
101
+ │ ├── options_flow.py # Tek hisse opsiyon zinciri
102
+ │ ├── leaps_scanner.py # S&P 100 LEAPS anomali tarama
103
+ │ ├── requirements.txt
104
+ │ └── start.sh
105
+
106
+ ├── _archive/ # Eski Docker deployment dosyaları
107
+ │ ├── docker-compose.yml
108
+ │ ├── nginx/
109
+ │ ├── deploy.sh
110
+ │ ├── setup-ssl.sh
111
+ │ ├── Dockerfile.backend
112
+ │ ├── Dockerfile.frontend
113
+ │ └── DEPLOY.md
114
+
115
+ ├── .gitignore # Kök + alt dizin ignore kuralları
116
+ ├── AGENTS.md # Proje bağlam dosyası (AI agent'ları için)
117
+ ├── GEMINI.md # Proje bağlam dosyası (Gemini için)
118
+ └── README.md # Bu dosya
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 🚀 Deploy Rehberi
124
+
125
+ ### Frontend (Vercel)
126
+
127
+ 1. [Vercel](https://vercel.com)'e GitHub hesabınla giriş yap
128
+ 2. **Add New Project** → Repository'yi seç
129
+ 3. **Root Directory:** `frontend` olarak ayarla
130
+ 4. **Framework Preset:** Next.js
131
+ 5. **Environment Variables:**
132
+ ```
133
+ NEXT_PUBLIC_API_URL=https://your-backend.onrender.com
134
+ ```
135
+ 6. **Deploy** butonuna tıkla
136
+
137
+ ### Backend (Render)
138
+
139
+ 1. [Render](https://render.com)'e giriş yap
140
+ 2. **New +** → **Web Service**
141
+ 3. GitHub repo'nu bağla
142
+ 4. **Root Directory:** `backend` olarak ayarla
143
+ 5. **Runtime:** Python 3
144
+ 6. **Build Command:** `pip install -r requirements.txt`
145
+ 7. **Start Command:** `uvicorn main:app --host 0.0.0.0 --port 8000`
146
+ 8. **Environment Variables:**
147
+ ```
148
+ FRONTEND_URL=https://your-frontend.vercel.app
149
+ ```
150
+ 9. **Create Web Service** butonuna tıkla
151
+
152
+ ### Environment Değişkenleri
153
+
154
+ | Değişken | Servis | Açıklama |
155
+ |-----------|--------|----------|
156
+ | `NEXT_PUBLIC_API_URL` | Frontend | Backend URL'si (örn: `https://terminal-ai-api.onrender.com`) |
157
+ | `FRONTEND_URL` | Backend | Frontend URL'si (CORS için, örn: `https://terminal-ai.vercel.app`) |
158
+
159
+ ---
160
+
161
+ ## 💻 Geliştirme
162
+
163
+ ### Gereksinimler
164
+ - Python 3.11+
165
+ - Node.js 20+
166
+ - npm veya yarn
167
+
168
+ ### Backend
169
+
170
+ ```bash
171
+ cd backend
172
+ python -m venv venv
173
+ source venv/bin/activate # Windows: venv\Scripts\activate
174
+ pip install -r requirements.txt
175
+ python main.py
176
+ # → http://localhost:8000
177
+ ```
178
+
179
+ ### Frontend
180
+
181
+ ```bash
182
+ cd frontend
183
+ npm install
184
+ npm run dev
185
+ # → http://localhost:3000
186
+ ```
187
+
188
+ ### API Dokümantasyonu
189
+
190
+ Backend çalışırken: `http://localhost:8000/docs` (Swagger UI)
191
+
192
+ ---
193
+
194
+ ## 📡 API Endpoint'ler
195
+
196
+ ### Backtest & Analiz
197
+ | Method | Endpoint | Açıklama |
198
+ |--------|----------|----------|
199
+ | GET | `/api/health` | Health check |
200
+ | GET | `/api/strategies` | Strateji listesi |
201
+ | POST | `/api/backtest` | Backtest çalıştır |
202
+ | POST | `/api/compare` | Tüm stratejileri karşılaştır |
203
+ | GET | `/api/ticker/{ticker}` | Ticker bilgisi |
204
+ | GET | `/api/ticker/{ticker}/range` | Tarih aralığı |
205
+ | GET | `/api/ticker/{ticker}/fundamentals` | Şirket çarpanları |
206
+
207
+ ### Opsiyon Akışı
208
+ | Method | Endpoint | Açıklama |
209
+ |--------|----------|----------|
210
+ | GET | `/api/ticker/{ticker}/options-flow` | Tek hisse opsiyon zinciri |
211
+ | GET | `/api/options-flow/leaps-board` | 22 ticker LEAPS board |
212
+ | GET | `/api/leaps/scanner` | S&P 100 LEAPS anomali tarama |
213
+
214
+ ---
215
+
216
+ ## ⚠️ Bilinen Sorunlar
217
+
218
+ 1. **vectorbt MACD:** `histogram` attribute'ı yok, `hist` kullanılıyor
219
+ 2. **vectorbt EMA:** Yok, pandas `ewm()` ile manuel hesaplanıyor
220
+ 3. **vectorbt BBANDS:** `.run()` bug'lı, pandas `rolling()` ile manuel
221
+ 4. **Plotly TS tipleri:** Versiyon mismatch var ama Turbopack ignore ediyor
222
+ 5. **yfinance rate limit:** IP bazlı, aşırı sorgu yapılmamalı
223
+
224
+ ---
225
+
226
+ ## 📄 Lisans
227
+
228
+ MIT License — Detaylar için [LICENSE](LICENSE) dosyasına bakın.
229
+
230
+ ---
231
+
232
+ <div align="center">
233
+
234
+ **⭐ Beğendiyseniz yıldız vermeyi unutmayın!**
235
+
236
+ </div>
_archive/DEPLOY.md ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Terminal.AI — Production Deployment
2
+
3
+ VectorBT tabanlı finansal backtest ve LEAPS opsiyon tarama platformu.
4
+
5
+ ## Gereksinimler
6
+
7
+ - Docker >= 24.0
8
+ - Docker Compose >= 2.20
9
+ - Domain adı (SSL için)
10
+ - Sunucu: en az 1GB RAM, 1 vCPU
11
+
12
+ ## Hızlı Kurulum
13
+
14
+ ```bash
15
+ # 1. Projeyi klonla
16
+ git clone https://github.com/username/terminal-ai.git
17
+ cd HERMES(deneme)
18
+
19
+ # 2. Environment dosyasını oluştur
20
+ cp .env.example .env
21
+ # .env dosyasını domain'ine göre düzenle
22
+
23
+ # 3. Deploy et
24
+ chmod +x deploy.sh
25
+ ./deploy.sh
26
+ ```
27
+
28
+ ## SSL Kurulumu (Opsiyonel ama önerilir)
29
+
30
+ ```bash
31
+ # Domain'i DNS'de sunucu IP'sine yönlendirdikten sonra:
32
+ chmod +x setup-ssl.sh
33
+ ./setup-ssl.sh yourdomain.com
34
+ ```
35
+
36
+ ## Komutlar
37
+
38
+ ```bash
39
+ # Çalıştır
40
+ docker compose up -d
41
+
42
+ # Durdur
43
+ docker compose down
44
+
45
+ # Logları izle
46
+ docker compose logs -f
47
+
48
+ # Sadece backend/backend logları
49
+ docker compose logs -f backend
50
+ docker compose logs -f frontend
51
+
52
+ # Yeniden build & deploy
53
+ docker compose up -d --build
54
+
55
+ # Container durumu
56
+ docker compose ps
57
+ ```
58
+
59
+ ## Yapı
60
+
61
+ ```
62
+ ├── backend/ # FastAPI + vectorbt + yfinance
63
+ ├── frontend/ # Next.js + React + Plotly
64
+ ├── nginx/ # Reverse proxy + SSL
65
+ ├── docker-compose.yml
66
+ ├── deploy.sh
67
+ └── setup-ssl.sh
68
+ ```
69
+
70
+ ## Portlar
71
+
72
+ - 80 → Nginx (HTTP)
73
+ - 443 → Nginx (HTTPS, SSL aktifken)
74
+ - 3000 → Next.js (sadece internal)
75
+ - 8000 → FastAPI (sadece internal)
76
+
77
+ ## Notlar
78
+
79
+ - Backend cache: 10 dakika (in-memory, container restart'ında temizlenir)
80
+ - LEAPS Scanner: S&P 100 taraması ~10 saniye
81
+ - yfinance rate limit: ~2000 saatlik istek (IP bazlı)
82
+ - SSL olmadan da çalışır (HTTP)
_archive/Dockerfile.backend ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy application code
10
+ COPY . .
11
+
12
+ # Expose port
13
+ EXPOSE 8000
14
+
15
+ # Run with uvicorn
16
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
_archive/Dockerfile.frontend ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage build for Next.js frontend
2
+ FROM node:20-alpine AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy package files
7
+ COPY package.json package-lock.json* ./
8
+
9
+ # Install dependencies
10
+ RUN npm ci
11
+
12
+ # Copy source code
13
+ COPY . .
14
+
15
+ # Build the application
16
+ RUN npm run build
17
+
18
+ # Production stage
19
+ FROM node:20-alpine AS runner
20
+
21
+ WORKDIR /app
22
+
23
+ # Copy built assets from builder
24
+ COPY --from=builder /app/.next ./.next
25
+ COPY --from=builder /app/public ./public
26
+ COPY --from=builder /app/package.json ./package.json
27
+ COPY --from=builder /app/node_modules ./node_modules
28
+
29
+ # Expose port
30
+ EXPOSE 3000
31
+
32
+ # Start the application
33
+ CMD ["npm", "start"]
_archive/deploy.sh ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Terminal.AI Deploy Script
3
+ # Kullanım: ./deploy.sh
4
+
5
+ set -e
6
+
7
+ echo "========================================="
8
+ echo " Terminal.AI Deployment"
9
+ echo "========================================="
10
+
11
+ # Renkler
12
+ RED='\033[0;31m'
13
+ GREEN='\033[0;32m'
14
+ YELLOW='\033[1;33m'
15
+ NC='\033[0m'
16
+
17
+ # Dizin kontrol
18
+ if [ ! -f "docker-compose.yml" ]; then
19
+ echo -e "${RED}Hata: docker-compose.yml bulunamadı!${NC}"
20
+ echo "Lütfen proje kök dizininde çalıştırın."
21
+ exit 1
22
+ fi
23
+
24
+ # SSL dizinleri oluştur
25
+ mkdir -p nginx/ssl
26
+
27
+ # Güncel kodu çek (git kullanıyorsan)
28
+ if [ -d ".git" ]; then
29
+ echo -e "${YELLOW}Güncel kod çekiliyor...${NC}"
30
+ git pull origin main 2>/dev/null || true
31
+ fi
32
+
33
+ # Docker Compose ile build & deploy
34
+ echo -e "${YELLOW}Container'lar build ediliyor...${NC}"
35
+ docker compose build --no-cache
36
+
37
+ echo -e "${YELLOW}Container'lar başlatılıyor...${NC}"
38
+ docker compose up -d
39
+
40
+ # Health check
41
+ echo -e "${YELLOW}Health check...${NC}"
42
+ sleep 5
43
+
44
+ if curl -sf http://localhost/api/health > /dev/null 2>&1; then
45
+ echo -e "${GREEN}Backend: OK${NC}"
46
+ else
47
+ echo -e "${RED}Backend: FAIL${NC}"
48
+ fi
49
+
50
+ if curl -sf http://localhost > /dev/null 2>&1; then
51
+ echo -e "${GREEN}Frontend: OK${NC}"
52
+ else
53
+ echo -e "${RED}Frontend: FAIL${NC}"
54
+ fi
55
+
56
+ echo ""
57
+ echo -e "${GREEN}=========================================${NC}"
58
+ echo -e "${GREEN} Deployment tamamlandı!${NC}"
59
+ echo -e "${GREEN}=========================================${NC}"
60
+ echo ""
61
+ echo "Logları izle: docker compose logs -f"
62
+ echo "Durdur: docker compose down"
63
+ echo "Yeniden başlat: docker compose restart"
_archive/docker-compose.yml ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "3.8"
2
+
3
+ services:
4
+ backend:
5
+ build:
6
+ context: ./backend
7
+ dockerfile: Dockerfile
8
+ container_name: terminal-ai-backend
9
+ restart: unless-stopped
10
+ expose:
11
+ - "8000"
12
+ environment:
13
+ - PYTHONUNBUFFERED=1
14
+ volumes:
15
+ - backend-data:/app/data
16
+ networks:
17
+ - terminal-ai
18
+ healthcheck:
19
+ test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
20
+ interval: 30s
21
+ timeout: 10s
22
+ retries: 3
23
+
24
+ frontend:
25
+ build:
26
+ context: ./frontend
27
+ dockerfile: Dockerfile
28
+ container_name: terminal-ai-frontend
29
+ restart: unless-stopped
30
+ expose:
31
+ - "3000"
32
+ environment:
33
+ - NEXT_PUBLIC_API_URL=/api
34
+ depends_on:
35
+ backend:
36
+ condition: service_healthy
37
+ networks:
38
+ - terminal-ai
39
+
40
+ nginx:
41
+ image: nginx:alpine
42
+ container_name: terminal-ai-nginx
43
+ restart: unless-stopped
44
+ ports:
45
+ - "80:80"
46
+ - "443:443"
47
+ volumes:
48
+ - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
49
+ - ./nginx/ssl:/etc/nginx/ssl:ro
50
+ - certbot-webroot:/var/www/certbot:ro
51
+ - certbot-certs:/etc/letsencrypt:ro
52
+ depends_on:
53
+ - frontend
54
+ - backend
55
+ networks:
56
+ - terminal-ai
57
+
58
+ # Opsiyonel: SSL sertifikası için Certbot
59
+ # İlk kurulumda comment out edildi, sonra aktif edilecek
60
+ # certbot:
61
+ # image: certbot/certbot
62
+ # container_name: terminal-ai-certbot
63
+ # volumes:
64
+ # - certbot-webroot:/var/www/certbot
65
+ # - certbot-certs:/etc/letsencrypt
66
+ # entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
67
+
68
+ volumes:
69
+ backend-data:
70
+ certbot-webroot:
71
+ certbot-certs:
72
+
73
+ networks:
74
+ terminal-ai:
75
+ driver: bridge
_archive/nginx/nginx.conf ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ events {
2
+ worker_connections 1024;
3
+ }
4
+
5
+ http {
6
+ include /etc/nginx/mime.types;
7
+ default_type application/octet-stream;
8
+
9
+ # Logging
10
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
11
+ '$status $body_bytes_sent "$http_referer" '
12
+ '"$http_user_agent" "$http_x_forwarded_for"';
13
+ access_log /var/log/nginx/access.log main;
14
+ error_log /var/log/nginx/error.log;
15
+
16
+ # Performance
17
+ sendfile on;
18
+ tcp_nopush on;
19
+ tcp_nodelay on;
20
+ keepalive_timeout 65;
21
+ types_hash_max_size 2048;
22
+
23
+ # Gzip compression
24
+ gzip on;
25
+ gzip_vary on;
26
+ gzip_proxied any;
27
+ gzip_comp_level 6;
28
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
29
+
30
+ # Rate limiting
31
+ limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
32
+ limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
33
+
34
+ # Upstream servers
35
+ upstream frontend {
36
+ server frontend:3000;
37
+ }
38
+
39
+ upstream backend {
40
+ server backend:8000;
41
+ }
42
+
43
+ # HTTP → HTTPS redirect (SSL aktifken)
44
+ # server {
45
+ # listen 80;
46
+ # server_name yourdomain.com www.yourdomain.com;
47
+ # location /.well-known/acme-challenge/ {
48
+ # root /var/www/certbot;
49
+ # }
50
+ # location / {
51
+ # return 301 https://$server_name$request_uri;
52
+ # }
53
+ # }
54
+
55
+ # HTTPS server (SSL aktifken)
56
+ # server {
57
+ # listen 443 ssl http2;
58
+ # server_name yourdomain.com www.yourdomain.com;
59
+ #
60
+ # ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
61
+ # ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
62
+ # ssl_protocols TLSv1.2 TLSv1.3;
63
+ # ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
64
+ # ssl_prefer_server_ciphers off;
65
+ #
66
+ # # Security headers
67
+ # add_header X-Frame-Options "SAMEORIGIN" always;
68
+ # add_header X-Content-Type-Options "nosniff" always;
69
+ # add_header X-XSS-Protection "1; mode=block" always;
70
+ # add_header Referrer-Policy "strict-origin-when-cross-origin" always;
71
+ #
72
+ # # Frontend
73
+ # location / {
74
+ # limit_req zone=general burst=50 nodelay;
75
+ # proxy_pass http://frontend;
76
+ # proxy_http_version 1.1;
77
+ # proxy_set_header Upgrade $http_upgrade;
78
+ # proxy_set_header Connection "upgrade";
79
+ # proxy_set_header Host $host;
80
+ # proxy_set_header X-Real-IP $remote_addr;
81
+ # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
82
+ # proxy_set_header X-Forwarded-Proto $scheme;
83
+ # }
84
+ #
85
+ # # Backend API
86
+ # location /api/ {
87
+ # limit_req zone=api burst=20 nodelay;
88
+ # proxy_pass http://backend;
89
+ # proxy_http_version 1.1;
90
+ # proxy_set_header Host $host;
91
+ # proxy_set_header X-Real-IP $remote_addr;
92
+ # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
93
+ # proxy_set_header X-Forwarded-Proto $scheme;
94
+ # proxy_read_timeout 120s;
95
+ # }
96
+ # }
97
+
98
+ # HTTP server (SSL'siz, ilk kurulum için)
99
+ server {
100
+ listen 80;
101
+ server_name _;
102
+
103
+ # Security headers
104
+ add_header X-Frame-Options "SAMEORIGIN" always;
105
+ add_header X-Content-Type-Options "nosniff" always;
106
+ add_header X-XSS-Protection "1; mode=block" always;
107
+
108
+ # Frontend
109
+ location / {
110
+ limit_req zone=general burst=50 nodelay;
111
+ proxy_pass http://frontend;
112
+ proxy_http_version 1.1;
113
+ proxy_set_header Upgrade $http_upgrade;
114
+ proxy_set_header Connection "upgrade";
115
+ proxy_set_header Host $host;
116
+ proxy_set_header X-Real-IP $remote_addr;
117
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
118
+ proxy_set_header X-Forwarded-Proto $scheme;
119
+ }
120
+
121
+ # Backend API
122
+ location /api/ {
123
+ limit_req zone=api burst=20 nodelay;
124
+ proxy_pass http://backend;
125
+ proxy_http_version 1.1;
126
+ proxy_set_header Host $host;
127
+ proxy_set_header X-Real-IP $remote_addr;
128
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
129
+ proxy_set_header X-Forwarded-Proto $scheme;
130
+ proxy_read_timeout 120s;
131
+ }
132
+
133
+ # Health check
134
+ location /health {
135
+ access_log off;
136
+ return 200 "OK\n";
137
+ }
138
+ }
139
+ }
_archive/setup-ssl.sh ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # SSL Sertifika Kurulumu (Let's Encrypt)
3
+ # Kullanım: ./setup-ssl.sh yourdomain.com
4
+
5
+ set -e
6
+
7
+ DOMAIN=$1
8
+
9
+ if [ -z "$DOMAIN" ]; then
10
+ echo "Kullanım: ./setup-ssh.sh yourdomain.com"
11
+ exit 1
12
+ fi
13
+
14
+ echo "SSL kurulumu başlıyor: $DOMAIN"
15
+
16
+ # Nginx config'i güncelle
17
+ sed -i "s/yourdomain.com/$DOMAIN/g" nginx/nginx.conf
18
+
19
+ # HTTP server'ı comment out, HTTPS server'ı aktif et
20
+ sed -i 's/^ # server {/ server {/' nginx/nginx.conf
21
+ sed -i 's/^ # listen 443/ listen 443/' nginx/nginx.conf
22
+ sed -i 's/^ # ssl_certificate/ ssl_certificate/' nginx/nginx.conf
23
+ # ... (tüm comment'leri kaldır)
24
+
25
+ # Certbot ile sertifika al
26
+ docker run -it --rm \
27
+ -v $(pwd)/certbot-webroot:/var/www/certbot \
28
+ -v $(pwd)/certbot-certs:/etc/letsencrypt \
29
+ certbot/certbot certonly \
30
+ --webroot \
31
+ --webroot-path=/var/www/certbot \
32
+ --email admin@$DOMAIN \
33
+ --agree-tos \
34
+ --no-eff-email \
35
+ -d $DOMAIN \
36
+ -d www.$DOMAIN
37
+
38
+ # Nginx'i yeniden başlat
39
+ docker compose restart nginx
40
+
41
+ echo "SSL kurulumu tamamlandı!"
42
+ echo "https://$DOMAIN adresinden erişebilirsiniz."
_archive/test_options.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Test if yfinance has options support and can detect unusual options activity."""
2
+ import yfinance as yf
3
+ import pandas as pd
4
+
5
+ print("=== yfinance Options Support Test ===\n")
6
+
7
+ # Test AAPL options
8
+ ticker = yf.Ticker("AAPL")
9
+ try:
10
+ opts = ticker.options
11
+ print(f"AAPL Option expiry dates available: {len(opts)}")
12
+ if opts:
13
+ print(f" Nearest expiry: {opts[0]}, Farthest: {opts[-1]}")
14
+
15
+ chain = ticker.option_chain(opts[0])
16
+ print(f"\n Calls columns: {list(chain.calls.columns)}")
17
+ print(f" Puts columns: {list(chain.puts.columns)}")
18
+
19
+ print("\n --- Top 5 Calls by volume ---")
20
+ top_calls = chain.calls.nlargest(5, 'volume')[['strike', 'lastPrice', 'volume', 'openInterest', 'impliedVolatility', 'contractSymbol']]
21
+ print(top_calls.to_string(index=False))
22
+
23
+ print("\n --- Top 5 Puts by volume ---")
24
+ top_puts = chain.puts.nlargest(5, 'volume')[['strike', 'lastPrice', 'volume', 'openInterest', 'impliedVolatility', 'contractSymbol']]
25
+ print(top_puts.to_string(index=False))
26
+ except Exception as e:
27
+ print(f"AAPL options error: {e}")
28
+
29
+ # Test multiple tickers for LEAPS (far OTM calls can be LEAPS proxy)
30
+ print("\n\n=== Multi-ticker Options Test (LEAPS focus) ===\n")
31
+ for sym in ['AAPL', 'TSLA', 'NVDA', 'MSFT', 'SPY', 'AMZN']:
32
+ try:
33
+ t = yf.Ticker(sym)
34
+ o = t.options
35
+ if o:
36
+ # Find far-dated expiry (more than 6 months out = potential LEAPS)
37
+ from datetime import datetime
38
+ today = datetime.today()
39
+ far_expiries = [
40
+ exp for exp in o
41
+ if (datetime.strptime(exp, '%Y-%m-%d') - today).days > 180
42
+ ]
43
+ print(f"{sym}: {len(o)} expiries, {len(far_expiries)} LEAPS-range (>6mo)")
44
+
45
+ # If far expiries exist, check call volume
46
+ if far_expiries:
47
+ far_chain = t.option_chain(far_expiries[0])
48
+ total_call_vol = far_chain.calls['volume'].sum()
49
+ total_put_vol = far_chain.puts['volume'].sum()
50
+ print(f" Far expiry {far_expiries[0]}: call_vol={total_call_vol}, put_vol={total_put_vol}")
51
+ else:
52
+ print(f"{sym}: No options data")
53
+ except Exception as e:
54
+ print(f"{sym}: Error - {e}")
_archive/test_options2.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Explore what 'abnormal options activity' means + yfinance options schema."""
2
+ import yfinance as yf
3
+ import pandas as pd
4
+ from datetime import datetime
5
+
6
+ print("=== Contract Symbol Format ===\n")
7
+ ticker = yf.Ticker("AAPL")
8
+ opts = ticker.options
9
+ today = datetime.today()
10
+
11
+ # Analyze all expiries to find LEAPS pattern and high-volume activities
12
+ print(f"All expiries for AAPL ({len(opts)} total):")
13
+ for i, exp in enumerate(opts):
14
+ try:
15
+ dt = datetime.strptime(exp, "%Y-%m-%d")
16
+ days_out = (dt - today).days
17
+ is_leaps = days_out > 365
18
+ flag = " <<< LEAPS" if is_leaps else ""
19
+ print(f" {exp} ({days_out}d from now){flag}")
20
+ except:
21
+ print(f" {exp} (parse error)")
22
+
23
+ # Fetch the farthest expiry for LEAPS analysis
24
+ if opts:
25
+ far_exp = opts[-1]
26
+ print(f"\n=== Far expiry chain: {far_exp} ===")
27
+ chain = ticker.option_chain(far_exp)
28
+
29
+ calls = chain.calls.copy()
30
+ puts = chain.puts.copy()
31
+
32
+ print(f"\nCalls: {len(calls)} rows")
33
+ print(f"Puts: {len(puts)} rows")
34
+
35
+ # Unusual call activity: high volume vs OI call ratio > 2
36
+ print("\n--- Top 10 calls by volume/oi ratio (> 2 = unusual) ---")
37
+ calls_active = calls[calls['volume'] > 0].copy()
38
+ calls_active['vol_oi_ratio'] = calls_active['volume'] / (calls_active['openInterest'] + 1)
39
+ unusual_calls = calls_active[calls_active['vol_oi_ratio'] > 2].nlargest(10, 'volume')
40
+ print(unusual_calls[['strike', 'volume', 'openInterest', 'vol_oi_ratio', 'impliedVolatility', 'contractSymbol']].to_string(index=False))
41
+
42
+ # Unusual put activity: high volume vs OI put ratio > 2
43
+ print("\n--- Top 10 puts by volume/oi ratio (> 2 = unusual) ---")
44
+ puts_active = puts[puts['volume'] > 0].copy()
45
+ puts_active['vol_oi_ratio'] = puts_active['volume'] / (puts_active['openInterest'] + 1)
46
+ unusual_puts = puts_active[puts_active['vol_oi_ratio'] > 2].nlargest(10, 'volume')
47
+ print(unusual_puts[['strike', 'volume', 'openInterest', 'vol_oi_ratio', 'impliedVolatility', 'contractSymbol']].to_string(index=False))
48
+
49
+ # Overall option flow signal
50
+ total_call_vol = calls['volume'].sum()
51
+ total_put_vol = puts['volume'].sum()
52
+ cpr = total_call_vol / (total_put_vol + 1)
53
+ print(f"\nCall/Put Volume Ratio: {cpr:.2f} (higher = bullish)")
54
+ print(f"Total Call Volume: {total_call_vol:,.0f}")
55
+ print(f"Total Put Volume: {total_put_vol:,.0f}")
56
+
57
+ # Highest IV strikes (potential LEAPS)
58
+ high_iv = calls.nlargest(5, 'impliedVolatility')[['strike', 'volume', 'openInterest', 'impliedVolatility', 'contractSymbol']]
59
+ print(f"\n--- 5 highest IV calls ---")
60
+ print(high_iv.to_string(index=False))
backend/.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ .git/
6
+ *.log
7
+ test_*.py
8
+ tmp/
backend/.gitignore ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Backend .gitignore
2
+ venv/
3
+ .venv/
4
+ __pycache__/
5
+ *.pyc
6
+ *.pyo
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ *.egg-info/
10
+ dist/
11
+ build/
12
+ .env
13
+ .env.local
14
+ *.log
15
+ test_*.py
16
+ tmp/
backend/data_fetcher.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Data fetching module using yfinance.
3
+ Provides OHLCV data for given ticker and date range.
4
+ """
5
+
6
+ import yfinance as yf
7
+ import pandas as pd
8
+ from datetime import datetime
9
+
10
+
11
+ def fetch_ohlcv(
12
+ ticker: str,
13
+ start: str,
14
+ end: str,
15
+ interval: str = "1d"
16
+ ) -> pd.DataFrame:
17
+ """
18
+ Fetch OHLCV data from Yahoo Finance.
19
+
20
+ Args:
21
+ ticker: Stock/crypto ticker symbol (e.g., "AAPL", "BTC-USD")
22
+ start: Start date in YYYY-MM-DD format
23
+ end: End date in YYYY-MM-DD format
24
+ interval: Data interval - "1m", "5m", "15m", "1h", "1d", "1wk", "1mo"
25
+
26
+ Returns:
27
+ pd.DataFrame with columns: Open, High, Low, Close, Volume
28
+ """
29
+ try:
30
+ asset = yf.Ticker(ticker)
31
+ df = asset.history(start=start, end=end, interval=interval)
32
+
33
+ if df.empty:
34
+ raise ValueError(f"No data found for {ticker} in range {start} to {end}")
35
+
36
+ # Standardize column names
37
+ df.index.name = "Date"
38
+ df = df[["Open", "High", "Low", "Close", "Volume"]]
39
+
40
+ # Remove timezone info from index for JSON compat
41
+ if df.index.tz is not None:
42
+ df.index = df.index.tz_localize(None)
43
+
44
+ return df
45
+
46
+ except Exception as e:
47
+ raise RuntimeError(f"Failed to fetch data for {ticker}: {str(e)}")
backend/encoder.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NanEncoder - Custom JSON serializer for vectorbt output.
3
+ Handles NaN, Inf, datetime, numpy types, and pandas objects.
4
+ """
5
+
6
+ import json
7
+ import math
8
+ import numpy as np
9
+ import pandas as pd
10
+ from datetime import datetime, date
11
+
12
+
13
+ class NanEncoder(json.JSONEncoder):
14
+ """Custom JSON encoder that handles vectorbt/pandas/numpy edge cases."""
15
+
16
+ def default(self, obj):
17
+ # numpy types
18
+ if isinstance(obj, (np.integer,)):
19
+ return int(obj)
20
+ if isinstance(obj, (np.floating,)):
21
+ if np.isnan(obj) or np.isinf(obj):
22
+ return None
23
+ return float(obj)
24
+ if isinstance(obj, np.ndarray):
25
+ return obj.tolist()
26
+ if isinstance(obj, (np.bool_,)):
27
+ return bool(obj)
28
+
29
+ # pandas types
30
+ if isinstance(obj, pd.Timestamp):
31
+ return obj.isoformat()
32
+ if isinstance(obj, pd.Series):
33
+ return obj.to_dict()
34
+ if isinstance(obj, pd.DataFrame):
35
+ return obj.to_dict(orient="records")
36
+ if isinstance(obj, pd.Timedelta):
37
+ return str(obj)
38
+
39
+ # datetime
40
+ if isinstance(obj, (datetime, date)):
41
+ return obj.isoformat()
42
+
43
+ # timedelta
44
+ from datetime import timedelta
45
+ if isinstance(obj, timedelta):
46
+ return str(obj)
47
+
48
+ return super().default(obj)
49
+
50
+
51
+ def safe_json_response(data: dict) -> dict:
52
+ """
53
+ Convert a dict with numpy/pandas values to JSON-safe dict.
54
+ Replaces NaN/Inf with None, converts timestamps to ISO strings.
55
+ """
56
+ def _clean(value):
57
+ if isinstance(value, dict):
58
+ return {k: _clean(v) for k, v in value.items()}
59
+ if isinstance(value, (list, tuple)):
60
+ return [_clean(v) for v in value]
61
+ if isinstance(value, (np.floating, float)):
62
+ if math.isnan(value) or math.isinf(value):
63
+ return None
64
+ return float(value)
65
+ if isinstance(value, (np.integer,)):
66
+ return int(value)
67
+ if isinstance(value, (np.bool_,)):
68
+ return bool(value)
69
+ if isinstance(value, np.ndarray):
70
+ return _clean(value.tolist())
71
+ if isinstance(value, pd.Timestamp):
72
+ return value.isoformat()
73
+ if isinstance(value, (datetime, date)):
74
+ return value.isoformat()
75
+ if isinstance(value, pd.Timedelta):
76
+ return str(value)
77
+ if isinstance(value, pd.Series):
78
+ return _clean(value.to_dict())
79
+ if isinstance(value, pd.DataFrame):
80
+ return _clean(value.to_dict(orient="records"))
81
+ if pd.isna(value):
82
+ return None
83
+ return value
84
+
85
+ return _clean(data)
backend/leaps_scanner.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LEAPS Scanner — S&P 100 tabanlı concurrent LEAPS anomali tarama.
3
+ Yakın zamanda yüksek miktarda LEAPS opsiyonu alınan hisseleri tespit eder.
4
+
5
+ Kaynak: yfinance (ücretsiz, ~15-20 dakika gecikmeli veri)
6
+ Strateji: S&P 100 ticker listesini concurrent tarayarak volume/OI > 2x LEAPS spike yakalar.
7
+ """
8
+
9
+ import yfinance as yf
10
+ import pandas as pd
11
+ import numpy as np
12
+ from datetime import datetime, timedelta
13
+ from concurrent.futures import ThreadPoolExecutor, as_completed
14
+ from typing import Optional
15
+
16
+ # ── Constants ──────────────────────────────────────────────────────────
17
+
18
+ LEAPS_MIN_DAYS = 365 # Vade > 1 yıl = LEAPS
19
+ UNUSUAL_VOL_OI_MIN = 2.0 # volume/openInterest >= 2x = anormal
20
+ MAX_WORKERS = 10 # Concurrent thread sayısı
21
+ TIMEOUT_PER_TICKER = 15 # Ticker başına saniye timeout
22
+
23
+ # S&P 100 ticker listesi (OEX — 100 büyük hacimli hisse)
24
+ SP100_TICKERS = [
25
+ "AAPL", "MSFT", "GOOGL", "GOOG", "AMZN", "NVDA", "TSLA", "META",
26
+ "BRK-B", "JPM", "V", "JNJ", "WMT", "PG", "MA", "UNH", "HD", "DIS",
27
+ "PYPL", "BAC", "XOM", "INTC", "VZ", "ADBE", "NFLX", "CRM", "CSCO",
28
+ "PFE", "ABT", "ACN", "TMO", "AVGO", "DHR", "NKE", "TXN", "QCOM",
29
+ "COST", "NEE", "LIN", "PM", "BMY", "HON", "UNP", "LOW", "IBM",
30
+ "RTX", "SBUX", "AMD", "INTU", "GS", "CAT", "MDT", "AMGN", "BLK",
31
+ "GILD", "ADI", "ISRG", "BKNG", "T", "PLTR", "NOW", "SPGI", "MO",
32
+ "ELV", "SYK", "ZTS", "ADP", "MMC", "CB", "CI", "SO", "DUK",
33
+ "BDX", "TMUS", "SCHW", "EOG", "REGN", "LRCX", "PGR", "APD", "CL",
34
+ "SHW", "SLB", "ETN", "AON", "ITW", "BSX", "CME", "HUM", "MRNA",
35
+ "ABNB", "ORLY", "ATVI", "MCO", "KLAC", "PANW", "SNPS", "CDNS",
36
+ "FTNT", "NXPI", "ADM", "KDP", "MNST", "MCHP", "CTAS", "PAYX",
37
+ "AEP", "EXC", "AZN", "LHX", "NOC", "STZ", "KHC", "MAR", "DXCM",
38
+ ]
39
+
40
+
41
+ # ── Helpers ────────────────────────────────────────────────────────────
42
+
43
+ def _safe_ratio(num, den, default=0.0):
44
+ if den == 0 or pd.isna(den):
45
+ return default
46
+ return float(num) / float(den)
47
+
48
+
49
+ def _is_leaps(expiry_date_str: str) -> bool:
50
+ """Vade > 365 gün ise LEAPS."""
51
+ try:
52
+ dt = datetime.strptime(expiry_date_str, "%Y-%m-%d")
53
+ return (dt - datetime.today()).days >= LEAPS_MIN_DAYS
54
+ except ValueError:
55
+ return False
56
+
57
+
58
+ def scan_ticker_leaps(sym: str) -> Optional[dict]:
59
+ """
60
+ Tek ticker tarama: LEAPS opsiyonlarda anormal hacim/OI spike ara.
61
+ Sonuç bulunamazsa None döner.
62
+ """
63
+ try:
64
+ stock = yf.Ticker(sym)
65
+ expiry_dates = stock.options
66
+ if not expiry_dates:
67
+ return None
68
+
69
+ # LEAPS vadelerini belirle
70
+ leaps_expiries = [exp for exp in expiry_dates if _is_leaps(exp)]
71
+ if not leaps_expiries:
72
+ return None
73
+
74
+ total_leaps_call_vol = 0
75
+ total_leaps_put_vol = 0
76
+ total_leaps_call_oi = 0
77
+ total_leaps_put_oi = 0
78
+ unusual_spikes = []
79
+
80
+ for exp in leaps_expiries:
81
+ try:
82
+ chain = stock.option_chain(exp)
83
+ except Exception:
84
+ continue
85
+
86
+ for is_call, df in [(True, chain.calls), (False, chain.puts)]:
87
+ if df.empty:
88
+ continue
89
+
90
+ active = df[df["volume"] > 0].copy()
91
+ if active.empty:
92
+ continue
93
+
94
+ # Volume/OI ratio hesapla
95
+ active["vol_oi"] = active.apply(
96
+ lambda r: _safe_ratio(r.get("volume", 0), r.get("openInterest", 0)),
97
+ axis=1,
98
+ )
99
+
100
+ # Toplam hacim/OI
101
+ vol_sum = int(active["volume"].sum())
102
+ oi_sum = int(active["openInterest"].sum())
103
+ if is_call:
104
+ total_leaps_call_vol += vol_sum
105
+ total_leaps_call_oi += oi_sum
106
+ else:
107
+ total_leaps_put_vol += vol_sum
108
+ total_leaps_put_oi += oi_sum
109
+
110
+ # Anormal spike'lar: vol/OI >= threshold
111
+ spikes = active[active["vol_oi"] >= UNUSUAL_VOL_OI_MIN]
112
+ for _, row in spikes.iterrows():
113
+ unusual_spikes.append({
114
+ "type": "LEAPS CALL" if is_call else "LEAPS PUT",
115
+ "expiry": exp,
116
+ "strike": float(row.get("strike", 0)),
117
+ "volume": int(row.get("volume", 0)),
118
+ "openInterest": int(row.get("openInterest", 0)),
119
+ "vol_oi_ratio": round(float(row.get("vol_oi", 0)), 1),
120
+ "lastPrice": float(row.get("lastPrice", 0)) if pd.notna(row.get("lastPrice")) else 0,
121
+ "impliedVolatility": round(float(row.get("impliedVolatility", 0)), 4) if pd.notna(row.get("impliedVolatility")) else 0,
122
+ "signal": "BULLISH" if is_call else "BEARISH",
123
+ })
124
+
125
+ if not unusual_spikes:
126
+ return None
127
+
128
+ # Özet hesapla
129
+ total_vol = total_leaps_call_vol + total_leaps_put_vol
130
+ cp_ratio = _safe_ratio(total_leaps_call_vol, total_leaps_put_vol)
131
+
132
+ # En büyük spike'ı bul
133
+ top_spike = max(unusual_spikes, key=lambda x: x["volume"])
134
+
135
+ return {
136
+ "ticker": sym,
137
+ "total_leaps_call_vol": total_leaps_call_vol,
138
+ "total_leaps_put_vol": total_leaps_put_vol,
139
+ "total_leaps_vol": total_vol,
140
+ "call_put_ratio": round(cp_ratio, 2),
141
+ "unusual_count": len(unusual_spikes),
142
+ "top_spike": top_spike,
143
+ "all_spikes": unusual_spikes[:10], # Max 10 spike
144
+ "leaps_expiries_count": len(leaps_expiries),
145
+ "nearest_leaps_expiry": leaps_expiries[0] if leaps_expiries else None,
146
+ "scan_time": datetime.utcnow().isoformat(),
147
+ }
148
+
149
+ except Exception:
150
+ return None
151
+
152
+
153
+ def scan_all_sp100_leaps(max_tickers: int = 100) -> list[dict]:
154
+ """
155
+ S&P 100 ticker listesini concurrent tarayarak LEAPS anomali bulur.
156
+ Sonuçları toplam LEAPS hacmine göre sıralı döndürür.
157
+
158
+ Parametre:
159
+ max_tickers: Taranacak maks ticker sayısı (varsayılan 100, S&P 100)
160
+
161
+ Dönüş:
162
+ list[dict]: LEAPS anomali bulunan ticker'lar, hacme göre sıralı
163
+ """
164
+ tickers = SP100_TICKERS[:max_tickers]
165
+ results = []
166
+
167
+ with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
168
+ future_to_sym = {
169
+ executor.submit(scan_ticker_leaps, sym): sym
170
+ for sym in tickers
171
+ }
172
+ for future in as_completed(future_to_sym, timeout=120):
173
+ try:
174
+ result = future.result(timeout=TIMEOUT_PER_TICKER)
175
+ if result is not None:
176
+ results.append(result)
177
+ except Exception:
178
+ continue
179
+
180
+ # Toplam LEAPS hacmine göre sırala
181
+ results.sort(key=lambda x: x["total_leaps_vol"], reverse=True)
182
+ return results
backend/main.py ADDED
@@ -0,0 +1,530 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Terminal.AI - VectorBT Financial Analysis Backend
3
+ FastAPI server providing backtesting and technical analysis endpoints.
4
+ """
5
+
6
+ from fastapi import FastAPI, HTTPException
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from pydantic import BaseModel, Field
9
+ from typing import Optional, Literal
10
+ import pandas as pd
11
+ import time
12
+ import os
13
+ from datetime import datetime
14
+
15
+ from data_fetcher import fetch_ohlcv
16
+ from strategies import (
17
+ run_strategy, run_all_strategies, STRATEGIES,
18
+ calculate_rsi, calculate_macd,
19
+ )
20
+ from options_flow import (
21
+ fetch_options_flow, rank_tickers_by_leaps_activity, rank_single_ticker,
22
+ DEFAULT_SCAN_TICKERS,
23
+ )
24
+ from leaps_scanner import scan_all_sp100_leaps
25
+ from encoder import safe_json_response
26
+
27
+ app = FastAPI(
28
+ title="Terminal.AI",
29
+ description="VectorBT-based financial backtesting and analysis platform",
30
+ version="1.1.0"
31
+ )
32
+
33
+ # CORS - allow Next.js frontend
34
+ # Production'da FRONTEND_URL env'den okunur (Vercel URL'i)
35
+ # Development'da localhost:3000 varsayılan
36
+ import os as _os
37
+ _cors_origins = ["http://localhost:3000"]
38
+ _frontend_url = _os.environ.get("FRONTEND_URL", "")
39
+ if _frontend_url:
40
+ _cors_origins.append(_frontend_url)
41
+ # https:// ön ekini de ekle (Vercel bazen https döndürür)
42
+ if _frontend_url.startswith("https://"):
43
+ _cors_origins.append(_frontend_url.replace("https://", "http://"))
44
+ elif _frontend_url.startswith("http://"):
45
+ _cors_origins.append(_frontend_url.replace("http://", "https://"))
46
+
47
+ app.add_middleware(
48
+ CORSMiddleware,
49
+ allow_origins=_cors_origins,
50
+ allow_credentials=True,
51
+ allow_methods=["*"],
52
+ allow_headers=["*"],
53
+ )
54
+
55
+ # ── Simple in-memory cache ──────────────────────────────────────────
56
+ _cache: dict = {}
57
+ _CACHE_TTL = 600 # 10 minutes
58
+
59
+
60
+ def _cached(key: str, ttl: int = _CACHE_TTL):
61
+ """Decorator-like helper: return cached value or None if expired/missing."""
62
+ entry = _cache.get(key)
63
+ if entry and time.time() - entry["ts"] < ttl:
64
+ return entry["data"]
65
+ return None
66
+
67
+
68
+ def _set_cache(key: str, data):
69
+ _cache[key] = {"data": data, "ts": time.time()}
70
+
71
+
72
+ # ── Request / Response Models ──────────────────────────────────────
73
+
74
+ class BacktestRequest(BaseModel):
75
+ ticker: str = Field(..., description="Ticker symbol, e.g. AAPL, BTC-USD")
76
+ start_date: str = Field(..., description="Start date YYYY-MM-DD")
77
+ end_date: str = Field(..., description="End date YYYY-MM-DD")
78
+ interval: str = Field(default="1d", description="Data interval: 1d, 1h, 1wk, etc.")
79
+ strategy: str = Field(default="sma", description="Strategy: sma, ema, bollinger")
80
+ fast_ma: int = Field(default=10, ge=2, le=200, description="Fast MA period (SMA/EMA)")
81
+ slow_ma: int = Field(default=30, ge=2, le=500, description="Slow MA period (SMA/EMA)")
82
+ bb_window: int = Field(default=20, ge=2, le=200, description="Bollinger Bands window")
83
+ bb_std: float = Field(default=2.0, ge=0.5, le=4.0, description="Bollinger Bands std dev")
84
+ fees: float = Field(default=0.001, ge=0, le=0.1, description="Transaction fee")
85
+ init_cash: float = Field(default=10000.0, ge=100, description="Initial cash")
86
+ rsi_window: int = Field(default=14, ge=2, le=100, description="RSI period")
87
+ macd_fast: int = Field(default=12, ge=2, le=100, description="MACD fast period")
88
+ macd_slow: int = Field(default=26, ge=2, le=200, description="MACD slow period")
89
+ macd_signal: int = Field(default=9, ge=2, le=100, description="MACD signal period")
90
+
91
+
92
+ class HealthResponse(BaseModel):
93
+ status: str
94
+ version: str
95
+
96
+
97
+ # ── Helpers ──────────────────────────────────────────────────────────
98
+
99
+ def _get_strategy_kwargs(request: BacktestRequest) -> dict:
100
+ """Extract strategy-specific kwargs from request."""
101
+ if request.strategy == "bollinger":
102
+ return {
103
+ "window": request.bb_window,
104
+ "std": request.bb_std,
105
+ "fees": request.fees,
106
+ "init_cash": request.init_cash,
107
+ }
108
+ else:
109
+ return {
110
+ "fast_window": request.fast_ma,
111
+ "slow_window": request.slow_ma,
112
+ "fees": request.fees,
113
+ "init_cash": request.init_cash,
114
+ }
115
+
116
+
117
+ def _build_backtest_response(df: pd.DataFrame, result: dict, request: BacktestRequest) -> dict:
118
+ """Build the standard backtest response dict."""
119
+ close = df["Close"]
120
+ dates = close.index.tolist()
121
+
122
+ # Calculate indicators
123
+ rsi_data = calculate_rsi(close, window=request.rsi_window)
124
+ macd_data = calculate_macd(
125
+ close=close,
126
+ fast_window=request.macd_fast,
127
+ slow_window=request.macd_slow,
128
+ signal_window=request.macd_signal,
129
+ )
130
+
131
+ response = {
132
+ "ticker": request.ticker,
133
+ "start_date": request.start_date,
134
+ "end_date": request.end_date,
135
+ "interval": request.interval,
136
+ "strategy": request.strategy,
137
+ "data_points": len(df),
138
+ "dates": dates,
139
+ "ohlcv": {
140
+ "open": df["Open"].tolist(),
141
+ "high": df["High"].tolist(),
142
+ "low": df["Low"].tolist(),
143
+ "close": close.tolist(),
144
+ "volume": df["Volume"].tolist(),
145
+ },
146
+ "portfolio": {
147
+ "stats": result["stats"],
148
+ "trades": result["trades"],
149
+ "equity_curve": result["equity_curve"],
150
+ "returns": result["returns"],
151
+ "drawdown": result["drawdown"],
152
+ },
153
+ "indicators": {
154
+ "rsi": rsi_data,
155
+ "macd": macd_data,
156
+ },
157
+ "signals": {
158
+ "entries": result["entries"],
159
+ "exits": result["exits"],
160
+ },
161
+ }
162
+
163
+ # Add strategy-specific indicator data
164
+ if request.strategy == "sma":
165
+ response["indicators"]["sma"] = {
166
+ "fast": result["fast_sma"],
167
+ "slow": result["slow_sma"],
168
+ "fast_window": request.fast_ma,
169
+ "slow_window": request.slow_ma,
170
+ }
171
+ elif request.strategy == "ema":
172
+ response["indicators"]["ema"] = {
173
+ "fast": result["fast_ema"],
174
+ "slow": result["slow_ema"],
175
+ "fast_window": request.fast_ma,
176
+ "slow_window": request.slow_ma,
177
+ }
178
+ elif request.strategy == "bollinger":
179
+ response["indicators"]["bollinger"] = {
180
+ "middle": result["middle"],
181
+ "upper": result["upper"],
182
+ "lower": result["lower"],
183
+ "window": request.bb_window,
184
+ "std": request.bb_std,
185
+ }
186
+
187
+ return response
188
+
189
+
190
+ # ── Endpoints ──────────────────────────────────────────────────────
191
+
192
+ @app.get("/api/health", response_model=HealthResponse)
193
+ async def health_check():
194
+ """Health check endpoint."""
195
+ return {"status": "ok", "version": "1.1.0"}
196
+
197
+
198
+ @app.get("/api/strategies")
199
+ async def list_strategies():
200
+ """List available strategies and their default parameters."""
201
+ return {
202
+ "strategies": [
203
+ {
204
+ "id": "sma",
205
+ "name": "SMA Crossover",
206
+ "description": "Buy when fast SMA crosses above slow SMA",
207
+ "params": {"fast_window": 10, "slow_window": 30},
208
+ },
209
+ {
210
+ "id": "ema",
211
+ "name": "EMA Crossover",
212
+ "description": "Buy when fast EMA crosses above slow EMA",
213
+ "params": {"fast_window": 12, "slow_window": 26},
214
+ },
215
+ {
216
+ "id": "bollinger",
217
+ "name": "Bollinger Bands",
218
+ "description": "Mean reversion: buy at lower band, sell at upper band",
219
+ "params": {"window": 20, "std": 2.0},
220
+ },
221
+ ]
222
+ }
223
+
224
+
225
+ @app.post("/api/backtest")
226
+ async def backtest(request: BacktestRequest):
227
+ """Run a backtest with the selected strategy and indicators."""
228
+ try:
229
+ # 1. Fetch data
230
+ df = fetch_ohlcv(
231
+ ticker=request.ticker,
232
+ start=request.start_date,
233
+ end=request.end_date,
234
+ interval=request.interval,
235
+ )
236
+
237
+ close = df["Close"]
238
+
239
+ # 2. Run selected strategy
240
+ strategy_kwargs = _get_strategy_kwargs(request)
241
+ strategy_kwargs["freq"] = request.interval
242
+ result = run_strategy(request.strategy, close=close, **strategy_kwargs)
243
+
244
+ # 3. Build response
245
+ response = _build_backtest_response(df, result, request)
246
+ return safe_json_response(response)
247
+
248
+ except ValueError as e:
249
+ raise HTTPException(status_code=400, detail=str(e))
250
+ except RuntimeError as e:
251
+ raise HTTPException(status_code=500, detail=str(e))
252
+ except Exception as e:
253
+ raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}")
254
+
255
+
256
+ @app.post("/api/compare")
257
+ async def compare_strategies(request: BacktestRequest):
258
+ """Run all strategies and return comparison results."""
259
+ try:
260
+ df = fetch_ohlcv(
261
+ ticker=request.ticker,
262
+ start=request.start_date,
263
+ end=request.end_date,
264
+ interval=request.interval,
265
+ )
266
+
267
+ close = df["Close"]
268
+ dates = close.index.tolist()
269
+
270
+ # Run all strategies with default params + user's fees/cash/freq
271
+ comparison = run_all_strategies(
272
+ close=close,
273
+ fees=request.fees,
274
+ init_cash=request.init_cash,
275
+ freq=request.interval,
276
+ )
277
+
278
+ # Calculate indicators (same for all strategies)
279
+ rsi_data = calculate_rsi(close, window=request.rsi_window)
280
+ macd_data = calculate_macd(
281
+ close=close,
282
+ fast_window=request.macd_fast,
283
+ slow_window=request.macd_slow,
284
+ signal_window=request.macd_signal,
285
+ )
286
+
287
+ response = {
288
+ "ticker": request.ticker,
289
+ "start_date": request.start_date,
290
+ "end_date": request.end_date,
291
+ "interval": request.interval,
292
+ "data_points": len(df),
293
+ "dates": dates,
294
+ "ohlcv": {
295
+ "open": df["Open"].tolist(),
296
+ "high": df["High"].tolist(),
297
+ "low": df["Low"].tolist(),
298
+ "close": close.tolist(),
299
+ "volume": df["Volume"].tolist(),
300
+ },
301
+ "indicators": {
302
+ "rsi": rsi_data,
303
+ "macd": macd_data,
304
+ },
305
+ "comparison": comparison,
306
+ }
307
+
308
+ return safe_json_response(response)
309
+
310
+ except ValueError as e:
311
+ raise HTTPException(status_code=400, detail=str(e))
312
+ except RuntimeError as e:
313
+ raise HTTPException(status_code=500, detail=str(e))
314
+ except Exception as e:
315
+ raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}")
316
+
317
+
318
+ @app.get("/api/ticker/{ticker}")
319
+ async def get_ticker_info(ticker: str):
320
+ """Get basic ticker info and latest price."""
321
+ try:
322
+ df = fetch_ohlcv(ticker=ticker, start="2000-01-01", end="2030-12-31")
323
+ return safe_json_response({
324
+ "ticker": ticker,
325
+ "latest_close": df["Close"].iloc[-1] if not df.empty else None,
326
+ "data_points": len(df),
327
+ "date_range": {
328
+ "start": df.index[0].isoformat(),
329
+ "end": df.index[-1].isoformat(),
330
+ }
331
+ })
332
+ except Exception as e:
333
+ raise HTTPException(status_code=500, detail=str(e))
334
+
335
+
336
+ @app.get("/api/ticker/{ticker}/range")
337
+ async def get_ticker_range(ticker: str):
338
+ """Get the earliest and latest available date for a ticker."""
339
+ cache_key = f"range:{ticker.upper()}"
340
+ cached = _cached(cache_key)
341
+ if cached:
342
+ return cached
343
+
344
+ try:
345
+ df = fetch_ohlcv(ticker=ticker, start="2000-01-01", end="2030-12-31")
346
+ if df.empty:
347
+ raise HTTPException(status_code=404, detail=f"No data found for {ticker}")
348
+ result = {
349
+ "ticker": ticker,
350
+ "earliest": df.index[0].isoformat(),
351
+ "latest": df.index[-1].isoformat(),
352
+ "data_points": len(df),
353
+ }
354
+ _set_cache(cache_key, result)
355
+ return result
356
+ except ValueError as e:
357
+ raise HTTPException(status_code=400, detail=str(e))
358
+ except RuntimeError as e:
359
+ raise HTTPException(status_code=500, detail=str(e))
360
+ except Exception as e:
361
+ raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}")
362
+
363
+
364
+ @app.get("/api/ticker/{ticker}/fundamentals")
365
+ async def get_ticker_fundamentals(ticker: str):
366
+ """Get company fundamentals and valuation multiples."""
367
+ cache_key = f"fundamentals:{ticker.upper()}"
368
+ cached = _cached(cache_key)
369
+ if cached:
370
+ return cached
371
+
372
+ try:
373
+ import yfinance as yf
374
+ stock = yf.Ticker(ticker)
375
+ info = stock.info
376
+
377
+ def safe(v):
378
+ if v is None or v == "None":
379
+ return None
380
+ return v
381
+
382
+ result = {
383
+ "ticker": ticker,
384
+ "company": {
385
+ "name": safe(info.get("shortName")),
386
+ "sector": safe(info.get("sector")),
387
+ "industry": safe(info.get("industry")),
388
+ "summary": safe(info.get("longBusinessSummary")),
389
+ },
390
+ "valuation": {
391
+ "marketCap": safe(info.get("marketCap")),
392
+ "enterpriseValue": safe(info.get("enterpriseValue")),
393
+ "trailingPE": safe(info.get("trailingPE")),
394
+ "forwardPE": safe(info.get("forwardPE")),
395
+ "priceToBook": safe(info.get("priceToBook")),
396
+ "enterpriseToEbitda": safe(info.get("enterpriseToEbitda")),
397
+ "priceToSales": safe(info.get("priceToSalesTrailing12Months")),
398
+ },
399
+ "financials": {
400
+ "revenuePerShare": safe(info.get("revenuePerShare")),
401
+ "earningsPerShare": safe(info.get("trailingEps")),
402
+ "profitMargins": safe(info.get("profitMargins")),
403
+ "operatingMargins": safe(info.get("operatingMargins")),
404
+ "returnOnEquity": safe(info.get("returnOnEquity")),
405
+ "debtToEquity": safe(info.get("debtToEquity")),
406
+ "currentRatio": safe(info.get("currentRatio")),
407
+ "freeCashflow": safe(info.get("freeCashflow")),
408
+ "revenueGrowth": safe(info.get("revenueGrowth")),
409
+ },
410
+ "trading": {
411
+ "beta": safe(info.get("beta")),
412
+ "dividendYield": safe(info.get("dividendYield")),
413
+ "fiftyTwoWeekHigh": safe(info.get("fiftyTwoWeekHigh")),
414
+ "fiftyTwoWeekLow": safe(info.get("fiftyTwoWeekLow")),
415
+ "averageVolume": safe(info.get("averageVolume")),
416
+ },
417
+ }
418
+ _set_cache(cache_key, result)
419
+ return result
420
+ except Exception as e:
421
+ raise HTTPException(status_code=500, detail=f"Failed to fetch fundamentals for {ticker}: {str(e)}")
422
+
423
+
424
+ # ── Options Flow Endpoints ────────────────────────────────────────────
425
+
426
+ @app.get("/api/ticker/{ticker}/options-flow")
427
+ async def get_options_flow(ticker: str):
428
+ """
429
+ Get full options chain analysis for a ticker:
430
+ - Per-expiry call/put volumes, OI, volume/OI ratios
431
+ - LEAPS (expiry > 1 year) highlighted separately
432
+ - Unusual volume/OI spikes (vol/OI > 2x)
433
+ - Call/Put volume ratio overall
434
+ - Last 24h change data when available
435
+ Cached for 5 minutes (options data doesn't change intra-minute).
436
+ """
437
+ cache_key = f"options-flow:{ticker.upper()}"
438
+ cached = _cached(cache_key, ttl=300)
439
+ if cached:
440
+ return cached
441
+
442
+ try:
443
+ data = fetch_options_flow(ticker)
444
+ _set_cache(cache_key, data)
445
+ return safe_json_response(data)
446
+ except ValueError as e:
447
+ raise HTTPException(status_code=400, detail=str(e))
448
+ except RuntimeError as e:
449
+ raise HTTPException(status_code=500, detail=str(e))
450
+ except Exception as e:
451
+ raise HTTPException(status_code=500, detail=f"Options data error for {ticker}: {str(e)}")
452
+
453
+
454
+ @app.get("/api/options-flow/leaps-board")
455
+ async def get_leaps_board(limit: int = 15):
456
+ """
457
+ Scan the top tickers by LEAPS (long-dated >1yr) options activity.
458
+ Returns tickers ranked by total unusual + high-volume options flow.
459
+ Useful for spotting which stocks have unusual long-dated options buying.
460
+ Default scan set: 22 liquid US equities + ETFs.
461
+ Set limit to control how many results returned (max 50).
462
+ """
463
+ import asyncio
464
+ limit = max(1, min(limit, 50))
465
+ try:
466
+ # Run blocking yfinance calls in thread pool for concurrency
467
+ loop = asyncio.get_event_loop()
468
+ tasks = [
469
+ loop.run_in_executor(None, rank_single_ticker, sym)
470
+ for sym in DEFAULT_SCAN_TICKERS
471
+ ]
472
+ results_raw = await asyncio.gather(*tasks, return_exceptions=True)
473
+ ranked = [r for r in results_raw if isinstance(r, dict)]
474
+ ranked.sort(
475
+ key=lambda x: x.get("total_call_volume", 0) + x.get("total_put_volume", 0),
476
+ reverse=True,
477
+ )
478
+ return safe_json_response({
479
+ "count": len(ranked),
480
+ "scan_tickers": DEFAULT_SCAN_TICKERS,
481
+ "results": ranked[:limit],
482
+ })
483
+ except Exception as e:
484
+ raise HTTPException(status_code=500, detail=f"LEAPS board error: {str(e)}")
485
+
486
+
487
+ @app.get("/api/leaps/scanner")
488
+ async def leaps_scanner(max_tickers: int = 100, min_spikes: int = 1):
489
+ """
490
+ S&P 100 tabanlı LEAPS anomali tarama.
491
+ Yakın zamanda yüksek miktarda LEAPS opsiyonu alınan hisseleri tespit eder.
492
+
493
+ Parametreler:
494
+ max_tickers: Taranacak maks ticker sayısı (varsayılan 100, max 100)
495
+ min_spikes: Minimum anormal spike sayısı filtresi (varsayılan 1)
496
+
497
+ Response:
498
+ tickers: LEAPS anomali bulunan ticker'lar (hacme göre sıralı)
499
+ scan_info: Tarama bilgisi (ticker sayısı, süre, zaman)
500
+ """
501
+ import time as _time
502
+ max_tickers = max(1, min(max_tickers, 100))
503
+ min_spikes = max(1, min_spikes)
504
+
505
+ try:
506
+ t0 = _time.time()
507
+ results = scan_all_sp100_leaps(max_tickers=max_tickers)
508
+ elapsed = round(_time.time() - t0, 1)
509
+
510
+ # Min spike filtresi
511
+ if min_spikes > 1:
512
+ results = [r for r in results if r["unusual_count"] >= min_spikes]
513
+
514
+ return safe_json_response({
515
+ "tickers": results,
516
+ "count": len(results),
517
+ "scan_info": {
518
+ "max_tickers": max_tickers,
519
+ "min_spikes": min_spikes,
520
+ "scan_time_sec": elapsed,
521
+ "scanned_at": datetime.utcnow().isoformat(),
522
+ },
523
+ })
524
+ except Exception as e:
525
+ raise HTTPException(status_code=500, detail=f"LEAPS scanner error: {str(e)}")
526
+
527
+
528
+ if __name__ == "__main__":
529
+ import uvicorn
530
+ uvicorn.run(app, host="0.0.0.0", port=8000, timeout_keep_alive=300)
backend/options_flow.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Options flow and unusual activity module.
3
+ Provides LEAPS detection, call/put ratio, and unusual volume analysis
4
+ via yfinance option chain data.
5
+ """
6
+
7
+ import yfinance as yf
8
+ import pandas as pd
9
+ import numpy as np
10
+ from datetime import datetime, timedelta
11
+ from typing import Optional
12
+
13
+ # ── Constants ──────────────────────────────────────────────────────────
14
+
15
+ LEAPS_MIN_DAYS = 365 # Expiry > 1 year = LEAPS
16
+ UNUSUAL_VOL_OI_THRESHOLD = 2.0 # volume / openInterest threshold for unusual activity
17
+ CPR_BULLISH = 3.0 # Call/Put ratio above this = bullish
18
+ CPR_BEARISH = 0.33 # Call/Put ratio below this = bearish
19
+
20
+ # Tickers to scan for the top LEAPS movers board
21
+ DEFAULT_SCAN_TICKERS = [
22
+ "AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "TSLA", "META",
23
+ "SPY", "QQQ", "IWM", "AMD", "INTC", "PLTR", "COIN",
24
+ "JPM", "BAC", "GS", "JNJ", "PFE", "UNH", "XOM", "CVX",
25
+ ]
26
+
27
+
28
+ # ── Helpers ────────────────────────────────────────────────────────────
29
+
30
+ def _safe_ratio(num, den, default=0.0):
31
+ """Avoid division by zero."""
32
+ if den == 0 or pd.isna(den):
33
+ return default
34
+ return float(num) / float(den)
35
+
36
+
37
+ def _detect_leaps(expiry_dates: list[str]) -> list[str]:
38
+ """Filter expiries that are LEAPS-range (> 365 days out)."""
39
+ today = datetime.today()
40
+ leaps = []
41
+ for exp in expiry_dates:
42
+ try:
43
+ dt = datetime.strptime(exp, "%Y-%m-%d")
44
+ if (dt - today).days >= LEAPS_MIN_DAYS:
45
+ leaps.append(exp)
46
+ except ValueError:
47
+ continue
48
+ return leaps
49
+
50
+
51
+ def _analyze_chain(
52
+ calls: pd.DataFrame,
53
+ puts: pd.DataFrame,
54
+ expiry: str,
55
+ is_leaps: bool = False,
56
+ ) -> dict:
57
+ """Analyze a single option chain (calls + puts) for one expiry."""
58
+ result = {
59
+ "expiry": expiry,
60
+ "is_leaps": is_leaps,
61
+ "total_call_volume": 0,
62
+ "total_put_volume": 0,
63
+ "total_call_oi": 0,
64
+ "total_put_oi": 0,
65
+ "call_put_ratio": 0.0,
66
+ "top_calls": [],
67
+ "top_puts": [],
68
+ "unusual_calls": [],
69
+ "unusual_puts": [],
70
+ }
71
+
72
+ # --- Calls ---
73
+ active_calls = calls[calls["volume"] > 0].copy()
74
+ if not active_calls.empty:
75
+ active_calls["vol_oi_ratio"] = active_calls.apply(
76
+ lambda r: _safe_ratio(r["volume"], r["openInterest"]), axis=1
77
+ )
78
+ result["total_call_volume"] = int(calls["volume"].sum())
79
+ result["total_call_oi"] = int(calls["openInterest"].sum())
80
+ # Top calls by volume
81
+ top = active_calls.nlargest(5, "volume")[
82
+ ["strike", "lastPrice", "volume", "openInterest",
83
+ "vol_oi_ratio", "impliedVolatility", "inTheMoney", "contractSymbol"]
84
+ ]
85
+ result["top_calls"] = _rows_to_dicts(top)
86
+ # Unusual: vol_oi_ratio > threshold
87
+ unusual = active_calls[active_calls["vol_oi_ratio"] >= UNUSUAL_VOL_OI_THRESHOLD].nlargest(10, "volume")
88
+ result["unusual_calls"] = _rows_to_dicts(unusual)
89
+
90
+ # --- Puts ---
91
+ active_puts = puts[puts["volume"] > 0].copy()
92
+ if not active_puts.empty:
93
+ active_puts["vol_oi_ratio"] = active_puts.apply(
94
+ lambda r: _safe_ratio(r["volume"], r["openInterest"]), axis=1
95
+ )
96
+ result["total_put_volume"] = int(puts["volume"].sum())
97
+ result["total_put_oi"] = int(puts["openInterest"].sum())
98
+ top = active_puts.nlargest(5, "volume")[
99
+ ["strike", "lastPrice", "volume", "openInterest",
100
+ "vol_oi_ratio", "impliedVolatility", "inTheMoney", "contractSymbol"]
101
+ ]
102
+ result["top_puts"] = _rows_to_dicts(top)
103
+ unusual = active_puts[active_puts["vol_oi_ratio"] >= UNUSUAL_VOL_OI_THRESHOLD].nlargest(10, "volume")
104
+ result["unusual_puts"] = _rows_to_dicts(unusual)
105
+
106
+ # Overall call/put ratio
107
+ result["call_put_ratio"] = _safe_ratio(
108
+ result["total_call_volume"], result["total_put_volume"]
109
+ )
110
+ return result
111
+
112
+
113
+ def _rows_to_dicts(df: pd.DataFrame) -> list[dict]:
114
+ """Convert DataFrame rows to list of dicts with safe JSON types."""
115
+ records = []
116
+ if df.empty:
117
+ return records
118
+ for _, row in df.iterrows():
119
+ rec = {}
120
+ for col in df.columns:
121
+ val = row[col]
122
+ if isinstance(val, (pd.Timestamp,)):
123
+ rec[col] = val.isoformat()
124
+ elif isinstance(val, (np.integer,)):
125
+ rec[col] = int(val)
126
+ elif isinstance(val, (np.floating,)):
127
+ rec[col] = float(val)
128
+ elif pd.isna(val):
129
+ rec[col] = None
130
+ else:
131
+ rec[col] = val
132
+ records.append(rec)
133
+ return records
134
+
135
+
136
+ # ── Public API ─────────────────────────────────────────────────────────
137
+
138
+ def fetch_options_flow(ticker: str) -> dict:
139
+ """
140
+ Fetch full options flow data for a ticker:
141
+ - All expiries (near and far)
142
+ - LEAPS highlighted
143
+ - Unusual volume/OI spikes
144
+ - Call/Put volume ratio
145
+
146
+ Returns a structured dict for JSON response.
147
+ """
148
+ try:
149
+ stock = yf.Ticker(ticker)
150
+ expiry_dates = stock.options
151
+ if not expiry_dates:
152
+ raise ValueError(f"No options data available for {ticker}")
153
+
154
+ has_leaps = False
155
+ leaps_expiries = _detect_leaps(expiry_dates)
156
+ if leaps_expiries:
157
+ has_leaps = True
158
+
159
+ all_expiries_data = []
160
+ unusual_activities = []
161
+ leaps_data = []
162
+
163
+ for i, exp in enumerate(expiry_dates):
164
+ is_leaps_flag = exp in leaps_expiries
165
+ chain = stock.option_chain(exp)
166
+ expiry_info = _analyze_chain(
167
+ chain.calls, chain.puts, expiry=exp, is_leaps=is_leaps_flag
168
+ )
169
+
170
+ if is_leaps_flag:
171
+ leaps_data.append(expiry_info)
172
+ # Mark highest volume/vol_oi strikes as unusual
173
+ for uc in expiry_info.get("unusual_calls", []):
174
+ uc["expiry"] = exp
175
+ uc["type"] = "leaps_call"
176
+ uc["signal"] = "BULLISH"
177
+ unusual_activities.append(uc)
178
+ for up in expiry_info.get("unusual_puts", []):
179
+ up["expiry"] = exp
180
+ up["type"] = "leaps_put"
181
+ up["signal"] = "BEARISH"
182
+ unusual_activities.append(up)
183
+
184
+ all_expiries_data.append(expiry_info)
185
+
186
+ # Sort unusual activities by volume descending
187
+ unusual_activities.sort(key=lambda x: x.get("volume", 0), reverse=True)
188
+ unusual_activities = unusual_activities[:20] # cap at 20
189
+
190
+ # Compute summary using the NEAREST expiry as primary
191
+ nearest = all_expiries_data[0] if all_expiries_data else {}
192
+ total_callVol = sum(e["total_call_volume"] for e in all_expiries_data)
193
+ total_putVol = sum(e["total_put_volume"] for e in all_expiries_data)
194
+
195
+ return {
196
+ "ticker": ticker,
197
+ "as_of": datetime.utcnow().isoformat(),
198
+ "has_leaps": has_leaps,
199
+ "leaps_expiries": leaps_expiries,
200
+ "summary": {
201
+ "total_call_volume": total_callVol,
202
+ "total_put_volume": total_putVol,
203
+ "call_put_ratio": round(_safe_ratio(total_callVol, total_putVol), 2),
204
+ "total_expiries": len(expiry_dates),
205
+ "nearest_call_put_ratio": nearest.get("call_put_ratio", 0.0),
206
+ "unusual_count": len(unusual_activities),
207
+ },
208
+ "all_expiries": all_expiries_data,
209
+ "leaps": leaps_data,
210
+ "unusual_activities": unusual_activities,
211
+ }
212
+
213
+ except ValueError:
214
+ raise
215
+ except Exception as e:
216
+ raise RuntimeError(f"Failed to fetch options data for {ticker}: {str(e)}")
217
+
218
+
219
+ def rank_single_ticker(sym: str) -> dict | None:
220
+ """Fetch options flow for a single ticker, returning a compact summary dict."""
221
+ try:
222
+ data = fetch_options_flow(sym)
223
+ summary = data["summary"]
224
+ return {
225
+ "ticker": sym,
226
+ "has_leaps": data["has_leaps"],
227
+ "leaps_count": len(data["leaps"]),
228
+ "total_call_volume": summary["total_call_volume"],
229
+ "total_put_volume": summary["total_put_volume"],
230
+ "call_put_ratio": summary["call_put_ratio"],
231
+ "nearest_call_put_ratio": summary["nearest_call_put_ratio"],
232
+ "unusual_count": summary["unusual_count"],
233
+ "top_leaps_expiry": data["leaps_expiries"][-1] if data["leaps_expiries"] else None,
234
+ }
235
+ except Exception:
236
+ return None
237
+
238
+
239
+ def rank_tickers_by_leaps_activity(tickers: list[str]) -> list[dict]:
240
+ """
241
+ Scan multiple tickers and rank them by options flow activity.
242
+ Returns list of {ticker, leaps_count, leaps_call_vol, leaps_put_vol, call_put_ratio, top_leaps_expiry}
243
+ sorted by total unusual activity.
244
+ """
245
+ results = []
246
+ for sym in tickers:
247
+ try:
248
+ data = fetch_options_flow(sym)
249
+ summary = data["summary"]
250
+
251
+ results.append({
252
+ "ticker": sym,
253
+ "has_leaps": data["has_leaps"],
254
+ "leaps_count": len(data["leaps"]),
255
+ "total_call_volume": summary["total_call_volume"],
256
+ "total_put_volume": summary["total_put_volume"],
257
+ "call_put_ratio": summary["call_put_ratio"],
258
+ "nearest_call_put_ratio": summary["nearest_call_put_ratio"],
259
+ "unusual_count": summary["unusual_count"],
260
+ "top_leaps_expiry": data["leaps_expiries"][-1] if data["leaps_expiries"] else None,
261
+ })
262
+ except Exception:
263
+ # Skip tickers with no options data
264
+ continue
265
+
266
+ # Sort by total call volume + put volume descending
267
+ results.sort(key=lambda x: x.get("total_call_volume", 0) + x.get("total_put_volume", 0), reverse=True)
268
+ return results
backend/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn==0.30.6
3
+ vectorbt==0.26.2
4
+ yfinance==0.2.54
5
+ pandas==2.2.3
6
+ numpy==1.26.4
7
+ pydantic==2.9.2
8
+ python-dateutil==2.9.0
9
+ httpx==0.27.0
backend/start.sh ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Backend başlat — önce portu temizle (Windows uyumlu)
3
+
4
+ PORT=8000
5
+
6
+ # Python ile portu temizle
7
+ python -c "
8
+ import subprocess, sys
9
+ result = subprocess.run(['netstat', '-ano'], capture_output=True, text=True)
10
+ for line in result.stdout.splitlines():
11
+ if ':${PORT}' in line and 'LISTENING' in line:
12
+ pid = line.strip().split()[-1]
13
+ print(f'Killing PID {pid} on port ${PORT}')
14
+ subprocess.run(['taskkill', '/PID', pid, '/F'], capture_output=True)
15
+ break
16
+ "
17
+
18
+ sleep 2
19
+
20
+ echo "Starting FastAPI on port $PORT..."
21
+ source venv/Scripts/activate
22
+ python main.py
backend/strategies.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Strategy module using vectorbt.
3
+ Implements SMA Crossover, EMA Crossover, Bollinger Bands, RSI, and MACD.
4
+ """
5
+
6
+ import vectorbt as vbt
7
+ import pandas as pd
8
+ import numpy as np
9
+
10
+
11
+ def run_sma_crossover(
12
+ close: pd.Series,
13
+ fast_window: int = 10,
14
+ slow_window: int = 30,
15
+ fees: float = 0.001,
16
+ init_cash: float = 10000.0,
17
+ freq: str = "1D"
18
+ ) -> dict:
19
+ """Run SMA Crossover backtest using vectorbt."""
20
+ fast_sma = vbt.MA.run(close, window=fast_window)
21
+ slow_sma = vbt.MA.run(close, window=slow_window)
22
+
23
+ entries = fast_sma.ma_crossed_above(slow_sma)
24
+ exits = fast_sma.ma_crossed_below(slow_sma)
25
+
26
+ portfolio = vbt.Portfolio.from_signals(
27
+ close=close,
28
+ entries=entries,
29
+ exits=exits,
30
+ init_cash=init_cash,
31
+ fees=fees,
32
+ freq=freq
33
+ )
34
+
35
+ stats = portfolio.stats()
36
+ trades = portfolio.trades.records_readable
37
+ equity_curve = portfolio.value()
38
+ returns = portfolio.returns()
39
+ drawdown = portfolio.drawdown()
40
+
41
+ return {
42
+ "stats": stats,
43
+ "trades": trades,
44
+ "equity_curve": equity_curve,
45
+ "returns": returns,
46
+ "drawdown": drawdown,
47
+ "fast_sma": fast_sma.ma,
48
+ "slow_sma": slow_sma.ma,
49
+ "entries": entries,
50
+ "exits": exits,
51
+ "label": f"SMA({fast_window}/{slow_window})",
52
+ }
53
+
54
+
55
+ def run_ema_crossover(
56
+ close: pd.Series,
57
+ fast_window: int = 12,
58
+ slow_window: int = 26,
59
+ fees: float = 0.001,
60
+ init_cash: float = 10000.0,
61
+ freq: str = "1D"
62
+ ) -> dict:
63
+ """Run EMA Crossover backtest using pandas EWM.
64
+
65
+ Note: vectorbt 0.26.x doesn't export vbt.EMA directly,
66
+ so we compute EMAs via pandas ewm() then detect crossovers manually.
67
+ """
68
+ fast_ema_series = close.ewm(span=fast_window, adjust=False).mean()
69
+ slow_ema_series = close.ewm(span=slow_window, adjust=False).mean()
70
+
71
+ entries = pd.Series(False, index=close.index)
72
+ exits = pd.Series(False, index=close.index)
73
+
74
+ for i in range(1, len(close)):
75
+ f_prev, f_curr = fast_ema_series.iloc[i - 1], fast_ema_series.iloc[i]
76
+ s_prev, s_curr = slow_ema_series.iloc[i - 1], slow_ema_series.iloc[i]
77
+ if pd.isna(f_curr) or pd.isna(s_curr) or pd.isna(f_prev) or pd.isna(s_prev):
78
+ continue
79
+ if f_prev <= s_prev and f_curr > s_curr:
80
+ entries.iloc[i] = True
81
+ if f_prev >= s_prev and f_curr < s_curr:
82
+ exits.iloc[i] = True
83
+
84
+ portfolio = vbt.Portfolio.from_signals(
85
+ close=close,
86
+ entries=entries,
87
+ exits=exits,
88
+ init_cash=init_cash,
89
+ fees=fees,
90
+ freq=freq
91
+ )
92
+
93
+ stats = portfolio.stats()
94
+ trades = portfolio.trades.records_readable
95
+ equity_curve = portfolio.value()
96
+ returns = portfolio.returns()
97
+ drawdown = portfolio.drawdown()
98
+
99
+ return {
100
+ "stats": stats,
101
+ "trades": trades,
102
+ "equity_curve": equity_curve,
103
+ "returns": returns,
104
+ "drawdown": drawdown,
105
+ "fast_ema": fast_ema_series,
106
+ "slow_ema": slow_ema_series,
107
+ "entries": entries,
108
+ "exits": exits,
109
+ "label": f"EMA({fast_window}/{slow_window})",
110
+ }
111
+
112
+
113
+ def run_bollinger_bands(
114
+ close: pd.Series,
115
+ window: int = 20,
116
+ std: float = 2.0,
117
+ fees: float = 0.001,
118
+ init_cash: float = 10000.0,
119
+ freq: str = "1D"
120
+ ) -> dict:
121
+ """Run Bollinger Bands mean-reversion backtest using pandas.
122
+
123
+ Note: vectorbt 0.26.x BBANDS.run has a parameter conflict,
124
+ so we compute bands manually with pandas rolling.
125
+ """
126
+ import numpy as np
127
+
128
+ rolling_mean = close.rolling(window=window).mean()
129
+ rolling_std = close.rolling(window=window).std(ddof=0)
130
+
131
+ middle = rolling_mean
132
+ upper = rolling_mean + (rolling_std * std)
133
+ lower = rolling_mean - (rolling_std * std)
134
+
135
+ # Mean reversion signals
136
+ entries = pd.Series(False, index=close.index)
137
+ exits = pd.Series(False, index=close.index)
138
+
139
+ for i in range(len(close)):
140
+ if pd.isna(lower.iloc[i]) or pd.isna(upper.iloc[i]) or pd.isna(close.iloc[i]):
141
+ continue
142
+ if close.iloc[i] <= lower.iloc[i]:
143
+ entries.iloc[i] = True
144
+ if close.iloc[i] >= upper.iloc[i]:
145
+ exits.iloc[i] = True
146
+
147
+ portfolio = vbt.Portfolio.from_signals(
148
+ close=close,
149
+ entries=entries,
150
+ exits=exits,
151
+ init_cash=init_cash,
152
+ fees=fees,
153
+ freq=freq
154
+ )
155
+
156
+ stats = portfolio.stats()
157
+ trades = portfolio.trades.records_readable
158
+ equity_curve = portfolio.value()
159
+ returns = portfolio.returns()
160
+ drawdown = portfolio.drawdown()
161
+
162
+ return {
163
+ "stats": stats,
164
+ "trades": trades,
165
+ "equity_curve": equity_curve,
166
+ "returns": returns,
167
+ "drawdown": drawdown,
168
+ "middle": middle,
169
+ "upper": upper,
170
+ "lower": lower,
171
+ "entries": entries,
172
+ "exits": exits,
173
+ "label": f"Bollinger({window},{std})",
174
+ }
175
+
176
+
177
+ # ── Strategy Registry ────────────────────────────────────────────────
178
+
179
+ STRATEGIES = {
180
+ "sma": {
181
+ "fn": run_sma_crossover,
182
+ "name": "SMA Crossover",
183
+ "params": {"fast_window": 10, "slow_window": 30},
184
+ },
185
+ "ema": {
186
+ "fn": run_ema_crossover,
187
+ "name": "EMA Crossover",
188
+ "params": {"fast_window": 12, "slow_window": 26},
189
+ },
190
+ "bollinger": {
191
+ "fn": run_bollinger_bands,
192
+ "name": "Bollinger Bands",
193
+ "params": {"window": 20, "std": 2.0},
194
+ },
195
+ }
196
+
197
+
198
+ def run_strategy(
199
+ strategy: str,
200
+ close: pd.Series,
201
+ **kwargs
202
+ ) -> dict:
203
+ """Run a named strategy with given parameters."""
204
+ if strategy not in STRATEGIES:
205
+ raise ValueError(f"Unknown strategy: {strategy}. Available: {list(STRATEGIES.keys())}")
206
+ return STRATEGIES[strategy]["fn"](close=close, **kwargs)
207
+
208
+
209
+ def run_all_strategies(
210
+ close: pd.Series,
211
+ fees: float = 0.001,
212
+ init_cash: float = 10000.0,
213
+ freq: str = "1D"
214
+ ) -> dict:
215
+ """Run all strategies and return comparison results."""
216
+ results = {}
217
+ for name, cfg in STRATEGIES.items():
218
+ try:
219
+ result = cfg["fn"](
220
+ close=close,
221
+ fees=fees,
222
+ init_cash=init_cash,
223
+ freq=freq,
224
+ **cfg["params"]
225
+ )
226
+ stats = result["stats"]
227
+ results[name] = {
228
+ "label": result["label"],
229
+ "name": cfg["name"],
230
+ "stats": stats,
231
+ "equity_curve": result["equity_curve"],
232
+ "returns": result["returns"],
233
+ "drawdown": result["drawdown"],
234
+ "trades": result["trades"],
235
+ }
236
+ except Exception as e:
237
+ results[name] = {"label": cfg["name"], "error": str(e)}
238
+ return results
239
+
240
+
241
+ # ── Indicators ───────────────────────────────────────────────────────
242
+
243
+ def calculate_rsi(
244
+ close: pd.Series,
245
+ window: int = 14
246
+ ) -> dict:
247
+ """Calculate RSI indicator."""
248
+ rsi = vbt.RSI.run(close, window=window)
249
+ return {
250
+ "rsi": rsi.rsi,
251
+ "overbought": 70,
252
+ "oversold": 30,
253
+ "window": window,
254
+ }
255
+
256
+
257
+ def calculate_macd(
258
+ close: pd.Series,
259
+ fast_window: int = 12,
260
+ slow_window: int = 26,
261
+ signal_window: int = 9
262
+ ) -> dict:
263
+ """Calculate MACD indicator."""
264
+ macd = vbt.MACD.run(
265
+ close,
266
+ fast_window=fast_window,
267
+ slow_window=slow_window,
268
+ signal_window=signal_window
269
+ )
270
+ return {
271
+ "macd": macd.macd,
272
+ "signal": macd.signal,
273
+ "histogram": macd.hist,
274
+ "fast_window": fast_window,
275
+ "slow_window": slow_window,
276
+ "signal_window": signal_window,
277
+ }
frontend/.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ node_modules/
2
+ .next/
3
+ .git/
4
+ *.log
5
+ .env.local
frontend/.gitignore ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files
34
+ .env
35
+ .env.local
36
+ .env.*.local
37
+ .env.production
38
+ .env.development
39
+
40
+ # vercel
41
+ .vercel
42
+
43
+ # typescript
44
+ *.tsbuildinfo
45
+ next-env.d.ts
frontend/AGENTS.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+ <!-- END:nextjs-agent-rules -->
frontend/CLAUDE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ @AGENTS.md
frontend/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
frontend/app/age.tsx ADDED
File without changes
frontend/app/api.ts ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
2
+
3
+ export interface BacktestRequest {
4
+ ticker: string;
5
+ start_date: string;
6
+ end_date: string;
7
+ interval?: string;
8
+ strategy?: string;
9
+ fast_ma?: number;
10
+ slow_ma?: number;
11
+ bb_window?: number;
12
+ bb_std?: number;
13
+ fees?: number;
14
+ init_cash?: number;
15
+ rsi_window?: number;
16
+ macd_fast?: number;
17
+ macd_slow?: number;
18
+ macd_signal?: number;
19
+ }
20
+
21
+ export interface StrategyInfo {
22
+ id: string;
23
+ name: string;
24
+ description: string;
25
+ params: Record<string, number>;
26
+ }
27
+
28
+ export interface BacktestResponse {
29
+ ticker: string;
30
+ start_date: string;
31
+ end_date: string;
32
+ interval: string;
33
+ strategy: string;
34
+ data_points: number;
35
+ dates: string[];
36
+ ohlcv: {
37
+ open: number[];
38
+ high: number[];
39
+ low: number[];
40
+ close: number[];
41
+ volume: number[];
42
+ };
43
+ portfolio: {
44
+ stats: Record<string, unknown>;
45
+ trades: unknown[];
46
+ equity_curve: Record<string, number>;
47
+ returns: Record<string, number>;
48
+ drawdown: Record<string, number>;
49
+ };
50
+ indicators: {
51
+ sma?: {
52
+ fast: Record<string, number>;
53
+ slow: Record<string, number>;
54
+ fast_window: number;
55
+ slow_window: number;
56
+ };
57
+ ema?: {
58
+ fast: Record<string, number>;
59
+ slow: Record<string, number>;
60
+ fast_window: number;
61
+ slow_window: number;
62
+ };
63
+ bollinger?: {
64
+ middle: Record<string, number>;
65
+ upper: Record<string, number>;
66
+ lower: Record<string, number>;
67
+ window: number;
68
+ std: number;
69
+ };
70
+ rsi: {
71
+ rsi: Record<string, number>;
72
+ overbought: number;
73
+ oversold: number;
74
+ window: number;
75
+ };
76
+ macd: {
77
+ macd: Record<string, number>;
78
+ signal: Record<string, number>;
79
+ histogram: Record<string, number>;
80
+ fast_window: number;
81
+ slow_window: number;
82
+ signal_window: number;
83
+ };
84
+ };
85
+ signals: {
86
+ entries: Record<string, boolean>;
87
+ exits: Record<string, boolean>;
88
+ };
89
+ }
90
+
91
+ // ── Options Flow ──────────────────────────────────────────────────────
92
+
93
+ export interface OptionsFlowResponse {
94
+ ticker: string;
95
+ as_of: string;
96
+ has_leaps: boolean;
97
+ leaps_expiries: string[];
98
+ summary: {
99
+ total_call_volume: number;
100
+ total_put_volume: number;
101
+ call_put_ratio: number;
102
+ total_expiries: number;
103
+ nearest_call_put_ratio: number;
104
+ unusual_count: number;
105
+ };
106
+ all_expiries: OptionsChainExpiry[];
107
+ leaps: OptionsChainExpiry[];
108
+ unusual_activities: UnusualActivity[];
109
+ }
110
+
111
+ export interface OptionsChainExpiry {
112
+ expiry: string;
113
+ is_leaps: boolean;
114
+ total_call_volume: number;
115
+ total_put_volume: number;
116
+ total_call_oi: number;
117
+ total_put_oi: number;
118
+ call_put_ratio: number;
119
+ top_calls: OptionStrike[];
120
+ top_puts: OptionStrike[];
121
+ unusual_calls: OptionStrike[];
122
+ unusual_puts: OptionStrike[];
123
+ }
124
+
125
+ export interface OptionStrike {
126
+ strike: number;
127
+ lastPrice?: number;
128
+ volume?: number;
129
+ openInterest?: number;
130
+ vol_oi_ratio?: number;
131
+ impliedVolatility?: number;
132
+ inTheMoney?: boolean;
133
+ contractSymbol?: string;
134
+ contractSize?: number;
135
+ expiry?: string;
136
+ type?: "leaps_call" | "leaps_put";
137
+ signal?: "BULLISH" | "BEARISH";
138
+ }
139
+
140
+ export interface UnusualActivity extends OptionStrike {}
141
+
142
+ // ── LEAPS Board (22 ticker statik liste) ──────────────────────────────
143
+
144
+ export interface LeapsBoardEntry {
145
+ ticker: string;
146
+ has_leaps: boolean;
147
+ leaps_count: number;
148
+ total_call_volume: number;
149
+ total_put_volume: number;
150
+ call_put_ratio: number;
151
+ nearest_call_put_ratio: number;
152
+ unusual_count: number;
153
+ top_leaps_expiry: string | null;
154
+ }
155
+
156
+ export interface LeapsBoardResponse {
157
+ count: number;
158
+ scan_tickers: string[];
159
+ results: LeapsBoardEntry[];
160
+ }
161
+
162
+ // ── LEAPS Scanner (S&P 100 concurrent tarama) ─────────────────────────
163
+
164
+ export interface LeapsScannerSpike {
165
+ type: string;
166
+ expiry: string;
167
+ strike: number;
168
+ volume: number;
169
+ openInterest: number;
170
+ vol_oi_ratio: number;
171
+ lastPrice?: number;
172
+ impliedVolatility?: number;
173
+ signal: string;
174
+ }
175
+
176
+ export interface LeapsScannerEntry {
177
+ ticker: string;
178
+ total_leaps_call_vol: number;
179
+ total_leaps_put_vol: number;
180
+ total_leaps_vol: number;
181
+ call_put_ratio: number;
182
+ unusual_count: number;
183
+ top_spike: LeapsScannerSpike;
184
+ all_spikes: LeapsScannerSpike[];
185
+ leaps_expiries_count: number;
186
+ nearest_leaps_expiry: string | null;
187
+ scan_time: string;
188
+ }
189
+
190
+ export interface LeapsScannerResponse {
191
+ tickers: LeapsScannerEntry[];
192
+ count: number;
193
+ scan_info: {
194
+ max_tickers: number;
195
+ min_spikes: number;
196
+ scan_time_sec: number;
197
+ scanned_at: string;
198
+ };
199
+ }
200
+
201
+ // ── Fetch Functions ───────────────────────────────���───────────────────
202
+
203
+ export const DEFAULT_SCAN_TICKERS = [
204
+ "AAPL","MSFT","GOOGL","AMZN","NVDA","TSLA","META",
205
+ "SPY","QQQ","IWM","AMD","INTC","PLTR","COIN",
206
+ "JPM","BAC","GS","JNJ","PFE","UNH","XOM","CVX",
207
+ ];
208
+
209
+ export async function fetchOptionsFlow(ticker: string): Promise<OptionsFlowResponse> {
210
+ const res = await fetch(`${API_BASE}/api/ticker/${encodeURIComponent(ticker)}/options-flow`);
211
+ if (!res.ok) {
212
+ const err = await res.json().catch(() => ({ detail: "Unknown error" }));
213
+ throw new Error(err.detail || `HTTP ${res.status}`);
214
+ }
215
+ return res.json();
216
+ }
217
+
218
+ export async function fetchLeapsBoard(limit: number = 15): Promise<LeapsBoardResponse> {
219
+ const res = await fetch(
220
+ `${API_BASE}/api/options-flow/leaps-board?limit=${encodeURIComponent(String(limit))}`
221
+ );
222
+ if (!res.ok) {
223
+ const err = await res.json().catch(() => ({ detail: "Unknown error" }));
224
+ throw new Error(err.detail || `HTTP ${res.status}`);
225
+ }
226
+ return res.json();
227
+ }
228
+
229
+ export async function fetchLeapsScanner(
230
+ max_tickers: number = 100,
231
+ min_spikes: number = 1,
232
+ ): Promise<LeapsScannerResponse> {
233
+ const res = await fetch(
234
+ `${API_BASE}/api/leaps/scanner?max_tickers=${encodeURIComponent(String(max_tickers))}&min_spikes=${encodeURIComponent(String(min_spikes))}`
235
+ );
236
+ if (!res.ok) {
237
+ const err = await res.json().catch(() => ({ detail: "Unknown error" }));
238
+ throw new Error(err.detail || `HTTP ${res.status}`);
239
+ }
240
+ return res.json();
241
+ }
242
+
243
+ export interface CompareResponse {
244
+ ticker: string;
245
+ start_date: string;
246
+ end_date: string;
247
+ interval: string;
248
+ data_points: number;
249
+ dates: string[];
250
+ ohlcv: {
251
+ open: number[];
252
+ high: number[];
253
+ low: number[];
254
+ close: number[];
255
+ volume: number[];
256
+ };
257
+ indicators: {
258
+ rsi: { rsi: Record<string, number>; overbought: number; oversold: number; window: number };
259
+ macd: {
260
+ macd: Record<string, number>;
261
+ signal: Record<string, number>;
262
+ histogram: Record<string, number>;
263
+ fast_window: number;
264
+ slow_window: number;
265
+ signal_window: number;
266
+ };
267
+ };
268
+ comparison: Record<string, {
269
+ label: string;
270
+ name: string;
271
+ stats: Record<string, unknown>;
272
+ equity_curve: Record<string, number>;
273
+ returns: Record<string, number>;
274
+ drawdown: Record<string, number>;
275
+ trades: unknown[];
276
+ error?: string;
277
+ }>;
278
+ }
279
+
280
+ export async function fetchStrategies(): Promise<StrategyInfo[]> {
281
+ const res = await fetch(`${API_BASE}/api/strategies`);
282
+ if (!res.ok) throw new Error("Failed to load strategies");
283
+ const data = await res.json();
284
+ return data.strategies;
285
+ }
286
+
287
+ export async function runBacktest(request: BacktestRequest): Promise<BacktestResponse> {
288
+ const res = await fetch(`${API_BASE}/api/backtest`, {
289
+ method: "POST",
290
+ headers: { "Content-Type": "application/json" },
291
+ body: JSON.stringify(request),
292
+ });
293
+
294
+ if (!res.ok) {
295
+ const error = await res.json().catch(() => ({ detail: "Unknown error" }));
296
+ throw new Error(error.detail || `HTTP ${res.status}`);
297
+ }
298
+
299
+ return res.json();
300
+ }
301
+
302
+ export async function compareStrategies(request: BacktestRequest): Promise<CompareResponse> {
303
+ const res = await fetch(`${API_BASE}/api/compare`, {
304
+ method: "POST",
305
+ headers: { "Content-Type": "application/json" },
306
+ body: JSON.stringify(request),
307
+ });
308
+
309
+ if (!res.ok) {
310
+ const error = await res.json().catch(() => ({ detail: "Unknown error" }));
311
+ throw new Error(error.detail || `HTTP ${res.status}`);
312
+ }
313
+
314
+ return res.json();
315
+ }
316
+
317
+ export async function healthCheck(): Promise<{ status: string; version: string }> {
318
+ const res = await fetch(`${API_BASE}/api/health`);
319
+ if (!res.ok) throw new Error("Backend unavailable");
320
+ return res.json();
321
+ }
322
+
323
+ export async function fetchTickerRange(ticker: string): Promise<{ earliest: string; latest: string; data_points: number }> {
324
+ const res = await fetch(`${API_BASE}/api/ticker/${encodeURIComponent(ticker)}/range`);
325
+ if (!res.ok) throw new Error(`No data for ${ticker}`);
326
+ return res.json();
327
+ }
328
+
329
+ export interface FundamentalsData {
330
+ ticker: string;
331
+ company: { name?: string; sector?: string; industry?: string; summary?: string };
332
+ valuation: {
333
+ marketCap?: number; enterpriseValue?: number;
334
+ trailingPE?: number; forwardPE?: number;
335
+ priceToBook?: number; enterpriseToEbitda?: number; priceToSales?: number;
336
+ };
337
+ financials: {
338
+ revenuePerShare?: number; earningsPerShare?: number;
339
+ profitMargins?: number; operatingMargins?: number;
340
+ returnOnEquity?: number; debtToEquity?: number;
341
+ currentRatio?: number; freeCashflow?: number; revenueGrowth?: number;
342
+ };
343
+ trading: {
344
+ beta?: number; dividendYield?: number;
345
+ fiftyTwoWeekHigh?: number; fiftyTwoWeekLow?: number; averageVolume?: number;
346
+ };
347
+ }
348
+
349
+ export async function fetchFundamentals(ticker: string): Promise<FundamentalsData> {
350
+ const res = await fetch(`${API_BASE}/api/ticker/${encodeURIComponent(ticker)}/fundamentals`);
351
+ if (!res.ok) {
352
+ const err = await res.json().catch(() => ({ detail: "Unknown error" }));
353
+ throw new Error(err.detail || `HTTP ${res.status}`);
354
+ }
355
+ return res.json();
356
+ }
frontend/app/components/BacktestForm.tsx ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect, useRef } from "react";
4
+ import { fetchStrategies, fetchTickerRange, type StrategyInfo } from "../api";
5
+
6
+ interface BacktestFormProps {
7
+ onSubmit: (config: BacktestConfig) => void;
8
+ onCompare?: (config: BacktestConfig) => void;
9
+ onTickerChange?: (ticker: string) => void;
10
+ isLoading: boolean;
11
+ isComparing?: boolean;
12
+ }
13
+
14
+ export interface BacktestConfig {
15
+ ticker: string;
16
+ start_date: string;
17
+ end_date: string;
18
+ interval: string;
19
+ strategy: string;
20
+ fast_ma: number;
21
+ slow_ma: number;
22
+ bb_window: number;
23
+ bb_std: number;
24
+ fees: number;
25
+ init_cash: number;
26
+ rsi_window: number;
27
+ macd_fast: number;
28
+ macd_slow: number;
29
+ macd_signal: number;
30
+ }
31
+
32
+ function getDefaultDates() {
33
+ const end = new Date();
34
+ const start = new Date(end);
35
+ start.setFullYear(start.getFullYear() - 1);
36
+ const fmt = (d: Date) => d.toISOString().split("T")[0];
37
+ return { start: fmt(start), end: fmt(end) };
38
+ }
39
+
40
+ const defaults = getDefaultDates();
41
+
42
+ const defaultConfig: BacktestConfig = {
43
+ ticker: "",
44
+ start_date: defaults.start,
45
+ end_date: defaults.end,
46
+ interval: "1d",
47
+ strategy: "sma",
48
+ fast_ma: 10,
49
+ slow_ma: 30,
50
+ bb_window: 20,
51
+ bb_std: 2.0,
52
+ fees: 0.001,
53
+ init_cash: 10000,
54
+ rsi_window: 14,
55
+ macd_fast: 12,
56
+ macd_slow: 26,
57
+ macd_signal: 9,
58
+ };
59
+
60
+ export default function BacktestForm({ onSubmit, onCompare, onTickerChange, isLoading, isComparing }: BacktestFormProps) {
61
+ const [config, setConfig] = useState<BacktestConfig>(defaultConfig);
62
+ const [strategies, setStrategies] = useState<StrategyInfo[]>([]);
63
+
64
+ useEffect(() => {
65
+ fetchStrategies()
66
+ .then(setStrategies)
67
+ .catch(() => setStrategies([
68
+ { id: "sma", name: "SMA Crossover", description: "", params: {} },
69
+ { id: "ema", name: "EMA Crossover", description: "", params: {} },
70
+ { id: "bollinger", name: "Bollinger Bands", description: "", params: {} },
71
+ ]));
72
+ }, []);
73
+
74
+ // Auto-fetch full available date range when ticker changes
75
+ const [loadingRange, setLoadingRange] = useState(false);
76
+ const tickerRef = useRef(config.ticker);
77
+ useEffect(() => {
78
+ const t = config.ticker.trim();
79
+ if (!t || t === tickerRef.current) return;
80
+ tickerRef.current = t;
81
+ const timer = setTimeout(() => {
82
+ setLoadingRange(true);
83
+ fetchTickerRange(t)
84
+ .then((range) => {
85
+ setConfig((prev) => ({
86
+ ...prev,
87
+ start_date: range.earliest.split("T")[0],
88
+ end_date: range.latest.split("T")[0],
89
+ }));
90
+ })
91
+ .catch(() => {})
92
+ .finally(() => setLoadingRange(false));
93
+ }, 600);
94
+ return () => clearTimeout(timer);
95
+ }, [config.ticker]);
96
+
97
+ const setFullRange = async () => {
98
+ const t = config.ticker.trim();
99
+ if (!t) return;
100
+ setLoadingRange(true);
101
+ try {
102
+ const range = await fetchTickerRange(t);
103
+ setConfig((prev) => ({
104
+ ...prev,
105
+ start_date: range.earliest.split("T")[0],
106
+ end_date: range.latest.split("T")[0],
107
+ }));
108
+ } catch {}
109
+ setLoadingRange(false);
110
+ };
111
+
112
+ const handleChange = (field: keyof BacktestConfig, value: string | number) => {
113
+ setConfig((prev) => ({ ...prev, [field]: value }));
114
+ };
115
+
116
+ const handleSubmit = (e: React.FormEvent) => {
117
+ e.preventDefault();
118
+ onSubmit(config);
119
+ };
120
+
121
+ const handleCompare = () => {
122
+ if (onCompare) onCompare(config);
123
+ };
124
+
125
+ const inputClass =
126
+ "w-full px-3 py-2 rounded text-sm outline-none transition-colors focus:border-[var(--accent-orange)]";
127
+ const labelClass = "text-xs text-[var(--text-secondary)] uppercase tracking-wider mb-1 block";
128
+
129
+ const isBollinger = config.strategy === "bollinger";
130
+
131
+ return (
132
+ <form onSubmit={handleSubmit} className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-5">
133
+ <div className="flex items-center gap-2 mb-4">
134
+ <span className="text-[var(--accent-orange)]">$</span>
135
+ <h2 className="text-sm font-bold uppercase tracking-wider">Backtest Configuration</h2>
136
+ </div>
137
+
138
+ {/* Row 1: Ticker + Interval */}
139
+ <div className="grid grid-cols-2 gap-4 mb-4">
140
+ <div>
141
+ <label className={labelClass}>Ticker Symbol</label>
142
+ <input
143
+ type="text"
144
+ value={config.ticker}
145
+ onChange={(e) => {
146
+ handleChange("ticker", e.target.value.toUpperCase());
147
+ onTickerChange?.(e.target.value.toUpperCase());
148
+ }}
149
+ className={inputClass}
150
+ placeholder="e.g. AAPL, TSLA, BTC-USD"
151
+ />
152
+ </div>
153
+ <div>
154
+ <label className={labelClass}>Interval</label>
155
+ <select
156
+ value={config.interval}
157
+ onChange={(e) => handleChange("interval", e.target.value)}
158
+ className={inputClass}
159
+ >
160
+ <option value="1m">1 Minute</option>
161
+ <option value="5m">5 Minutes</option>
162
+ <option value="15m">15 Minutes</option>
163
+ <option value="1h">1 Hour</option>
164
+ <option value="1d">1 Day</option>
165
+ <option value="1wk">1 Week</option>
166
+ <option value="1mo">1 Month</option>
167
+ </select>
168
+ </div>
169
+ </div>
170
+
171
+ {/* Row 2: Date Range */}
172
+ <div className="grid grid-cols-2 gap-4 mb-4">
173
+ <div>
174
+ <div className="flex items-center justify-between mb-1">
175
+ <label className={labelClass}>Start Date</label>
176
+ <button
177
+ type="button"
178
+ onClick={setFullRange}
179
+ disabled={loadingRange}
180
+ className="text-[10px] text-[var(--accent-orange)] hover:underline uppercase tracking-wider disabled:opacity-40"
181
+ >
182
+ {loadingRange ? "..." : "All Time"}
183
+ </button>
184
+ </div>
185
+ <input
186
+ type="date"
187
+ value={config.start_date}
188
+ onChange={(e) => handleChange("start_date", e.target.value)}
189
+ className={inputClass}
190
+ />
191
+ </div>
192
+ <div>
193
+ <label className={labelClass}>End Date</label>
194
+ <input
195
+ type="date"
196
+ value={config.end_date}
197
+ onChange={(e) => handleChange("end_date", e.target.value)}
198
+ className={inputClass}
199
+ />
200
+ </div>
201
+ </div>
202
+
203
+ {/* Row 3: Strategy Select */}
204
+ <div className="border-t border-[var(--border-color)] pt-4 mt-4">
205
+ <h3 className="text-xs text-[var(--accent-orange)] uppercase tracking-wider mb-3">
206
+ Strategy
207
+ </h3>
208
+ <div className="grid grid-cols-3 gap-2 mb-4">
209
+ {strategies.map((s) => (
210
+ <button
211
+ key={s.id}
212
+ type="button"
213
+ onClick={() => handleChange("strategy", s.id)}
214
+ className={`px-3 py-2 rounded text-xs font-bold uppercase tracking-wider transition-colors ${
215
+ config.strategy === s.id
216
+ ? "bg-[var(--accent-orange)] text-black"
217
+ : "bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--border-color)]"
218
+ }`}
219
+ >
220
+ {s.name.split(" ")[0]}
221
+ </button>
222
+ ))}
223
+ </div>
224
+ <div className="text-[10px] text-[var(--text-muted)] mb-4 italic">
225
+ {strategies.find((s) => s.id === config.strategy)?.description || ""}
226
+ </div>
227
+ </div>
228
+
229
+ {/* Row 4: Strategy Parameters */}
230
+ <div className="border-t border-[var(--border-color)] pt-4">
231
+ <h3 className="text-xs text-[var(--accent-orange)] uppercase tracking-wider mb-3">
232
+ {isBollinger ? "Bollinger Bands" : "MA Parameters"}
233
+ </h3>
234
+ {isBollinger ? (
235
+ <div className="grid grid-cols-2 gap-4 mb-4">
236
+ <div>
237
+ <label className={labelClass}>Window</label>
238
+ <input
239
+ type="number"
240
+ value={config.bb_window}
241
+ onChange={(e) => handleChange("bb_window", parseInt(e.target.value) || 2)}
242
+ min={2}
243
+ max={200}
244
+ className={inputClass}
245
+ />
246
+ </div>
247
+ <div>
248
+ <label className={labelClass}>Std Dev</label>
249
+ <input
250
+ type="number"
251
+ value={config.bb_std}
252
+ onChange={(e) => handleChange("bb_std", parseFloat(e.target.value) || 0.5)}
253
+ min={0.5}
254
+ max={4}
255
+ step={0.1}
256
+ className={inputClass}
257
+ />
258
+ </div>
259
+ </div>
260
+ ) : (
261
+ <div className="grid grid-cols-2 gap-4 mb-4">
262
+ <div>
263
+ <label className={labelClass}>Fast MA Period</label>
264
+ <input
265
+ type="number"
266
+ value={config.fast_ma}
267
+ onChange={(e) => handleChange("fast_ma", parseInt(e.target.value) || 2)}
268
+ min={2}
269
+ max={200}
270
+ className={inputClass}
271
+ />
272
+ </div>
273
+ <div>
274
+ <label className={labelClass}>Slow MA Period</label>
275
+ <input
276
+ type="number"
277
+ value={config.slow_ma}
278
+ onChange={(e) => handleChange("slow_ma", parseInt(e.target.value) || 2)}
279
+ min={2}
280
+ max={500}
281
+ className={inputClass}
282
+ />
283
+ </div>
284
+ </div>
285
+ )}
286
+ </div>
287
+
288
+ {/* Row 5: Indicator Settings */}
289
+ <div className="border-t border-[var(--border-color)] pt-4">
290
+ <h3 className="text-xs text-[var(--accent-orange)] uppercase tracking-wider mb-3">
291
+ Indicators
292
+ </h3>
293
+ <div className="grid grid-cols-4 gap-3 mb-4">
294
+ <div>
295
+ <label className={labelClass}>RSI Period</label>
296
+ <input
297
+ type="number"
298
+ value={config.rsi_window}
299
+ onChange={(e) => handleChange("rsi_window", parseInt(e.target.value) || 2)}
300
+ min={2}
301
+ max={100}
302
+ className={inputClass}
303
+ />
304
+ </div>
305
+ <div>
306
+ <label className={labelClass}>MACD Fast</label>
307
+ <input
308
+ type="number"
309
+ value={config.macd_fast}
310
+ onChange={(e) => handleChange("macd_fast", parseInt(e.target.value) || 2)}
311
+ min={2}
312
+ max={100}
313
+ className={inputClass}
314
+ />
315
+ </div>
316
+ <div>
317
+ <label className={labelClass}>MACD Slow</label>
318
+ <input
319
+ type="number"
320
+ value={config.macd_slow}
321
+ onChange={(e) => handleChange("macd_slow", parseInt(e.target.value) || 2)}
322
+ min={2}
323
+ max={200}
324
+ className={inputClass}
325
+ />
326
+ </div>
327
+ <div>
328
+ <label className={labelClass}>MACD Signal</label>
329
+ <input
330
+ type="number"
331
+ value={config.macd_signal}
332
+ onChange={(e) => handleChange("macd_signal", parseInt(e.target.value) || 2)}
333
+ min={2}
334
+ max={100}
335
+ className={inputClass}
336
+ />
337
+ </div>
338
+ </div>
339
+ </div>
340
+
341
+ {/* Submit */}
342
+ <div className="flex gap-2">
343
+ <button
344
+ type="submit"
345
+ disabled={isLoading || isComparing}
346
+ className="flex-1 px-4 py-3 bg-[var(--accent-orange)] hover:bg-[var(--accent-orange-hover)] disabled:opacity-50 disabled:cursor-not-allowed text-black font-bold text-sm uppercase tracking-wider rounded transition-colors glow-orange"
347
+ >
348
+ {isLoading ? (
349
+ <span className="flex items-center justify-center gap-2">
350
+ <span className="inline-block w-4 h-4 border-2 border-black border-t-transparent rounded-full animate-spin" />
351
+ Running...
352
+ </span>
353
+ ) : (
354
+ <span className="flex items-center justify-center gap-2">
355
+ <span>{">"}</span> Execute
356
+ </span>
357
+ )}
358
+ </button>
359
+ {onCompare && (
360
+ <button
361
+ type="button"
362
+ onClick={handleCompare}
363
+ disabled={isLoading || isComparing}
364
+ className="px-4 py-3 bg-[var(--bg-tertiary)] hover:bg-[var(--border-color)] disabled:opacity-50 disabled:cursor-not-allowed text-[var(--text-secondary)] font-bold text-sm uppercase tracking-wider rounded transition-colors"
365
+ >
366
+ {isComparing ? (
367
+ <span className="inline-block w-4 h-4 border-2 border-[var(--text-secondary)] border-t-transparent rounded-full animate-spin" />
368
+ ) : (
369
+ "Compare All"
370
+ )}
371
+ </button>
372
+ )}
373
+ </div>
374
+ </form>
375
+ );
376
+ }
frontend/app/components/CandlestickChart.tsx ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+
5
+ const Plot = dynamic(() => import("react-plotly.js"), { ssr: false });
6
+
7
+ interface CandlestickChartProps {
8
+ dates: string[];
9
+ open: number[];
10
+ high: number[];
11
+ low: number[];
12
+ close: number[];
13
+ ticker: string;
14
+ /** Strategy-specific overlay lines e.g. SMA/EMA/BB */
15
+ overlays?: { name: string; values: (number | null)[]; color: string; dash?: string }[];
16
+ }
17
+
18
+ export default function CandlestickChart({
19
+ dates,
20
+ open,
21
+ high,
22
+ low,
23
+ close,
24
+ ticker,
25
+ overlays,
26
+ }: CandlestickChartProps) {
27
+ const layout = {
28
+ paper_bgcolor: "rgba(0,0,0,0)",
29
+ plot_bgcolor: "rgba(0,0,0,0)",
30
+ font: { color: "#a0a0a0", family: "JetBrains Mono, monospace", size: 10 },
31
+ margin: { t: 30, r: 20, b: 40, l: 60 },
32
+ xaxis: {
33
+ gridcolor: "#2a2a2a",
34
+ zerolinecolor: "#2a2a2a",
35
+ showgrid: true,
36
+ rangeslider: { visible: false },
37
+ },
38
+ yaxis: {
39
+ gridcolor: "#2a2a2a",
40
+ zerolinecolor: "#2a2a2a",
41
+ showgrid: true,
42
+ title: "Price ($)",
43
+ titlefont: { size: 10 },
44
+ },
45
+ legend: {
46
+ font: { size: 10 },
47
+ bgcolor: "rgba(0,0,0,0)",
48
+ bordercolor: "#2a2a2a",
49
+ borderwidth: 1,
50
+ },
51
+ hovermode: "x unified" as const,
52
+ dragmode: "pan" as const,
53
+ title: {
54
+ text: `${ticker} — Price Chart`,
55
+ font: { size: 13, color: "#ffffff" },
56
+ },
57
+ };
58
+
59
+ const config = { responsive: true, displayModeBar: true, displaylogo: false, scrollZoom: true, modeBarButtonsToRemove: ['sendDataToCloud', 'lasso2d', 'select2d', 'resetScale2d'], modeBarButtons: [['zoom2d', 'pan2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetViews']] };
60
+
61
+ // Build the data array
62
+ const data: unknown[] = [
63
+ {
64
+ x: dates,
65
+ open: open,
66
+ high: high,
67
+ low: low,
68
+ close: close,
69
+ type: "candlestick",
70
+ name: "OHLC",
71
+ increasing: { line: { color: "#10b981" }, fillcolor: "#10b981" },
72
+ decreasing: { line: { color: "#ef4444" }, fillcolor: "#ef4444" },
73
+ },
74
+ ];
75
+
76
+ // Add overlay lines
77
+ if (overlays) {
78
+ overlays.forEach((line) => {
79
+ data.push({
80
+ x: dates,
81
+ y: line.values,
82
+ type: "scatter",
83
+ mode: "lines",
84
+ name: line.name,
85
+ line: { color: line.color, width: 1.5, dash: line.dash || "solid" as const },
86
+ });
87
+ });
88
+ }
89
+
90
+ // Add entry/exit markers if we could compute them
91
+ // (They are passed separately in PriceChart for signal visibility)
92
+
93
+ return (
94
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-4">
95
+ <Plot data={data} layout={layout} config={config} style={{ width: "100%", height: "450px" }} />
96
+ </div>
97
+ );
98
+ }
frontend/app/components/ComparePanel.tsx ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+
5
+ const Plot = dynamic(() => import("react-plotly.js"), { ssr: false });
6
+
7
+ interface ComparePanelProps {
8
+ comparison: Record<string, {
9
+ label: string;
10
+ name: string;
11
+ stats: Record<string, unknown>;
12
+ equity_curve: Record<string, number>;
13
+ error?: string;
14
+ }>;
15
+ dates: string[];
16
+ }
17
+
18
+ export default function ComparePanel({ comparison, dates }: ComparePanelProps) {
19
+ const entries = Object.entries(comparison).filter(([, v]) => !v.error);
20
+ const errors = Object.entries(comparison).filter(([, v]) => v.error);
21
+
22
+ if (entries.length === 0) {
23
+ return (
24
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-5">
25
+ <div className="text-center py-8 text-[var(--text-muted)] text-sm">
26
+ No strategies to compare
27
+ </div>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ // Equity curve comparison chart
33
+ const equityData = entries.map(([key, val]) => {
34
+ const curve = val.equity_curve || {};
35
+ const values = dates.map((d) => {
36
+ const v = (curve as Record<string, number>)[d];
37
+ return v !== undefined ? v : null;
38
+ });
39
+ return {
40
+ x: dates,
41
+ y: values,
42
+ type: "scatter",
43
+ mode: "lines",
44
+ name: val.label || key,
45
+ line: { width: 1.5 },
46
+ };
47
+ });
48
+
49
+ const equityLayout = {
50
+ paper_bgcolor: "rgba(0,0,0,0)",
51
+ plot_bgcolor: "rgba(0,0,0,0)",
52
+ font: { color: "#a0a0a0", family: "JetBrains Mono, monospace", size: 10 },
53
+ margin: { t: 30, r: 20, b: 40, l: 60 },
54
+ xaxis: { gridcolor: "#2a2a2a", zerolinecolor: "#2a2a2a", showgrid: true },
55
+ yaxis: { gridcolor: "#2a2a2a", zerolinecolor: "#2a2a2a", showgrid: true, titlefont: { size: 10 } },
56
+ hovermode: "x unified" as const,
57
+ dragmode: "pan" as const,
58
+ title: { text: "Strategy Comparison — Equity Curve", font: { size: 12, color: "#ffffff" } },
59
+ legend: { font: { size: 10 }, bgcolor: "rgba(0,0,0,0)", bordercolor: "#2a2a2a", borderwidth: 1 },
60
+ };
61
+
62
+ const colors = ["#f59e0b", "#10b981", "#3b82f6", "#a78bfa", "#ef4444"];
63
+ equityData.forEach((d: unknown, i: number) => {
64
+ (d as Record<string, unknown>).line = { ...(d as Record<string, unknown>).line as object, color: colors[i % colors.length] };
65
+ });
66
+
67
+ const config = { responsive: true, displayModeBar: true, displaylogo: false, modeBarButtonsToRemove: ['sendDataToCloud', 'lasso2d', 'select2d'], modeBarButtons: [['zoom2d', 'pan2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetViews']] };
68
+
69
+ // Key metrics comparison table
70
+ const metricKeys = ["Total Return [%]", "End Value", "Sharpe Ratio", "Max Drawdown [%]", "Total Trades", "Win Rate [%]"];
71
+
72
+ return (
73
+ <div className="space-y-6">
74
+ {/* Equity curve comparison */}
75
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-4">
76
+ <Plot data={equityData} layout={equityLayout} config={config} style={{ width: "100%", height: "300px" }} />
77
+ </div>
78
+
79
+ {/* Comparison table */}
80
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-5">
81
+ <div className="flex items-center gap-2 mb-4">
82
+ <span className="text-[var(--accent-orange)]">#</span>
83
+ <h2 className="text-sm font-bold uppercase tracking-wider">Strategy Comparison</h2>
84
+ </div>
85
+ <div className="overflow-x-auto">
86
+ <table className="w-full text-xs">
87
+ <thead className="text-[var(--text-muted)] uppercase tracking-wider">
88
+ <tr>
89
+ <th className="text-left py-2 px-3">Metric</th>
90
+ {entries.map(([key, val]) => (
91
+ <th key={key} className="text-right py-2 px-3">{val.label || key}</th>
92
+ ))}
93
+ </tr>
94
+ </thead>
95
+ <tbody>
96
+ {metricKeys.map((metric) => (
97
+ <tr key={metric} className="border-t border-[var(--border-color)]">
98
+ <td className="py-2 px-3 text-[var(--text-secondary)]">{metric}</td>
99
+ {entries.map(([key, val]) => {
100
+ const raw = val.stats?.[metric];
101
+ const display = raw != null ? String(raw) : "N/A";
102
+ return (
103
+ <td key={key} className="py-2 px-3 text-right font-bold">{display}</td>
104
+ );
105
+ })}
106
+ </tr>
107
+ ))}
108
+ </tbody>
109
+ </table>
110
+ </div>
111
+ </div>
112
+
113
+ {/* Errors */}
114
+ {errors.length > 0 && (
115
+ <div className="bg-red-950/30 border border-red-800 rounded-lg p-4">
116
+ <div className="flex items-center gap-2 text-red-400 text-sm">[ERROR] Some strategies failed</div>
117
+ {errors.map(([key, val]) => (
118
+ <div key={key} className="text-xs text-red-300 mt-1">
119
+ {key}: {val.error}
120
+ </div>
121
+ ))}
122
+ </div>
123
+ )}
124
+ </div>
125
+ );
126
+ }
frontend/app/components/DrawdownChart.tsx ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+
5
+ const Plot = dynamic(() => import("react-plotly.js"), { ssr: false });
6
+
7
+ interface DrawdownChartProps {
8
+ dates: string[];
9
+ drawdown: (number | null)[];
10
+ maxDrawdown?: number;
11
+ }
12
+
13
+ export default function DrawdownChart({ dates, drawdown, maxDrawdown }: DrawdownChartProps) {
14
+ const layout = {
15
+ paper_bgcolor: "rgba(0,0,0,0)",
16
+ plot_bgcolor: "rgba(0,0,0,0)",
17
+ font: { color: "#a0a0a0", family: "JetBrains Mono, monospace", size: 10 },
18
+ margin: { t: 35, r: 60, b: 45, l: 65 },
19
+ xaxis: {
20
+ gridcolor: "#2a2a2a",
21
+ zerolinecolor: "#2a2a2a",
22
+ showgrid: true,
23
+ },
24
+ yaxis: {
25
+ gridcolor: "#2a2a2a",
26
+ zerolinecolor: "#2a2a2a",
27
+ showgrid: true,
28
+ title: "Drawdown",
29
+ titlefont: { size: 10 },
30
+ tickformat: ".1%",
31
+ range: [Math.min(...drawdown.filter((v): v is number => v !== null)) * 1.15, 0.01],
32
+ },
33
+ hovermode: "x unified" as const,
34
+ dragmode: "pan" as const,
35
+ showlegend: false,
36
+ title: {
37
+ text: maxDrawdown
38
+ ? `Drawdown — Max: ${(maxDrawdown * 100).toFixed(2)}%`
39
+ : "Drawdown",
40
+ font: { size: 12, color: "#ffffff" },
41
+ },
42
+ annotations: maxDrawdown
43
+ ? [
44
+ {
45
+ x: dates[0],
46
+ y: maxDrawdown,
47
+ xref: "x" as const,
48
+ yref: "y" as const,
49
+ text: `Max DD ${(maxDrawdown * 100).toFixed(2)}%`,
50
+ showarrow: true,
51
+ arrowhead: 2,
52
+ arrowcolor: "#ef4444",
53
+ arrowsize: 1,
54
+ ax: 0,
55
+ ay: -30,
56
+ font: { color: "#ef4444", size: 9 },
57
+ bgcolor: "rgba(0,0,0,0.6)",
58
+ bordercolor: "#ef4444",
59
+ borderwidth: 1,
60
+ borderpad: 3,
61
+ },
62
+ ]
63
+ : undefined,
64
+ };
65
+
66
+ const config = {
67
+ responsive: true,
68
+ displayModeBar: true,
69
+ displaylogo: false,
70
+ modeBarButtonsToRemove: ["sendDataToCloud", "lasso2d", "select2d"],
71
+ modeBarButtons: [["zoom2d", "pan2d", "zoomIn2d", "zoomOut2d", "autoScale2d", "resetViews"]],
72
+ };
73
+
74
+ const data = [
75
+ {
76
+ x: dates,
77
+ y: drawdown,
78
+ type: "scatter",
79
+ mode: "lines",
80
+ name: "Drawdown",
81
+ line: { color: "#ef4444", width: 2 },
82
+ fill: "tozeroy",
83
+ fillcolor: "rgba(239, 68, 68, 0.15)",
84
+ },
85
+ {
86
+ x: dates,
87
+ y: drawdown.map(() => 0),
88
+ type: "scatter",
89
+ mode: "lines",
90
+ name: "Zero",
91
+ line: { color: "#ef4444", width: 1, dash: "dot" },
92
+ },
93
+ ];
94
+
95
+ return (
96
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-4">
97
+ <Plot data={data} layout={layout} config={config} style={{ width: "100%", height: "220px" }} />
98
+ </div>
99
+ );
100
+ }
frontend/app/components/EquityChart.tsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+
5
+ const Plot = dynamic(() => import("react-plotly.js"), { ssr: false });
6
+
7
+ interface EquityChartProps {
8
+ dates: string[];
9
+ equity: (number | null)[];
10
+ initCash: number;
11
+ }
12
+
13
+ export default function EquityChart({ dates, equity, initCash }: EquityChartProps) {
14
+ const layout = {
15
+ paper_bgcolor: "rgba(0,0,0,0)",
16
+ plot_bgcolor: "rgba(0,0,0,0)",
17
+ font: { color: "#a0a0a0", family: "JetBrains Mono, monospace", size: 10 },
18
+ margin: { t: 30, r: 20, b: 40, l: 60 },
19
+ xaxis: {
20
+ gridcolor: "#2a2a2a",
21
+ zerolinecolor: "#2a2a2a",
22
+ showgrid: true,
23
+ },
24
+ yaxis: {
25
+ gridcolor: "#2a2a2a",
26
+ zerolinecolor: "#2a2a2a",
27
+ showgrid: true,
28
+ title: "Value ($)",
29
+ titlefont: { size: 10 },
30
+ },
31
+ hovermode: "x unified" as const,
32
+ dragmode: "pan" as const,
33
+ title: {
34
+ text: "Equity Curve",
35
+ font: { size: 12, color: "#ffffff" },
36
+ },
37
+ shapes: [
38
+ {
39
+ type: "line" as const,
40
+ x0: dates[0],
41
+ x1: dates[dates.length - 1],
42
+ y0: initCash,
43
+ y1: initCash,
44
+ line: { color: "#666666", width: 1, dash: "dot" as const },
45
+ },
46
+ ],
47
+ annotations: [
48
+ {
49
+ x: dates[0],
50
+ y: initCash,
51
+ xref: "x" as const,
52
+ yref: "y" as const,
53
+ text: `$${initCash.toLocaleString()}`,
54
+ showarrow: false,
55
+ font: { color: "#666666", size: 9 },
56
+ },
57
+ ],
58
+ };
59
+
60
+ const config = { responsive: true, displayModeBar: true, displaylogo: false, modeBarButtonsToRemove: ['sendDataToCloud', 'lasso2d', 'select2d'], modeBarButtons: [['zoom2d', 'pan2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetViews']] };
61
+
62
+ const data = [
63
+ {
64
+ x: dates,
65
+ y: equity,
66
+ type: "scatter" as const,
67
+ mode: "lines" as const,
68
+ name: "Portfolio Value",
69
+ line: { color: "#f59e0b", width: 1.5 },
70
+ fill: "tozeroy" as const,
71
+ fillcolor: "rgba(245, 158, 11, 0.05)",
72
+ },
73
+ ];
74
+
75
+ return (
76
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-4">
77
+ <Plot data={data} layout={layout} config={config} style={{ width: "100%", height: "300px" }} />
78
+ </div>
79
+ );
80
+ }
frontend/app/components/FundamentalsPanel.tsx ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ interface FundamentalsPanelProps {
4
+ data: {
5
+ ticker: string;
6
+ company: { name?: string; sector?: string; industry?: string; summary?: string };
7
+ valuation: {
8
+ marketCap?: number; enterpriseValue?: number;
9
+ trailingPE?: number; forwardPE?: number;
10
+ priceToBook?: number; enterpriseToEbitda?: number; priceToSales?: number;
11
+ };
12
+ financials: {
13
+ revenuePerShare?: number; earningsPerShare?: number;
14
+ profitMargins?: number; operatingMargins?: number;
15
+ returnOnEquity?: number; debtToEquity?: number;
16
+ currentRatio?: number; freeCashflow?: number; revenueGrowth?: number;
17
+ };
18
+ trading: {
19
+ beta?: number; dividendYield?: number;
20
+ fiftyTwoWeekHigh?: number; fiftyTwoWeekLow?: number; averageVolume?: number;
21
+ };
22
+ };
23
+ }
24
+
25
+ function fmt(v: number | undefined | null, type: "money" | "percent" | "ratio" | "volume" = "ratio"): string {
26
+ if (v == null) return "—";
27
+ if (type === "money") {
28
+ if (v >= 1e12) return `$${(v / 1e12).toFixed(2)}T`;
29
+ if (v >= 1e9) return `$${(v / 1e9).toFixed(2)}B`;
30
+ if (v >= 1e6) return `$${(v / 1e6).toFixed(2)}M`;
31
+ return `$${v.toLocaleString()}`;
32
+ }
33
+ if (type === "percent") return `${(v * 100).toFixed(2)}%`;
34
+ if (type === "volume") return v >= 1e6 ? `${(v / 1e6).toFixed(1)}M` : v.toLocaleString();
35
+ return v.toFixed(2);
36
+ }
37
+
38
+ function StatRow({ label, value, color }: { label: string; value: string; color?: string }) {
39
+ return (
40
+ <div className="flex justify-between items-center py-1.5 border-b border-[var(--border-color)] last:border-0">
41
+ <span className="text-[10px] text-[var(--text-secondary)] uppercase tracking-wider">{label}</span>
42
+ <span className={`text-xs font-bold ${color || "text-[var(--text-primary)]"}`}>{value}</span>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ export default function FundamentalsPanel({ data }: FundamentalsPanelProps) {
48
+ const { valuation, financials, trading } = data;
49
+
50
+ return (
51
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-5">
52
+ {/* Header */}
53
+ <div className="flex items-center gap-2 mb-3">
54
+ <span className="text-[var(--accent-orange)]">$</span>
55
+ <h2 className="text-sm font-bold uppercase tracking-wider">Fundamentals</h2>
56
+ </div>
57
+ <div className="text-xs text-[var(--text-muted)] mb-4 leading-relaxed line-clamp-3">
58
+ {data.company.sector} — {data.company.industry}
59
+ </div>
60
+
61
+ {/* Valuation Multiples */}
62
+ <div className="mb-3">
63
+ <div className="text-[10px] text-[var(--accent-orange)] uppercase tracking-wider mb-1 font-bold">Valuation</div>
64
+ <StatRow label="Market Cap" value={fmt(valuation.marketCap, "money")} />
65
+ <StatRow label="Enterprise Value" value={fmt(valuation.enterpriseValue, "money")} />
66
+ <StatRow label="P/E (Trailing)" value={fmt(valuation.trailingPE)} />
67
+ <StatRow label="P/E (Forward)" value={fmt(valuation.forwardPE)} />
68
+ <StatRow label="P/B" value={fmt(valuation.priceToBook)} />
69
+ <StatRow label="EV/EBITDA" value={fmt(valuation.enterpriseToEbitda)} />
70
+ {valuation.priceToSales != null && (
71
+ <StatRow label="P/S" value={fmt(valuation.priceToSales)} />
72
+ )}
73
+ </div>
74
+
75
+ {/* Financial Health */}
76
+ <div className="mb-3">
77
+ <div className="text-[10px] text-[var(--accent-orange)] uppercase tracking-wider mb-1 font-bold">Financials</div>
78
+ <StatRow label="Revenue / Share" value={fmt(financials.revenuePerShare)} />
79
+ <StatRow label="EPS (TTM)" value={fmt(financials.earningsPerShare)} />
80
+ <StatRow label="Profit Margin" value={fmt(financials.profitMargins, "percent")} />
81
+ <StatRow label="Operating Margin" value={fmt(financials.operatingMargins, "percent")} />
82
+ <StatRow label="ROE" value={fmt(financials.returnOnEquity, "percent")} />
83
+ <StatRow label="Revenue Growth" value={fmt(financials.revenueGrowth, "percent")}
84
+ color={financials.revenueGrowth != null && financials.revenueGrowth >= 0 ? "text-emerald-400" : "text-red-400"} />
85
+ <StatRow label="D/E" value={fmt(financials.debtToEquity)} />
86
+ <StatRow label="Current Ratio" value={fmt(financials.currentRatio)} />
87
+ <StatRow label="Free Cash Flow" value={fmt(financials.freeCashflow, "money")} />
88
+ </div>
89
+
90
+ {/* Trading */}
91
+ <div>
92
+ <div className="text-[10px] text-[var(--accent-orange)] uppercase tracking-wider mb-1 font-bold">Trading</div>
93
+ <StatRow label="Beta" value={fmt(trading.beta)} />
94
+ <StatRow label="Div. Yield" value={fmt(trading.dividendYield, "percent")} />
95
+ <StatRow label="52W High" value={trading.fiftyTwoWeekHigh != null ? `$${trading.fiftyTwoWeekHigh.toFixed(2)}` : "—"} />
96
+ <StatRow label="52W Low" value={trading.fiftyTwoWeekLow != null ? `$${trading.fiftyTwoWeekLow.toFixed(2)}` : "—"} />
97
+ <StatRow label="Avg Volume" value={fmt(trading.averageVolume, "volume")} />
98
+ </div>
99
+ </div>
100
+ );
101
+ }
frontend/app/components/IndicatorChart.tsx ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+
5
+ const Plot = dynamic(() => import("react-plotly.js"), { ssr: false });
6
+
7
+ interface IndicatorChartProps {
8
+ dates: string[];
9
+ values: (number | null)[];
10
+ title: string;
11
+ color: string;
12
+ overbought?: number;
13
+ oversold?: number;
14
+ secondaryValues?: (number | null)[];
15
+ secondaryColor?: string;
16
+ secondaryName?: string;
17
+ histogramValues?: (number | null)[];
18
+ }
19
+
20
+ export default function IndicatorChart({
21
+ dates,
22
+ values,
23
+ title,
24
+ color,
25
+ overbought,
26
+ oversold,
27
+ secondaryValues,
28
+ secondaryColor,
29
+ secondaryName,
30
+ histogramValues,
31
+ }: IndicatorChartProps) {
32
+ const layout = {
33
+ paper_bgcolor: "rgba(0,0,0,0)",
34
+ plot_bgcolor: "rgba(0,0,0,0)",
35
+ font: { color: "#a0a0a0", family: "JetBrains Mono, monospace", size: 10 },
36
+ margin: { t: 30, r: 20, b: 40, l: 60 },
37
+ xaxis: {
38
+ gridcolor: "#2a2a2a",
39
+ zerolinecolor: "#2a2a2a",
40
+ showgrid: true,
41
+ },
42
+ yaxis: {
43
+ gridcolor: "#2a2a2a",
44
+ zerolinecolor: "#2a2a2a",
45
+ showgrid: true,
46
+ },
47
+ legend: {
48
+ font: { size: 10 },
49
+ bgcolor: "rgba(0,0,0,0)",
50
+ bordercolor: "#2a2a2a",
51
+ borderwidth: 1,
52
+ },
53
+ hovermode: "x unified" as const,
54
+ dragmode: "pan" as const,
55
+ title: {
56
+ text: title,
57
+ font: { size: 12, color: "#ffffff" },
58
+ },
59
+ shapes: [
60
+ ...(overbought != null
61
+ ? [{
62
+ type: "line" as const,
63
+ x0: dates[0],
64
+ x1: dates[dates.length - 1],
65
+ y0: overbought,
66
+ y1: overbought,
67
+ line: { color: "#ef4444", width: 1, dash: "dot" as const },
68
+ }]
69
+ : []),
70
+ ...(oversold != null
71
+ ? [{
72
+ type: "line" as const,
73
+ x0: dates[0],
74
+ x1: dates[dates.length - 1],
75
+ y0: oversold,
76
+ y1: oversold,
77
+ line: { color: "#10b981", width: 1, dash: "dot" as const },
78
+ }]
79
+ : []),
80
+ ],
81
+ annotations: [
82
+ ...(overbought != null
83
+ ? [{
84
+ x: dates[dates.length - 1],
85
+ y: overbought,
86
+ xref: "x" as const,
87
+ yref: "y" as const,
88
+ text: "OB",
89
+ showarrow: false,
90
+ font: { color: "#ef4444", size: 9 },
91
+ xanchor: "left" as const,
92
+ }]
93
+ : []),
94
+ ...(oversold != null
95
+ ? [{
96
+ x: dates[dates.length - 1],
97
+ y: oversold,
98
+ xref: "x" as const,
99
+ yref: "y" as const,
100
+ text: "OS",
101
+ showarrow: false,
102
+ font: { color: "#10b981", size: 9 },
103
+ xanchor: "left" as const,
104
+ }]
105
+ : []),
106
+ ],
107
+ };
108
+
109
+ const config = { responsive: true, displayModeBar: true, displaylogo: false, modeBarButtonsToRemove: ['sendDataToCloud', 'lasso2d', 'select2d'], modeBarButtons: [['zoom2d', 'pan2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetViews']] };
110
+
111
+ const data: unknown[] = [];
112
+
113
+ if (histogramValues) {
114
+ data.push({
115
+ x: dates,
116
+ y: histogramValues,
117
+ type: "bar",
118
+ name: "Histogram",
119
+ marker: { color: histogramValues.map((v) => (v != null && v >= 0 ? "#10b981" : "#ef4444")), opacity: 0.5 },
120
+ });
121
+ }
122
+
123
+ data.push({
124
+ x: dates,
125
+ y: values,
126
+ type: "scatter",
127
+ mode: "lines",
128
+ name: title.split(" ")[0],
129
+ line: { color, width: 1.5 },
130
+ });
131
+
132
+ if (secondaryValues && secondaryColor && secondaryName) {
133
+ data.push({
134
+ x: dates,
135
+ y: secondaryValues,
136
+ type: "scatter",
137
+ mode: "lines",
138
+ name: secondaryName,
139
+ line: { color: secondaryColor, width: 1, dash: "dash" },
140
+ });
141
+ }
142
+
143
+ return (
144
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-4">
145
+ <Plot data={data} layout={layout} config={config} style={{ width: "100%", height: "250px" }} />
146
+ </div>
147
+ );
148
+ }
frontend/app/components/OptionsFlowPanel.tsx ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect, useMemo } from "react";
4
+ import { fetchOptionsFlow, type OptionsFlowResponse } from "../api";
5
+ import dynamic from "next/dynamic";
6
+
7
+ // Lazy-load Plotly
8
+ const Plot = dynamic(() => import("react-plotly.js").then((m) => m.default), { ssr: false });
9
+
10
+ // ── Types ──────────────────────────────────────────────────────────────
11
+
12
+ interface StrikeData {
13
+ strike: number;
14
+ volume: number;
15
+ openInterest: number;
16
+ vol_oi_ratio: number;
17
+ contractSymbol?: string;
18
+ }
19
+
20
+ interface ExpiryData {
21
+ expiry: string;
22
+ is_leaps: boolean;
23
+ total_call_volume: number;
24
+ total_put_volume: number;
25
+ call_put_ratio: number;
26
+ top_calls: StrikeData[];
27
+ top_puts: StrikeData[];
28
+ }
29
+
30
+ interface UnusualActivity {
31
+ type: string;
32
+ expiry: string;
33
+ strike: number;
34
+ volume: number;
35
+ openInterest: number;
36
+ vol_oi_ratio: number;
37
+ signal: string;
38
+ contractSymbol: string;
39
+ }
40
+
41
+ interface SummaryData {
42
+ total_call_volume: number;
43
+ total_put_volume: number;
44
+ call_put_ratio: number;
45
+ total_expiries: number;
46
+ unusual_count: number;
47
+ }
48
+
49
+ // ── Helpers ────────────────────────────────────────────────────────────
50
+
51
+ function fmtVol(n: number): string {
52
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
53
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
54
+ return String(n);
55
+ }
56
+
57
+ function rpColor(ratio: number): string {
58
+ if (ratio === 0) return "#555";
59
+ return ratio > 1 ? "#22c55e" : ratio < 1 ? "#ef4444" : "#f59e0b";
60
+ }
61
+
62
+ function rpLabel(r: number): string {
63
+ if (r > 3) return "Strongly Bullish";
64
+ if (r > 1.5) return "Bullish";
65
+ if (r > 1) return "Slightly Bullish";
66
+ if (r > 0.67) return "Neutral";
67
+ if (r > 0.33) return "Slightly Bearish";
68
+ return "Strongly Bearish";
69
+ }
70
+
71
+ // ── Sub-components ─────────────────────────────────────────────────────
72
+
73
+ function Kpi({ label, value, color, sub }: { label: string; value: string; color: string; sub?: string }) {
74
+ return (
75
+ <div style={{ background: "#0f0f0f", borderRadius: 4, padding: "8px 10px", border: "1px solid #1a1a1a" }}>
76
+ <p style={{ margin: 0, color: "#555", fontFamily: "monospace", fontSize: 10 }}>{label}</p>
77
+ <p style={{ margin: "2px 0 0", color, fontFamily: "monospace", fontSize: 18, fontWeight: 700 }}>{value}</p>
78
+ {sub && <p style={{ margin: 0, color: "#888", fontFamily: "monospace", fontSize: 9 }}>{sub}</p>}
79
+ </div>
80
+ );
81
+ }
82
+
83
+ function ExpiryCard({ expiry, isLeaps, callVol, putVol, ratio, topCalls, topPuts }: {
84
+ expiry: string; isLeaps: boolean; callVol: number;
85
+ putVol: number; ratio: number; topCalls: StrikeData[]; topPuts: StrikeData[];
86
+ }) {
87
+ const [open, setOpen] = useState(true);
88
+ return (
89
+ <div style={{
90
+ background: "#0f0f0f", border: `1px solid ${isLeaps ? "#f97316" : "#2a2a2a"}`,
91
+ borderRadius: 6, marginBottom: 8, overflow: "hidden",
92
+ }}>
93
+ <button
94
+ onClick={() => setOpen(!open)}
95
+ style={{
96
+ width: "100%", padding: "8px 12px", display: "flex",
97
+ alignItems: "center", justifyContent: "space-between", cursor: "pointer",
98
+ background: "transparent", border: "none", color: "#e5e7eb",
99
+ fontFamily: "monospace", fontSize: 12,
100
+ }}
101
+ >
102
+ <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
103
+ <span>{expiry}</span>
104
+ {isLeaps && <span style={{ color: "#f97316", fontSize: 9, fontWeight: 700 }}>LEAPS</span>}
105
+ </span>
106
+ <span style={{ color: rpColor(ratio), fontSize: 11 }}>
107
+ C/P {ratio.toFixed(2)}
108
+ </span>
109
+ </button>
110
+ {open && (
111
+ <div style={{ padding: "0 12px 12px", display: "flex", gap: 24 }}>
112
+ <div style={{ flex: 1 }}>
113
+ <p style={{ margin: "0 0 4px", color: "#22c55e", fontFamily: "monospace", fontSize: 11, fontWeight: 600 }}>
114
+ CALLS {fmtVol(callVol)}
115
+ </p>
116
+ <MiniTable rows={topCalls.slice(0, 4)} />
117
+ </div>
118
+ <div style={{ flex: 1 }}>
119
+ <p style={{ margin: "0 0 4px", color: "#ef4444", fontFamily: "monospace", fontSize: 11, fontWeight: 600 }}>
120
+ PUTS {fmtVol(putVol)}
121
+ </p>
122
+ <MiniTable rows={topPuts.slice(0, 4)} />
123
+ </div>
124
+ </div>
125
+ )}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ function MiniTable({ rows }: { rows: StrikeData[] }) {
131
+ return (
132
+ <table style={{ width: "100%", borderCollapse: "collapse", tableLayout: "fixed", fontFamily: "monospace", fontSize: 11 }}>
133
+ <thead>
134
+ <tr style={{ color: "#555", borderBottom: "1px solid #222" }}>
135
+ <th style={{ padding: "2px 4px", textAlign: "left" }}>Strike</th>
136
+ <th style={{ padding: "2px 4px", textAlign: "left" }}>Vol</th>
137
+ <th style={{ padding: "2px 4px", textAlign: "left" }}>OI</th>
138
+ <th style={{ padding: "2px 4px", textAlign: "left" }}>V/OI</th>
139
+ </tr>
140
+ </thead>
141
+ <tbody style={{ color: "#e5e7eb" }}>
142
+ {rows.map((s, i) => (
143
+ <tr key={i} style={{ borderBottom: "1px solid #111" }}>
144
+ <td style={{ padding: "2px 4px" }}>{s.strike}</td>
145
+ <td style={{ padding: "2px 4px" }}>{fmtVol(s.volume ?? 0)}</td>
146
+ <td style={{ padding: "2px 4px" }}>{fmtVol(s.openInterest ?? 0)}</td>
147
+ <td style={{ padding: "2px 4px", color: (s.vol_oi_ratio ?? 0) > 2 ? "#f97316" : "#888" }}>
148
+ {(s.vol_oi_ratio ?? 0).toFixed(1)}
149
+ </td>
150
+ </tr>
151
+ ))}
152
+ </tbody>
153
+ </table>
154
+ );
155
+ }
156
+
157
+ function UnusualTable({ activities }: { activities: UnusualActivity[] }) {
158
+ if (activities.length === 0) {
159
+ return <p style={{ color: "#555", fontFamily: "monospace", fontSize: 11 }}>No unusual activity detected.</p>;
160
+ }
161
+ return (
162
+ <div style={{ overflowX: "auto" }}>
163
+ <table style={{ width: "100%", borderCollapse: "collapse", fontFamily: "monospace", fontSize: 11 }}>
164
+ <thead>
165
+ <tr style={{ color: "#555", borderBottom: "1px solid #333", textAlign: "left" }}>
166
+ <th style={{ padding: "6px 8px" }}>Type</th>
167
+ <th style={{ padding: "6px 8px" }}>Expiry</th>
168
+ <th style={{ padding: "6px 8px" }}>Strike</th>
169
+ <th style={{ padding: "6px 8px" }}>Volume</th>
170
+ <th style={{ padding: "6px 8px" }}>OI</th>
171
+ <th style={{ padding: "6px 8px" }}>V/OI</th>
172
+ <th style={{ padding: "6px 8px" }}>Signal</th>
173
+ </tr>
174
+ </thead>
175
+ <tbody>
176
+ {activities.map((a, i) => (
177
+ <tr key={i} style={{ borderBottom: "1px solid #1a1a1a" }}>
178
+ <td style={{ padding: "4px 8px", color: a.type === "leaps_call" ? "#22c55e" : "#ef4444", fontWeight: 600 }}>
179
+ {a.type === "leaps_call" ? "LEAPS CALL" : "LEAPS PUT"}
180
+ </td>
181
+ <td style={{ padding: "4px 8px", color: "#888" }}>{a.expiry}</td>
182
+ <td style={{ padding: "4px 8px" }}>${(a.strike ?? 0).toFixed(0)}</td>
183
+ <td style={{ padding: "4px 8px" }}>{fmtVol(a.volume ?? 0)}</td>
184
+ <td style={{ padding: "4px 8px" }}>{fmtVol(a.openInterest ?? 0)}</td>
185
+ <td style={{ padding: "4px 8px", color: (a.vol_oi_ratio ?? 0) > 3 ? "#f97316" : "#bbb" }}>
186
+ {(a.vol_oi_ratio ?? 0).toFixed(1)}
187
+ </td>
188
+ <td style={{ padding: "4px 8px", fontWeight: 700, color: a.signal === "BULLISH" ? "#22c55e" : "#ef4444" }}>
189
+ {a.signal}
190
+ </td>
191
+ </tr>
192
+ ))}
193
+ </tbody>
194
+ </table>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ // ── Main Panel ──────────────────────────────────────────────────────────
200
+
201
+ interface OptionsFlowPanelProps {
202
+ ticker: string;
203
+ }
204
+
205
+ export default function OptionsFlowPanel({ ticker }: OptionsFlowPanelProps) {
206
+ const [data, setData] = useState<OptionsFlowResponse | null>(null);
207
+ const [isLoading, setIsLoading] = useState(false);
208
+ const [error, setError] = useState<string | null>(null);
209
+
210
+ useEffect(() => {
211
+ if (!ticker) return;
212
+ let cancelled = false;
213
+ setIsLoading(true);
214
+ setError(null);
215
+ fetchOptionsFlow(ticker)
216
+ .then((d) => { if (!cancelled) setData(d); })
217
+ .catch(() => { if (!cancelled) setError("Options data unavailable."); })
218
+ .finally(() => { if (!cancelled) setIsLoading(false); });
219
+ return () => { cancelled = true; };
220
+ }, [ticker]);
221
+
222
+ if (!ticker) return null;
223
+ if (isLoading) return <p style={{ color: "#666", marginTop: 8, fontFamily: "monospace" }}>Loading options flow…</p>;
224
+ if (error) return <p style={{ color: "#ef4444", marginTop: 8, fontFamily: "monospace" }}>{error}</p>;
225
+ if (!data) return null;
226
+
227
+ const summary = data.summary as SummaryData;
228
+ const expiries = (data.all_expiries || []) as ExpiryData[];
229
+ const leaps = (data.leaps || []) as ExpiryData[];
230
+ const unusual = (data.unusual_activities || []) as UnusualActivity[];
231
+
232
+ const nearestIdx = expiries.findIndex((e: ExpiryData) => !e.is_leaps);
233
+ const primaryExpiry = nearestIdx >= 0 ? expiries[nearestIdx] : expiries[0] || null;
234
+
235
+ return (
236
+ <div style={{
237
+ background: "#0a0a0a", border: "1px solid #1e1e1e",
238
+ borderRadius: 6, padding: 16, marginTop: 12,
239
+ }}>
240
+ {/* Header */}
241
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }}>
242
+ <h3 style={{ margin: 0, color: "#f97316", fontFamily: "monospace", fontSize: 14, fontWeight: 700, letterSpacing: 1 }}>
243
+ OPTIONS FLOW ─ {data.ticker}
244
+ </h3>
245
+ <span style={{ color: "#555", fontFamily: "monospace", fontSize: 10 }}>
246
+ {new Date(data.as_of).toLocaleString("en-US", { hour12: false })}
247
+ </span>
248
+ </div>
249
+
250
+ {/* KPI row */}
251
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: 8, marginBottom: 16 }}>
252
+ <Kpi label="CALL VOL" value={fmtVol(summary.total_call_volume)} color="#22c55e" />
253
+ <Kpi label="PUT VOL" value={fmtVol(summary.total_put_volume)} color="#ef4444" />
254
+ <Kpi label="C/P RATIO" value={summary.call_put_ratio.toFixed(2)} sub={rpLabel(summary.call_put_ratio)} color={rpColor(summary.call_put_ratio)} />
255
+ <Kpi label="EXPIRIES" value={String(summary.total_expiries)} color="#777" />
256
+ <Kpi label="LEAPS" value={String(leaps.length)} sub={leaps.length > 0 ? (data as any).leaps_expiries?.[0] : "—"} color="#f97316" />
257
+ </div>
258
+
259
+ {/* Expiry breakdown */}
260
+ <div style={{ marginBottom: 12 }}>
261
+ <p style={{ margin: "0 0 6px", color: "#888", fontFamily: "monospace", fontSize: 11, fontWeight: 600 }}>
262
+ EXPIRY BREAKDOWN ({expiries.length} total, {leaps.length} LEAPS)
263
+ </p>
264
+ {expiries.slice().reverse().map((e: ExpiryData) => (
265
+ <ExpiryCard
266
+ key={e.expiry}
267
+ expiry={e.expiry}
268
+ isLeaps={e.is_leaps}
269
+ callVol={e.total_call_volume}
270
+ putVol={e.total_put_volume}
271
+ ratio={e.call_put_ratio}
272
+ topCalls={e.top_calls || []}
273
+ topPuts={e.top_puts || []}
274
+ />
275
+ ))}
276
+ </div>
277
+
278
+ {/* Unusual Activity */}
279
+ {unusual.length > 0 && (
280
+ <div>
281
+ <p style={{ margin: "0 0 6px", color: "#f97316", fontFamily: "monospace", fontSize: 11, fontWeight: 700 }}>
282
+ ⚑ UNUSUAL ACTIVITY ({summary.unusual_count}) — vol/OI ≥ 2
283
+ </p>
284
+ <UnusualTable activities={unusual} />
285
+ </div>
286
+ )}
287
+
288
+ {unusual.length === 0 && (
289
+ <p style={{ color: "#444", fontFamily: "monospace", fontSize: 10, marginTop: 4 }}>
290
+ No unusual vol/OI spikes detected.
291
+ </p>
292
+ )}
293
+ </div>
294
+ );
295
+ }
frontend/app/components/PriceChart.tsx ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+ import { useMemo } from "react";
5
+
6
+ const Plot = dynamic(() => import("react-plotly.js"), { ssr: false });
7
+
8
+ interface PriceChartProps {
9
+ dates: string[];
10
+ open: number[];
11
+ high: number[];
12
+ low: number[];
13
+ close: number[];
14
+ entries: boolean[];
15
+ exits: boolean[];
16
+ ticker: string;
17
+ /** Strategy overlays */
18
+ overlays?: { name: string; values: (number | null)[]; color: string; dash?: string }[];
19
+ /** Use candlestick style instead of line */
20
+ candlestick?: boolean;
21
+ }
22
+
23
+ export default function PriceChart({
24
+ dates,
25
+ open,
26
+ high,
27
+ low,
28
+ close,
29
+ entries,
30
+ exits,
31
+ ticker,
32
+ overlays,
33
+ candlestick,
34
+ }: PriceChartProps) {
35
+ const entryPoints = useMemo(() => {
36
+ const x: string[] = [];
37
+ const y: number[] = [];
38
+ entries.forEach((v, i) => {
39
+ if (v && close[i] != null) {
40
+ x.push(dates[i]);
41
+ y.push(close[i]);
42
+ }
43
+ });
44
+ return { x, y };
45
+ }, [entries, dates, close]);
46
+
47
+ const exitPoints = useMemo(() => {
48
+ const x: string[] = [];
49
+ const y: number[] = [];
50
+ exits.forEach((v, i) => {
51
+ if (v && close[i] != null) {
52
+ x.push(dates[i]);
53
+ y.push(close[i]);
54
+ }
55
+ });
56
+ return { x, y };
57
+ }, [exits, dates, close]);
58
+
59
+ const layout = {
60
+ paper_bgcolor: "rgba(0,0,0,0)",
61
+ plot_bgcolor: "rgba(0,0,0,0)",
62
+ font: { color: "#a0a0a0", family: "JetBrains Mono, monospace", size: 10 },
63
+ margin: { t: 30, r: 20, b: 40, l: 60 },
64
+ xaxis: {
65
+ gridcolor: "#2a2a2a",
66
+ zerolinecolor: "#2a2a2a",
67
+ showgrid: true,
68
+ title: "",
69
+ rangeslider: { visible: candlestick ? false : undefined },
70
+ },
71
+ yaxis: {
72
+ gridcolor: "#2a2a2a",
73
+ zerolinecolor: "#2a2a2a",
74
+ showgrid: true,
75
+ title: "Price ($)",
76
+ titlefont: { size: 10 },
77
+ },
78
+ legend: {
79
+ font: { size: 10 },
80
+ bgcolor: "rgba(0,0,0,0)",
81
+ bordercolor: "#2a2a2a",
82
+ borderwidth: 1,
83
+ },
84
+ hovermode: "x unified" as const,
85
+ dragmode: "pan" as const,
86
+ title: {
87
+ text: `${ticker} — Price Chart`,
88
+ font: { size: 13, color: "#ffffff" },
89
+ },
90
+ };
91
+
92
+ const config = { responsive: true, displayModeBar: true, displaylogo: false, scrollZoom: true, modeBarButtonsToRemove: ['sendDataToCloud', 'lasso2d', 'select2d', 'resetScale2d'], modeBarButtons: [['zoom2d', 'pan2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetViews']] };
93
+
94
+ const data: unknown[] = [];
95
+
96
+ if (candlestick && open.length > 0 && high.length > 0) {
97
+ // Candlestick chart
98
+ data.push({
99
+ x: dates,
100
+ open: open,
101
+ high: high,
102
+ low: low,
103
+ close: close,
104
+ type: "candlestick",
105
+ name: "OHLC",
106
+ increasing: { line: { color: "#10b981" }, fillcolor: "#10b981" },
107
+ decreasing: { line: { color: "#ef4444" }, fillcolor: "#ef4444" },
108
+ });
109
+ } else {
110
+ // Line chart
111
+ data.push({
112
+ x: dates,
113
+ y: close,
114
+ type: "scatter",
115
+ mode: "lines",
116
+ name: "Close",
117
+ line: { color: "#ffffff", width: 1.5 },
118
+ });
119
+ }
120
+
121
+ // Overlay lines (SMA/EMA/BB)
122
+ if (overlays) {
123
+ overlays.forEach((line) => {
124
+ data.push({
125
+ x: dates,
126
+ y: line.values,
127
+ type: "scatter",
128
+ mode: "lines",
129
+ name: line.name,
130
+ line: { color: line.color, width: 1, dash: line.dash || "solid" },
131
+ });
132
+ });
133
+ }
134
+
135
+ // Entry markers
136
+ if (entryPoints.x.length > 0) {
137
+ data.push({
138
+ x: entryPoints.x,
139
+ y: entryPoints.y,
140
+ type: "scatter",
141
+ mode: "markers",
142
+ name: "Buy",
143
+ marker: { color: "#10b981", size: 10, symbol: "triangle-up" },
144
+ });
145
+ }
146
+
147
+ // Exit markers
148
+ if (exitPoints.x.length > 0) {
149
+ data.push({
150
+ x: exitPoints.x,
151
+ y: exitPoints.y,
152
+ type: "scatter",
153
+ mode: "markers",
154
+ name: "Sell",
155
+ marker: { color: "#ef4444", size: 10, symbol: "triangle-down" },
156
+ });
157
+ }
158
+
159
+ return (
160
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-4">
161
+ <Plot data={data} layout={layout} config={config} style={{ width: "100%", height: "400px" }} />
162
+ </div>
163
+ );
164
+ }
frontend/app/components/StatsPanel.tsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ interface StatsPanelProps {
4
+ stats: Record<string, unknown> | null;
5
+ ticker: string;
6
+ }
7
+
8
+ export default function StatsPanel({ stats, ticker }: StatsPanelProps) {
9
+ if (!stats) {
10
+ return (
11
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-5">
12
+ <div className="flex items-center gap-2 mb-4">
13
+ <span className="text-[var(--accent-orange)]">#</span>
14
+ <h2 className="text-sm font-bold uppercase tracking-wider">Portfolio Statistics</h2>
15
+ </div>
16
+ <div className="text-center py-8 text-[var(--text-muted)] text-sm">
17
+ Run a backtest to see statistics
18
+ </div>
19
+ </div>
20
+ );
21
+ }
22
+
23
+ const totalReturn = typeof stats["Total Return [%]"] === "number"
24
+ ? (stats["Total Return [%]"] as number)
25
+ : typeof stats["Total Return"] === "number"
26
+ ? (stats["Total Return"] as number) * 100
27
+ : null;
28
+
29
+ const sharpe = typeof stats["Sharpe Ratio"] === "number"
30
+ ? stats["Sharpe Ratio"] as number
31
+ : null;
32
+
33
+ const maxDD = typeof stats["Max Drawdown [%]"] === "number"
34
+ ? stats["Max Drawdown [%]"] as number
35
+ : typeof stats["Max Drawdown"] === "number"
36
+ ? (stats["Max Drawdown"] as number) * 100
37
+ : null;
38
+
39
+ const endValue = typeof stats["End Value"] === "number"
40
+ ? stats["End Value"] as number
41
+ : typeof stats["Final Value"] === "number"
42
+ ? stats["Final Value"] as number
43
+ : null;
44
+
45
+ const winRate = typeof stats["Win Rate [%]"] === "number"
46
+ ? stats["Win Rate [%]"] as number
47
+ : null;
48
+
49
+ const closedTrades = typeof stats["Total Closed Trades"] === "number"
50
+ ? stats["Total Closed Trades"] as number
51
+ : typeof stats["Total Trades"] === "number"
52
+ ? stats["Total Trades"] as number
53
+ : null;
54
+
55
+ return (
56
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-5">
57
+ <div className="flex items-center justify-between mb-4">
58
+ <div className="flex items-center gap-2">
59
+ <span className="text-[var(--accent-orange)]">#</span>
60
+ <h2 className="text-sm font-bold uppercase tracking-wider">Portfolio Statistics</h2>
61
+ </div>
62
+ <span className="text-xs text-[var(--text-muted)] bg-[var(--bg-tertiary)] px-2 py-1 rounded">
63
+ {ticker}
64
+ </span>
65
+ </div>
66
+
67
+ {/* Key Metrics — only 4 cards */}
68
+ <div className="grid grid-cols-2 gap-3 mb-4">
69
+ {totalReturn !== null && (
70
+ <div className="bg-[var(--bg-tertiary)] rounded p-3 text-center">
71
+ <div className="text-[10px] text-[var(--text-muted)] uppercase tracking-wider mb-1">Total Return</div>
72
+ <div className={`text-lg font-bold ${totalReturn >= 0 ? "text-emerald-400" : "text-red-400"}`}>
73
+ {totalReturn.toFixed(2)}%
74
+ </div>
75
+ </div>
76
+ )}
77
+ {endValue !== null && (
78
+ <div className="bg-[var(--bg-tertiary)] rounded p-3 text-center">
79
+ <div className="text-[10px] text-[var(--text-muted)] uppercase tracking-wider mb-1">Final Value</div>
80
+ <div className="text-lg font-bold text-[var(--text-primary)]">
81
+ ${endValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
82
+ </div>
83
+ </div>
84
+ )}
85
+ {sharpe !== null && (
86
+ <div className="bg-[var(--bg-tertiary)] rounded p-3 text-center">
87
+ <div className="text-[10px] text-[var(--text-muted)] uppercase tracking-wider mb-1">Sharpe Ratio</div>
88
+ <div className="text-lg font-bold text-[var(--accent-orange)]">
89
+ {sharpe.toFixed(3)}
90
+ </div>
91
+ </div>
92
+ )}
93
+ {maxDD !== null && (
94
+ <div className="bg-[var(--bg-tertiary)] rounded p-3 text-center">
95
+ <div className="text-[10px] text-[var(--text-muted)] uppercase tracking-wider mb-1">Max Drawdown</div>
96
+ <div className="text-lg font-bold text-red-400">
97
+ {maxDD.toFixed(2)}%
98
+ </div>
99
+ </div>
100
+ )}
101
+ </div>
102
+
103
+ {/* Only Win Rate + Closed Trades */}
104
+ <div className="space-y-0">
105
+ {winRate !== null && (
106
+ <div className="flex justify-between items-center py-2 border-b border-[var(--border-color)] last:border-0">
107
+ <span className="text-xs text-[var(--text-secondary)] uppercase tracking-wider">Win Rate</span>
108
+ <span className={`text-sm font-bold ${winRate >= 50 ? "text-emerald-400" : "text-red-400"}`}>
109
+ {winRate.toFixed(1)}%
110
+ </span>
111
+ </div>
112
+ )}
113
+ {closedTrades !== null && closedTrades > 0 && (
114
+ <div className="flex justify-between items-center py-2 border-b border-[var(--border-color)] last:border-0">
115
+ <span className="text-xs text-[var(--text-secondary)] uppercase tracking-wider">Closed Trades</span>
116
+ <span className="text-sm font-bold text-[var(--text-primary)]">{String(closedTrades)}</span>
117
+ </div>
118
+ )}
119
+ </div>
120
+ </div>
121
+ );
122
+ }
frontend/app/components/TerminalHeader.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ interface TerminalHeaderProps {
4
+ status: "connected" | "disconnected" | "loading";
5
+ }
6
+
7
+ export default function TerminalHeader({ status }: TerminalHeaderProps) {
8
+ const statusColor = {
9
+ connected: "bg-emerald-500",
10
+ disconnected: "bg-red-500",
11
+ loading: "bg-amber-500",
12
+ }[status];
13
+
14
+ return (
15
+ <header className="border-b border-[var(--border-color)] bg-[var(--bg-secondary)] px-6 py-3">
16
+ <div className="flex items-center justify-between">
17
+ <div className="flex items-center gap-3">
18
+ <div className="flex items-center gap-2">
19
+ <span className="text-[var(--accent-orange)] text-lg font-bold">{">"}</span>
20
+ <h1 className="text-lg font-bold tracking-wider text-[var(--text-primary)]">
21
+ Terminal<span className="text-[var(--accent-orange)]">.AI</span>
22
+ </h1>
23
+ </div>
24
+ <span className="text-[var(--text-muted)] text-xs">v1.0.0</span>
25
+ <span className="text-[var(--text-muted)] text-xs">|</span>
26
+ <span className="text-[var(--text-muted)] text-xs">VectorBT Engine</span>
27
+ </div>
28
+ <div className="flex items-center gap-2">
29
+ <div className={`w-2 h-2 rounded-full ${statusColor}`} />
30
+ <span className="text-xs text-[var(--text-secondary)] uppercase tracking-wider">
31
+ {status === "connected" ? "ONLINE" : status === "loading" ? "CONNECTING" : "OFFLINE"}
32
+ </span>
33
+ </div>
34
+ </div>
35
+ </header>
36
+ );
37
+ }
frontend/app/components/VolumeChart.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+
5
+ const Plot = dynamic(() => import("react-plotly.js"), { ssr: false });
6
+
7
+ interface VolumeChartProps {
8
+ dates: string[];
9
+ volume: number[];
10
+ close: number[];
11
+ }
12
+
13
+ export default function VolumeChart({ dates, volume, close }: VolumeChartProps) {
14
+ // Color bars green/red based on price movement
15
+ const colors = close.map((c, i) => {
16
+ if (i === 0) return "#10b981";
17
+ return c >= (close[i - 1] || c) ? "#10b981" : "#ef4444";
18
+ });
19
+
20
+ const layout = {
21
+ paper_bgcolor: "rgba(0,0,0,0)",
22
+ plot_bgcolor: "rgba(0,0,0,0)",
23
+ font: { color: "#a0a0a0", family: "JetBrains Mono, monospace", size: 10 },
24
+ margin: { t: 25, r: 20, b: 40, l: 60 },
25
+ xaxis: {
26
+ gridcolor: "#2a2a2a",
27
+ zerolinecolor: "#2a2a2a",
28
+ showgrid: true,
29
+ },
30
+ yaxis: {
31
+ gridcolor: "#2a2a2a",
32
+ zerolinecolor: "#2a2a2a",
33
+ showgrid: true,
34
+ title: "Volume",
35
+ titlefont: { size: 10 },
36
+ },
37
+ hovermode: "x unified" as const,
38
+ dragmode: "pan" as const,
39
+ showlegend: false,
40
+ title: {
41
+ text: "Volume",
42
+ font: { size: 12, color: "#ffffff" },
43
+ },
44
+ bargap: 0,
45
+ };
46
+
47
+ const config = { responsive: true, displayModeBar: true, displaylogo: false, modeBarButtonsToRemove: ['sendDataToCloud', 'lasso2d', 'select2d'], modeBarButtons: [['zoom2d', 'pan2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetViews']] };
48
+
49
+ const data = [
50
+ {
51
+ x: dates,
52
+ y: volume,
53
+ type: "bar",
54
+ marker: { color: colors, opacity: 0.6 },
55
+ },
56
+ ];
57
+
58
+ return (
59
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-4">
60
+ <Plot data={data} layout={layout} config={config} style={{ width: "100%", height: "150px" }} />
61
+ </div>
62
+ );
63
+ }
frontend/app/favicon.ico ADDED
frontend/app/globals.css ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --bg-primary: #0a0a0a;
5
+ --bg-secondary: #111111;
6
+ --bg-tertiary: #1a1a1a;
7
+ --border-color: #2a2a2a;
8
+ --accent-orange: #f59e0b;
9
+ --accent-orange-hover: #d97706;
10
+ --accent-green: #10b981;
11
+ --accent-red: #ef4444;
12
+ --text-primary: #ffffff;
13
+ --text-secondary: #a0a0a0;
14
+ --text-muted: #666666;
15
+ }
16
+
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ html,
22
+ body {
23
+ margin: 0;
24
+ padding: 0;
25
+ background-color: var(--bg-primary);
26
+ color: var(--text-primary);
27
+ font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
28
+ -webkit-font-smoothing: antialiased;
29
+ -moz-osx-font-smoothing: grayscale;
30
+ }
31
+
32
+ /* Scrollbar styling */
33
+ ::-webkit-scrollbar {
34
+ width: 6px;
35
+ height: 6px;
36
+ }
37
+
38
+ ::-webkit-scrollbar-track {
39
+ background: var(--bg-secondary);
40
+ }
41
+
42
+ ::-webkit-scrollbar-thumb {
43
+ background: var(--border-color);
44
+ border-radius: 3px;
45
+ }
46
+
47
+ ::-webkit-scrollbar-thumb:hover {
48
+ background: var(--accent-orange);
49
+ }
50
+
51
+ /* Terminal cursor blink */
52
+ @keyframes blink {
53
+ 0%, 50% { opacity: 1; }
54
+ 51%, 100% { opacity: 0; }
55
+ }
56
+
57
+ .cursor-blink {
58
+ animation: blink 1s infinite;
59
+ }
60
+
61
+ /* Glow effect for accent elements */
62
+ .glow-orange {
63
+ box-shadow: 0 0 15px rgba(245, 158, 11, 0.15);
64
+ }
65
+
66
+ /* Input styling */
67
+ input[type="text"],
68
+ input[type="number"],
69
+ select {
70
+ background-color: var(--bg-tertiary);
71
+ border: 1px solid var(--border-color);
72
+ color: var(--text-primary);
73
+ font-family: inherit;
74
+ }
75
+
76
+ input[type="number"]::-webkit-inner-spin-button,
77
+ input[type="number"]::-webkit-outer-spin-button {
78
+ opacity: 1;
79
+ }
80
+
81
+ /* Plotly overrides for dark theme */
82
+ .js-plotly-plot .plotly .main-svg {
83
+ background: transparent !important;
84
+ }
frontend/app/layout.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Terminal.AI — VectorBT Financial Analysis",
6
+ description: "Professional backtesting and technical analysis platform powered by VectorBT",
7
+ };
8
+
9
+ export default function RootLayout({
10
+ children,
11
+ }: {
12
+ children: React.ReactNode;
13
+ }) {
14
+ return (
15
+ <html lang="en" suppressHydrationWarning>
16
+ <body className="min-h-screen antialiased" suppressHydrationWarning>
17
+ {children}
18
+ </body>
19
+ </html>
20
+ );
21
+ }
frontend/app/page.tsx ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback, useRef } from "react";
4
+ import TerminalHeader from "./components/TerminalHeader";
5
+ import BacktestForm, { type BacktestConfig } from "./components/BacktestForm";
6
+ import PriceChart from "./components/PriceChart";
7
+ import IndicatorChart from "./components/IndicatorChart";
8
+ import EquityChart from "./components/EquityChart";
9
+ import VolumeChart from "./components/VolumeChart";
10
+ import DrawdownChart from "./components/DrawdownChart";
11
+ import ComparePanel from "./components/ComparePanel";
12
+ import FundamentalsPanel from "./components/FundamentalsPanel";
13
+ import OptionsFlowPanel from "./components/OptionsFlowPanel";
14
+ import {
15
+ runBacktest, compareStrategies, healthCheck, fetchFundamentals,
16
+ fetchOptionsFlow, fetchLeapsScanner,
17
+ type BacktestResponse, type CompareResponse, type FundamentalsData,
18
+ type OptionsFlowResponse, type LeapsScannerEntry, type LeapsScannerResponse,
19
+ } from "./api";
20
+
21
+ export default function Home() {
22
+ const [status, setStatus] = useState<"connected" | "disconnected" | "loading">("loading");
23
+ const [isLoading, setIsLoading] = useState(false);
24
+ const [isComparing, setIsComparing] = useState(false);
25
+ const [error, setError] = useState<string | null>(null);
26
+ const [result, setResult] = useState<BacktestResponse | null>(null);
27
+ const [compareResult, setCompareResult] = useState<CompareResponse | null>(null);
28
+ const [fundamentals, setFundamentals] = useState<FundamentalsData | null>(null);
29
+ const [useCandlestick, setUseCandlestick] = useState(true);
30
+ const [activeTab, setActiveTab] = useState<"backtest" | "compare" | "options">("backtest");
31
+ const [optionsData, setOptionsData] = useState<OptionsFlowResponse | null>(null);
32
+ const [optionsLoading, setOptionsLoading] = useState(false);
33
+ const [leapsScanner, setLeapsScanner] = useState<LeapsScannerEntry[]>([]);
34
+ const [leapsScannerLoading, setLeapsScannerLoading] = useState(false);
35
+ const [leapsScannerInfo, setLeapsScannerInfo] = useState<LeapsScannerResponse["scan_info"] | null>(null);
36
+ const fundTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
37
+
38
+ const dictToArray = useCallback(
39
+ (dict: Record<string, number> | undefined, dates: string[]): (number | null)[] => {
40
+ if (!dict) return [];
41
+ return dates.map((d) => {
42
+ const v = dict[d];
43
+ return v !== undefined ? v : null;
44
+ });
45
+ },
46
+ [],
47
+ );
48
+
49
+ useEffect(() => {
50
+ healthCheck().then(() => setStatus("connected")).catch(() => setStatus("disconnected"));
51
+ }, []);
52
+
53
+ useEffect(() => {
54
+ if (activeTab === "options") loadLeapsScanner();
55
+ }, [activeTab]);
56
+
57
+ const loadLeapsScanner = async () => {
58
+ setLeapsScannerLoading(true);
59
+ try {
60
+ const data = await fetchLeapsScanner(100, 1);
61
+ setLeapsScanner(data.tickers);
62
+ setLeapsScannerInfo(data.scan_info);
63
+ } catch {
64
+ setLeapsScanner([]);
65
+ } finally {
66
+ setLeapsScannerLoading(false);
67
+ }
68
+ };
69
+
70
+ const handleBacktest = async (config: BacktestConfig) => {
71
+ setIsLoading(true);
72
+ setError(null);
73
+ setResult(null);
74
+ setCompareResult(null);
75
+ setFundamentals(null);
76
+ setActiveTab("backtest");
77
+ try {
78
+ const [data, fundData] = await Promise.all([
79
+ runBacktest(config),
80
+ fetchFundamentals(config.ticker).catch(() => null),
81
+ ]);
82
+ setResult(data);
83
+ if (fundData) setFundamentals(fundData);
84
+ } catch (err) {
85
+ setError(err instanceof Error ? err.message : "Unknown error");
86
+ } finally {
87
+ setIsLoading(false);
88
+ }
89
+ };
90
+
91
+ const handleCompare = async (config: BacktestConfig) => {
92
+ setIsComparing(true);
93
+ setError(null);
94
+ setResult(null);
95
+ setCompareResult(null);
96
+ setFundamentals(null);
97
+ setActiveTab("compare");
98
+ try {
99
+ const [data, fundData] = await Promise.all([
100
+ compareStrategies(config),
101
+ fetchFundamentals(config.ticker).catch(() => null),
102
+ ]);
103
+ setCompareResult(data);
104
+ if (fundData) setFundamentals(fundData);
105
+ } catch (err) {
106
+ setError(err instanceof Error ? err.message : "Unknown error");
107
+ } finally {
108
+ setIsComparing(false);
109
+ }
110
+ };
111
+
112
+ // Derived data
113
+ const dates = result?.dates || [];
114
+ const close = result?.ohlcv?.close || [];
115
+ const open = result?.ohlcv?.open || [];
116
+ const high = result?.ohlcv?.high || [];
117
+ const low = result?.ohlcv?.low || [];
118
+ const volume = result?.ohlcv?.volume || [];
119
+
120
+ const strategyName = result?.strategy || "sma";
121
+ const fastOverlay = dictToArray(result?.indicators?.sma?.fast as Record<string, number> | undefined, dates);
122
+ const slowOverlay = dictToArray(result?.indicators?.sma?.slow as Record<string, number> | undefined, dates);
123
+ const emaFastOverlay = dictToArray(result?.indicators?.ema?.fast as Record<string, number> | undefined, dates);
124
+ const emaSlowOverlay = dictToArray(result?.indicators?.ema?.slow as Record<string, number> | undefined, dates);
125
+ const bbUpper = dictToArray(result?.indicators?.bollinger?.upper as Record<string, number> | undefined, dates);
126
+ const bbMiddle = dictToArray(result?.indicators?.bollinger?.middle as Record<string, number> | undefined, dates);
127
+ const bbLower = dictToArray(result?.indicators?.bollinger?.lower as Record<string, number> | undefined, dates);
128
+
129
+ const entries = dictToArray(result?.signals?.entries as unknown as Record<string, number> | undefined, dates).map(Boolean);
130
+ const exits = dictToArray(result?.signals?.exits as unknown as Record<string, number> | undefined, dates).map(Boolean);
131
+
132
+ const rsiValues = dictToArray(result?.indicators?.rsi?.rsi as Record<string, number> | undefined, dates);
133
+ const macdLine = dictToArray(result?.indicators?.macd?.macd as Record<string, number> | undefined, dates);
134
+ const macdSignal = dictToArray(result?.indicators?.macd?.signal as Record<string, number> | undefined, dates);
135
+ const macdHist = dictToArray(result?.indicators?.macd?.histogram as Record<string, number> | undefined, dates);
136
+ const equityArr = dictToArray(result?.portfolio?.equity_curve as Record<string, number> | undefined, dates);
137
+ const drawdownArr = dictToArray(result?.portfolio?.drawdown as Record<string, number> | undefined, dates);
138
+
139
+ const overlays: { name: string; values: (number | null)[]; color: string; dash?: string }[] = [];
140
+ if (strategyName === "sma" && fastOverlay.length > 0) {
141
+ overlays.push({ name: "Fast SMA", values: fastOverlay, color: "#f59e0b", dash: "dot" });
142
+ overlays.push({ name: "Slow SMA", values: slowOverlay, color: "#3b82f6", dash: "dash" });
143
+ } else if (strategyName === "ema" && emaFastOverlay.length > 0) {
144
+ overlays.push({ name: "Fast EMA", values: emaFastOverlay, color: "#f59e0b", dash: "dot" });
145
+ overlays.push({ name: "Slow EMA", values: emaSlowOverlay, color: "#3b82f6", dash: "dash" });
146
+ } else if (strategyName === "bollinger" && bbUpper.length > 0) {
147
+ overlays.push({ name: "Upper BB", values: bbUpper, color: "#a78bfa", dash: "dot" });
148
+ overlays.push({ name: "Middle BB", values: bbMiddle, color: "#a78bfa" });
149
+ overlays.push({ name: "Lower BB", values: bbLower, color: "#a78bfa", dash: "dot" });
150
+ }
151
+
152
+ return (
153
+ <div className="min-h-screen flex flex-col">
154
+ <TerminalHeader status={status} />
155
+
156
+ <main className="flex-1 p-6">
157
+ <div className="max-w-[1600px] mx-auto">
158
+ {/* Terminal prompt line */}
159
+ <div className="flex items-center gap-2 mb-6 text-sm flex-wrap">
160
+ <span className="text-[var(--accent-orange)]">{">"}</span>
161
+ <span className="text-[var(--text-secondary)]">vectorbt</span>
162
+ <span className="text-[var(--text-muted)]">--backtest</span>
163
+ <span className="text-[var(--text-muted)]">--engine={result?.strategy || "sma"}</span>
164
+ <span className="text-[var(--text-muted)]">--indicators=rsi,macd</span>
165
+ <span className="cursor-blink text-[var(--accent-orange)]">_</span>
166
+
167
+ {/* Tabs */}
168
+ <div className="ml-auto flex gap-1">
169
+ <button
170
+ onClick={() => setActiveTab("backtest")}
171
+ className={`px-2 py-1 text-[10px] uppercase tracking-wider rounded ${
172
+ activeTab === "backtest"
173
+ ? "bg-[var(--accent-orange)] text-black"
174
+ : "text-[var(--text-muted)] hover:text-[var(--text-secondary)]"
175
+ }`}
176
+ >
177
+ Backtest
178
+ </button>
179
+ {compareResult && (
180
+ <button
181
+ onClick={() => setActiveTab("compare")}
182
+ className={`px-2 py-1 text-[10px] uppercase tracking-wider rounded ${
183
+ activeTab === "compare"
184
+ ? "bg-[var(--accent-orange)] text-black"
185
+ : "text-[var(--text-muted)] hover:text-[var(--text-secondary)]"
186
+ }`}
187
+ >
188
+ Compare
189
+ </button>
190
+ )}
191
+ <button
192
+ onClick={() => { setActiveTab("options"); loadLeapsScanner(); }}
193
+ className={`px-2 py-1 text-[10px] uppercase tracking-wider rounded ${
194
+ activeTab === "options"
195
+ ? "bg-[var(--accent-orange)] text-black"
196
+ : "text-[var(--text-muted)] hover:text-[var(--text-secondary)]"
197
+ }`}
198
+ >
199
+ Options
200
+ </button>
201
+ </div>
202
+ </div>
203
+
204
+ <div className="grid grid-cols-12 gap-6">
205
+ {/* Left Panel */}
206
+ <div className="col-span-12 lg:col-span-3 space-y-6">
207
+ <BacktestForm
208
+ onSubmit={handleBacktest}
209
+ onCompare={handleCompare}
210
+ onTickerChange={(ticker) => {
211
+ if (!ticker.trim()) return;
212
+ if (fundTimerRef.current) clearTimeout(fundTimerRef.current);
213
+ fundTimerRef.current = setTimeout(() => {
214
+ fetchFundamentals(ticker).then(setFundamentals).catch(() => {});
215
+ }, 800);
216
+ fetchOptionsFlow(ticker).then(setOptionsData).catch(() => {});
217
+ }}
218
+ isLoading={isLoading}
219
+ isComparing={isComparing}
220
+ />
221
+ {fundamentals && <FundamentalsPanel data={fundamentals} />}
222
+ </div>
223
+
224
+ {/* Right Panel */}
225
+ <div className="col-span-12 lg:col-span-9 space-y-6">
226
+ {/* Error */}
227
+ {error && (
228
+ <div className="bg-red-950/30 border border-red-800 rounded-lg p-4">
229
+ <div className="flex items-center gap-2 text-red-400 text-sm">
230
+ <span>[ERROR]</span>
231
+ <span>{error}</span>
232
+ </div>
233
+ </div>
234
+ )}
235
+
236
+ {/* Loading */}
237
+ {(isLoading || isComparing) && (
238
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-12 text-center">
239
+ <div className="inline-block w-8 h-8 border-2 border-[var(--accent-orange)] border-t-transparent rounded-full animate-spin mb-4" />
240
+ <div className="text-[var(--text-secondary)] text-sm">
241
+ {isComparing ? "Running all strategies..." : "Running backtest simulation..."}
242
+ </div>
243
+ <div className="text-[var(--text-muted)] text-xs mt-1">
244
+ Fetching data from Yahoo Finance + VectorBT analysis
245
+ </div>
246
+ </div>
247
+ )}
248
+
249
+ {/* Empty state */}
250
+ {!isLoading && !isComparing && !result && !compareResult && !error && (
251
+ <div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-12 text-center">
252
+ <div className="text-[var(--text-muted)] text-4xl mb-4">{">"}_</div>
253
+ <div className="text-[var(--text-secondary)] text-sm mb-2">Terminal.AI Ready</div>
254
+ <div className="text-[var(--text-muted)] text-xs">
255
+ Configure parameters and execute a backtest to begin analysis
256
+ </div>
257
+ </div>
258
+ )}
259
+
260
+ {/* Backtest Results */}
261
+ {result && !isLoading && activeTab === "backtest" && (
262
+ <>
263
+ <div className="flex items-center gap-4 text-xs text-[var(--text-muted)] flex-wrap">
264
+ <span>[{result.ticker}] {result.start_date} → {result.end_date}</span>
265
+ <span>|</span>
266
+ <span>{result.data_points} data points</span>
267
+ <span>|</span>
268
+ <span>Interval: {result.interval}</span>
269
+ <span>|</span>
270
+ <span>Strategy: {result.strategy.toUpperCase()}</span>
271
+ <label className="ml-auto flex items-center gap-2 cursor-pointer">
272
+ <span className="text-[var(--text-muted)] text-[10px] uppercase tracking-wider">
273
+ {useCandlestick ? "Candlestick" : "Line"}
274
+ </span>
275
+ <button
276
+ onClick={() => setUseCandlestick(!useCandlestick)}
277
+ className={`w-8 h-4 rounded-full transition-colors ${
278
+ useCandlestick ? "bg-[var(--accent-orange)]" : "bg-[var(--bg-tertiary)]"
279
+ }`}
280
+ >
281
+ <span
282
+ className={`block w-3 h-3 bg-black rounded-full transition-transform mt-0.5 ${
283
+ useCandlestick ? "translate-x-4" : "translate-x-0.5"
284
+ }`}
285
+ />
286
+ </button>
287
+ </label>
288
+ </div>
289
+
290
+ <PriceChart
291
+ dates={dates} open={open} high={high} low={low} close={close}
292
+ entries={entries} exits={exits} ticker={result.ticker}
293
+ overlays={overlays} candlestick={useCandlestick}
294
+ />
295
+
296
+ {volume.length > 0 && (
297
+ <VolumeChart dates={dates} volume={volume} close={close} />
298
+ )}
299
+
300
+ <EquityChart
301
+ dates={dates} equity={equityArr}
302
+ initCash={
303
+ result.portfolio.stats && typeof result.portfolio.stats["Start Value"] === "number"
304
+ ? (result.portfolio.stats["Start Value"] as number)
305
+ : 10000
306
+ }
307
+ />
308
+
309
+ {drawdownArr.length > 0 && (
310
+ <DrawdownChart
311
+ dates={dates} drawdown={drawdownArr}
312
+ maxDrawdown={
313
+ result.portfolio.stats && typeof result.portfolio.stats["Max Drawdown [%]"] === "number"
314
+ ? (result.portfolio.stats["Max Drawdown [%]"] as number) / 100
315
+ : undefined
316
+ }
317
+ />
318
+ )}
319
+
320
+ <div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
321
+ <IndicatorChart
322
+ dates={dates} values={rsiValues}
323
+ title={`RSI (${result.indicators.rsi.window})`}
324
+ color="#a78bfa"
325
+ overbought={result.indicators.rsi.overbought}
326
+ oversold={result.indicators.rsi.oversold}
327
+ />
328
+ <IndicatorChart
329
+ dates={dates} values={macdLine}
330
+ title={`MACD (${result.indicators.macd.fast_window}/${result.indicators.macd.slow_window}/${result.indicators.macd.signal_window})`}
331
+ color="#f59e0b"
332
+ secondaryValues={macdSignal}
333
+ secondaryColor="#3b82f6"
334
+ secondaryName="Signal"
335
+ histogramValues={macdHist}
336
+ />
337
+ </div>
338
+ </>
339
+ )}
340
+
341
+ {/* Compare Results */}
342
+ {compareResult && !isComparing && activeTab === "compare" && (
343
+ <>
344
+ <div className="flex items-center gap-4 text-xs text-[var(--text-muted)] flex-wrap">
345
+ <span>[{compareResult.ticker}] {compareResult.start_date} → {compareResult.end_date}</span>
346
+ <span>|</span>
347
+ <span>{compareResult.data_points} data points</span>
348
+ <span>|</span>
349
+ <span>Interval: {compareResult.interval}</span>
350
+ </div>
351
+
352
+ <ComparePanel
353
+ comparison={compareResult.comparison}
354
+ dates={compareResult.dates}
355
+ />
356
+
357
+ <div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
358
+ <IndicatorChart
359
+ dates={compareResult.dates}
360
+ values={dictToArray(compareResult.indicators.rsi.rsi as Record<string, number>, compareResult.dates)}
361
+ title={`RSI (${compareResult.indicators.rsi.window})`}
362
+ color="#a78bfa"
363
+ overbought={compareResult.indicators.rsi.overbought}
364
+ oversold={compareResult.indicators.rsi.oversold}
365
+ />
366
+ <IndicatorChart
367
+ dates={compareResult.dates}
368
+ values={dictToArray(compareResult.indicators.macd.macd as Record<string, number>, compareResult.dates)}
369
+ title={`MACD (${compareResult.indicators.macd.fast_window}/${compareResult.indicators.macd.slow_window}/${compareResult.indicators.macd.signal_window})`}
370
+ color="#f59e0b"
371
+ secondaryValues={dictToArray(compareResult.indicators.macd.signal as Record<string, number>, compareResult.dates)}
372
+ secondaryColor="#3b82f6"
373
+ secondaryName="Signal"
374
+ histogramValues={dictToArray(compareResult.indicators.macd.histogram as Record<string, number>, compareResult.dates)}
375
+ />
376
+ </div>
377
+ </>
378
+ )}
379
+
380
+ {/* Options Tab */}
381
+ {activeTab === "options" && (
382
+ <div className="space-y-6">
383
+ {/* Per-ticker options flow panel */}
384
+ {result && <OptionsFlowPanel ticker={result.ticker} />}
385
+ {!result && (
386
+ <div style={{
387
+ background: "var(--bg-secondary)", border: "1px solid var(--border-color)",
388
+ borderRadius: 8, padding: 48, textAlign: "center",
389
+ }}>
390
+ <div style={{ color: "var(--text-muted)", fontSize: 48, marginBottom: 16 }}>Op_</div>
391
+ <div style={{ color: "var(--text-secondary)", fontSize: 14, marginBottom: 8 }}>Options Flow</div>
392
+ <div style={{ color: "var(--text-muted)", fontSize: 12 }}>
393
+ Run a backtest first to view options flow for that ticker.
394
+ </div>
395
+ </div>
396
+ )}
397
+
398
+ {/* LEAPS Scanner — S&P 100 (en alt) */}
399
+ <div style={{
400
+ background: "#0a0a0a", border: "1px solid #1e1e1e",
401
+ borderRadius: 6, padding: 16,
402
+ }}>
403
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }}>
404
+ <h3 style={{
405
+ margin: 0, color: "#f97316", fontFamily: "monospace",
406
+ fontSize: 14, fontWeight: 700, letterSpacing: 1,
407
+ }}>
408
+ ⚑ LEAPS SCANNER — S&P 100 — anormal hacim/OI spike
409
+ </h3>
410
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
411
+ {leapsScannerInfo && (
412
+ <span style={{ color: "#555", fontFamily: "monospace", fontSize: 10 }}>
413
+ {leapsScannerInfo.scan_time_sec}s — {leapsScanner.length} anomalili
414
+ </span>
415
+ )}
416
+ <button
417
+ onClick={loadLeapsScanner}
418
+ disabled={leapsScannerLoading}
419
+ style={{
420
+ background: leapsScannerLoading ? "#333" : "#f97316",
421
+ color: leapsScannerLoading ? "#666" : "#000",
422
+ border: "none", borderRadius: 4, padding: "4px 10px",
423
+ fontFamily: "monospace", fontSize: 10, fontWeight: 700,
424
+ cursor: leapsScannerLoading ? "not-allowed" : "pointer",
425
+ }}
426
+ >
427
+ {leapsScannerLoading ? "Scanning…" : "↻ Rescan"}
428
+ </button>
429
+ </div>
430
+ </div>
431
+
432
+ {leapsScannerLoading ? (
433
+ <p style={{ color: "#888", fontFamily: "monospace" }}>Scanning S&P 100 for LEAPS anomalies… (~10s)</p>
434
+ ) : leapsScanner.length ? (
435
+ <table style={{
436
+ width: "100%", borderCollapse: "collapse", fontFamily: "monospace", fontSize: 11,
437
+ }}>
438
+ <thead>
439
+ <tr>
440
+ <th style={{ padding: "6px 8px", textAlign: "left", color: "#888" }}>Ticker</th>
441
+ <th style={{ padding: "6px 8px", textAlign: "right", color: "#888" }}>LEAPS Vol</th>
442
+ <th style={{ padding: "6px 8px", textAlign: "right", color: "#888" }}>C/P</th>
443
+ <th style={{ padding: "6px 8px", textAlign: "right", color: "#888" }}>Spikes</th>
444
+ <th style={{ padding: "6px 8px", textAlign: "left", color: "#888" }}>Top Spike</th>
445
+ <th style={{ padding: "6px 8px", textAlign: "right", color: "#888" }}>Signal</th>
446
+ </tr>
447
+ </thead>
448
+ <tbody>
449
+ {leapsScanner.map((entry, i) => {
450
+ const spike = entry.top_spike;
451
+ const handleClickTicker = async () => {
452
+ setActiveTab("backtest");
453
+ setIsLoading(true);
454
+ setError(null);
455
+ setResult(null);
456
+ setCompareResult(null);
457
+ setFundamentals(null);
458
+ try {
459
+ const ticker = entry.ticker;
460
+ // Get full date range for this ticker
461
+ let startDate, endDate;
462
+ try {
463
+ const { default: api } = await import("./api");
464
+ // fetchTickerRange is exported from api
465
+ const rangeRes = await fetch(`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/api/ticker/${encodeURIComponent(ticker)}/range`);
466
+ if (rangeRes.ok) {
467
+ const range = await rangeRes.json();
468
+ startDate = range.earliest.split("T")[0];
469
+ endDate = range.latest.split("T")[0];
470
+ }
471
+ } catch {}
472
+ if (!startDate) {
473
+ const end = new Date();
474
+ const start = new Date(end);
475
+ start.setFullYear(start.getFullYear() - 1);
476
+ const fmt = (d: Date) => d.toISOString().split("T")[0];
477
+ startDate = fmt(start);
478
+ endDate = fmt(end);
479
+ }
480
+ const config: BacktestConfig = {
481
+ ticker, start_date: startDate, end_date: endDate,
482
+ interval: "1d", strategy: "sma",
483
+ fast_ma: 10, slow_ma: 30, bb_window: 20, bb_std: 2.0,
484
+ fees: 0.001, init_cash: 10000,
485
+ rsi_window: 14, macd_fast: 12, macd_slow: 26, macd_signal: 9,
486
+ };
487
+ const [data, fundData] = await Promise.all([
488
+ runBacktest(config),
489
+ fetchFundamentals(ticker).catch(() => null),
490
+ ]);
491
+ setResult(data);
492
+ if (fundData) setFundamentals(fundData);
493
+ } catch (err) {
494
+ setError(err instanceof Error ? err.message : "Unknown error");
495
+ } finally {
496
+ setIsLoading(false);
497
+ }
498
+ };
499
+ return (
500
+ <tr
501
+ key={i}
502
+ onClick={handleClickTicker}
503
+ style={{ borderBottom: "1px solid #1a1a1a", cursor: "pointer" }}
504
+ className="hover:bg-[#1a1a1a] transition-colors"
505
+ >
506
+ <td style={{ padding: "6px 8px", color: "#f97316", fontWeight: 700 }}>{entry.ticker}</td>
507
+ <td style={{ padding: "6px 8px", textAlign: "right", color: "#e5e7eb" }}>{entry.total_leaps_vol.toLocaleString()}</td>
508
+ <td style={{
509
+ padding: "6px 8px", textAlign: "right",
510
+ color: entry.call_put_ratio > 1 ? "#22c55e" : "#ef4444",
511
+ }}>{entry.call_put_ratio.toFixed(2)}</td>
512
+ <td style={{ padding: "6px 8px", textAlign: "right", color: "#f97316" }}>{entry.unusual_count}</td>
513
+ <td style={{ padding: "6px 8px", color: "#888", fontSize: 10 }}>
514
+ {spike?.type} {spike?.expiry} K={spike?.strike?.toFixed(0)} vol={spike?.volume?.toLocaleString()} voi={spike?.vol_oi_ratio}
515
+ </td>
516
+ <td style={{
517
+ padding: "6px 8px", textAlign: "right", fontWeight: 700,
518
+ color: entry.call_put_ratio > 1.5 ? "#22c55e" : entry.call_put_ratio < 0.67 ? "#ef4444" : "#f59e0b",
519
+ }}>
520
+ {entry.call_put_ratio > 1.5 ? "Bullish" : entry.call_put_ratio < 0.67 ? "Bearish" : "Neutral"}
521
+ </td>
522
+ </tr>
523
+ );
524
+ })}
525
+ </tbody>
526
+ </table>
527
+ ) : !leapsScannerLoading ? (
528
+ <p style={{ color: "#555", fontFamily: "monospace", fontSize: 10 }}>
529
+ No LEAPS anomalies found. Click Rescan to try again.
530
+ </p>
531
+ ) : null}
532
+ </div>
533
+ </div>
534
+ )}
535
+ </div>
536
+ </div>
537
+ </div>
538
+ </main>
539
+
540
+ <footer className="border-t border-[var(--border-color)] bg-[var(--bg-secondary)] px-6 py-3">
541
+ <div className="max-w-[1600px] mx-auto flex items-center justify-between text-xs text-[var(--text-muted)]">
542
+ <span>Terminal.AI v1.1.0 — Powered by VectorBT + FastAPI + Next.js</span>
543
+ <span>Data: Yahoo Finance (yfinance)</span>
544
+ </div>
545
+ </footer>
546
+ </div>
547
+ );
548
+ }
frontend/eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
frontend/next.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true,
4
+ };
5
+
6
+ module.exports = nextConfig;
frontend/next.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;