GoshawkVortexAI commited on
Commit
05d69d0
·
verified ·
1 Parent(s): 2f62409

Delete backtest.py

Browse files
Files changed (1) hide show
  1. backtest.py +0 -393
backtest.py DELETED
@@ -1,393 +0,0 @@
1
- """
2
- backtest.py — Haber duygusunu piyasa fiyatıyla eşleştirip backtest eder.
3
-
4
- Akış:
5
- 1. Haberleri al (news_feed varsa onu, yoksa RSS yedeği)
6
- 2. Her habere model etiketi ver (Bullish / Neutral / Bearish)
7
- 3. Haber yayın zamanındaki Binance mumuyla eşleştir (giriş fiyatı)
8
- 4. Belirlenen ufuklarda (1s, 4s, 24s) ileri getiriyi hesapla
9
- 5. İsabet oranı, ortalama getiri, equity eğrisi ve haber bazlı tablo üret
10
-
11
- Not: Sinyal, haber yayınlandıktan sonraki İLK mumun açılışıyla işleme girer
12
- (look-ahead yok). Komisyon + kayma (bps) her işlemden düşülür.
13
- """
14
-
15
- import html
16
- import json
17
- import math
18
- import re
19
- import time
20
- import urllib.request
21
- import xml.etree.ElementTree as ET
22
- from datetime import datetime, timezone
23
- from email.utils import parsedate_to_datetime
24
-
25
- BINANCE_HOSTS = [
26
- "https://api.binance.com",
27
- "https://data-api.binance.vision",
28
- ]
29
-
30
- RSS_FEEDS = [
31
- "https://www.coindesk.com/arc/outboundfeeds/rss/",
32
- "https://cointelegraph.com/rss",
33
- "https://decrypt.co/feed",
34
- ]
35
-
36
- HORIZONS = {"1 saat": 1, "4 saat": 4, "24 saat": 24}
37
-
38
- _UA = {"User-Agent": "Mozilla/5.0 (GoshawkVortex-Backtest)"}
39
-
40
-
41
- # ------------------------------------------------------------
42
- # HTTP yardımcıları
43
- # ------------------------------------------------------------
44
-
45
- def _get(url, timeout=12):
46
- req = urllib.request.Request(url, headers=_UA)
47
- with urllib.request.urlopen(req, timeout=timeout) as r:
48
- return r.read()
49
-
50
-
51
- # ------------------------------------------------------------
52
- # HABER KAYNAĞI
53
- # ------------------------------------------------------------
54
-
55
- def fetch_rss_news(limit=120):
56
- """Yedek haber kaynağı. Her öğe: {title, ts (epoch sn), source}."""
57
- items = []
58
- for url in RSS_FEEDS:
59
- try:
60
- root = ET.fromstring(_get(url))
61
- except Exception:
62
- continue
63
- for it in root.iter("item"):
64
- title = (it.findtext("title") or "").strip()
65
- pub = it.findtext("pubDate")
66
- if not title or not pub:
67
- continue
68
- try:
69
- ts = parsedate_to_datetime(pub).astimezone(timezone.utc).timestamp()
70
- except Exception:
71
- continue
72
- items.append({
73
- "title": html.unescape(re.sub(r"<[^>]+>", "", title)),
74
- "ts": ts,
75
- "source": url.split("/")[2],
76
- })
77
- items.sort(key=lambda x: x["ts"], reverse=True)
78
- return items[:limit]
79
-
80
-
81
- # ------------------------------------------------------------
82
- # FİYAT VERİSİ (Binance)
83
- # ------------------------------------------------------------
84
-
85
- def fetch_klines(symbol, start_ms, end_ms, interval="1h"):
86
- """Binance mum verisi. Satır: [open_time_ms, open, high, low, close]."""
87
- out, cursor, last_err = [], int(start_ms), None
88
- while cursor < end_ms:
89
- data = None
90
- for host in BINANCE_HOSTS:
91
- url = (f"{host}/api/v3/klines?symbol={symbol}&interval={interval}"
92
- f"&startTime={cursor}&endTime={int(end_ms)}&limit=1000")
93
- try:
94
- data = json.loads(_get(url))
95
- break
96
- except Exception as e: # noqa: BLE001
97
- last_err = e
98
- if data is None:
99
- raise RuntimeError(f"Fiyat verisi alınamadı ({symbol}): {last_err}")
100
- if not data:
101
- break
102
- for k in data:
103
- out.append([int(k[0]), float(k[1]), float(k[2]), float(k[3]), float(k[4])])
104
- nxt = int(data[-1][0]) + 3_600_000
105
- if nxt <= cursor:
106
- break
107
- cursor = nxt
108
- if len(data) < 1000:
109
- break
110
- return out
111
-
112
-
113
- # ------------------------------------------------------------
114
- # EŞLEŞTİRME + BACKTEST ÇEKİRDEĞİ (ağ gerektirmez, test edilebilir)
115
- # ------------------------------------------------------------
116
-
117
- def _candle_index(candles, ts_ms):
118
- """ts'den SONRA açılan ilk mumun indeksi (look-ahead yok)."""
119
- lo, hi = 0, len(candles)
120
- while lo < hi:
121
- mid = (lo + hi) // 2
122
- if candles[mid][0] > ts_ms:
123
- hi = mid
124
- else:
125
- lo = mid + 1
126
- return lo if lo < len(candles) else None
127
-
128
-
129
- def direction_of(label, score=None, threshold=0.0):
130
- """Bullish → +1, Bearish → -1, Neutral → 0."""
131
- l = (label or "").strip().lower()
132
- if l.startswith("bull"):
133
- return 1
134
- if l.startswith("bear"):
135
- return -1
136
- return 0
137
-
138
-
139
- def match_and_evaluate(signals, candles_by_symbol, horizon_h=4,
140
- fee_bps=10, slippage_bps=5, long_only=False):
141
- """
142
- signals: [{title, ts, symbol, label, score}]
143
- candles_by_symbol: {symbol: [[open_ms, o, h, l, c], ...]} (1s mumlar)
144
- Döner: trades listesi.
145
- """
146
- cost = 2 * (fee_bps + slippage_bps) / 10_000 # gir + çık
147
- trades = []
148
- for s in signals:
149
- d = direction_of(s["label"])
150
- if d == 0 or (long_only and d < 0):
151
- continue
152
- candles = candles_by_symbol.get(s["symbol"])
153
- if not candles:
154
- continue
155
- i = _candle_index(candles, int(s["ts"] * 1000))
156
- if i is None or i + horizon_h >= len(candles):
157
- continue # ufuk henüz dolmadı → değerlendirilemez
158
- entry = candles[i][1] # sonraki mumun açılışı
159
- exit_ = candles[i + horizon_h][1] # ufuk sonundaki açılış
160
- if entry <= 0:
161
- continue
162
- raw = (exit_ / entry - 1) * d
163
- ret = raw - cost
164
- trades.append({
165
- "ts": s["ts"], "symbol": s["symbol"], "title": s["title"],
166
- "label": s["label"], "dir": d, "entry": entry, "exit": exit_,
167
- "ret": ret, "win": ret > 0,
168
- "raw": raw, "mkt": exit_ / entry - 1,
169
- })
170
- trades.sort(key=lambda t: t["ts"])
171
- return trades
172
-
173
-
174
- def summarize(trades):
175
- n = len(trades)
176
- if n == 0:
177
- return {"n": 0}
178
- rets = [t["ret"] for t in trades]
179
- wins = sum(1 for t in trades if t["win"])
180
- mean = sum(rets) / n
181
- var = sum((r - mean) ** 2 for r in rets) / (n - 1) if n > 1 else 0.0
182
- std = math.sqrt(var)
183
- equity, peak, mdd, eq = 1.0, 1.0, 0.0, [1.0]
184
- for r in rets:
185
- equity *= (1 + r) # işlemler sırayla, eşit ağırlık
186
- peak = max(peak, equity)
187
- mdd = max(mdd, (peak - equity) / peak)
188
- eq.append(equity)
189
- gross_win = sum(r for r in rets if r > 0)
190
- gross_loss = -sum(r for r in rets if r < 0)
191
- longs = [t for t in trades if t["dir"] > 0]
192
- shorts = [t for t in trades if t["dir"] < 0]
193
- def _wr(ts_, key):
194
- return (sum(1 for t in ts_ if t[key] > 0) / len(ts_)) if ts_ else None
195
- diag = {
196
- "gross_win": _wr(trades, "raw"),
197
- "mkt_mean": sum(t["mkt"] for t in trades) / n,
198
- "n_long": len(longs), "n_short": len(shorts),
199
- "long_gross_win": _wr(longs, "raw"), "short_gross_win": _wr(shorts, "raw"),
200
- "n_symbols": len({t["symbol"] for t in trades}),
201
- "span_h": (trades[-1]["ts"] - trades[0]["ts"]) / 3600,
202
- }
203
- return {
204
- **diag,
205
- "n": n,
206
- "win_rate": wins / n,
207
- "mean": mean,
208
- "total": equity - 1,
209
- "std": std,
210
- "t_stat": (mean / (std / math.sqrt(n))) if std > 0 and n > 1 else 0.0,
211
- "profit_factor": (gross_win / gross_loss) if gross_loss > 0 else float("inf"),
212
- "max_dd": mdd,
213
- "equity": eq,
214
- "best": max(rets), "worst": min(rets),
215
- }
216
-
217
-
218
- # ------------------------------------------------------------
219
- # ÜST SEVİYE: haberleri çek → etiketle → fiyatla → raporla
220
- # ------------------------------------------------------------
221
-
222
- def _symbol_for(asset):
223
- a = (asset or "").upper().replace("/", "").replace("-", "")
224
- if not a or a == "ALL":
225
- return None
226
- return a if a.endswith(("USDT", "USDC")) else a + "USDT"
227
-
228
-
229
- def collect_signals(classify_fn, detect_fn, asset="ALL", max_news=60,
230
- news_fn=None, log=None):
231
- """Haberleri çekip varlıklara ve duygu etiketine bağlar."""
232
- news = (news_fn or fetch_rss_news)(max_news * 3)
233
- want = _symbol_for(asset)
234
- signals = []
235
- for n in news:
236
- if len(signals) >= max_news:
237
- break
238
- try:
239
- assets = detect_fn(n["title"]) or []
240
- except Exception:
241
- assets = []
242
- # detect_assets çıktısı str veya (str, ...) olabilir → normalize et
243
- syms = []
244
- for a in assets:
245
- a = a[0] if isinstance(a, (tuple, list)) and a else a
246
- sym = _symbol_for(str(a))
247
- if sym:
248
- syms.append(sym)
249
- if want:
250
- if want not in syms:
251
- continue
252
- syms = [want]
253
- if not syms:
254
- continue
255
- try:
256
- label, score = classify_fn(n["title"])
257
- except Exception as e: # noqa: BLE001
258
- if log is not None:
259
- log.append(f"Etiketleme hatası: {e}")
260
- continue
261
- signals.append({"title": n["title"], "ts": n["ts"], "symbol": syms[0],
262
- "label": label, "score": score,
263
- "source": n.get("source", "")})
264
- return signals
265
-
266
-
267
- def run_backtest(classify_fn, detect_fn, asset="ALL", max_news=60,
268
- horizon_label="4 saat", fee_bps=10, slippage_bps=5,
269
- long_only=False, news_fn=None, price_fn=None):
270
- """Tam akış. Döner: (signals, trades, summary, notes)."""
271
- notes = []
272
- horizon_h = HORIZONS.get(horizon_label, 4)
273
- signals = collect_signals(classify_fn, detect_fn, asset, max_news,
274
- news_fn=news_fn, log=notes)
275
- if not signals:
276
- return [], [], {"n": 0}, notes + ["Eşleşen haber bulunamadı."]
277
-
278
- price_fn = price_fn or fetch_klines
279
- candles = {}
280
- t0 = min(s["ts"] for s in signals) * 1000 - 3_600_000
281
- t1 = max(time.time() * 1000, max(s["ts"] for s in signals) * 1000)
282
- for sym in sorted({s["symbol"] for s in signals}):
283
- try:
284
- candles[sym] = price_fn(sym, t0, t1 + horizon_h * 3_600_000)
285
- except Exception as e: # noqa: BLE001
286
- notes.append(str(e))
287
-
288
- trades = match_and_evaluate(signals, candles, horizon_h, fee_bps,
289
- slippage_bps, long_only)
290
- skipped = sum(1 for s in signals if direction_of(s["label"]) != 0) - len(trades)
291
- if skipped > 0:
292
- notes.append(f"{skipped} sinyal atlandı (ufuk henüz dolmadı veya fiyat yok).")
293
- return signals, trades, summarize(trades), notes
294
-
295
-
296
- # ------------------------------------------------------------
297
- # HTML RAPOR (uygulamanın koyu temasına uygun)
298
- # ------------------------------------------------------------
299
-
300
- def _pct(x, d=2):
301
- return f"{x * 100:+.{d}f}%"
302
-
303
-
304
- def _equity_svg(eq, w=640, h=140):
305
- if len(eq) < 2:
306
- return ""
307
- lo, hi = min(eq), max(eq)
308
- span = (hi - lo) or 1.0
309
- pts = " ".join(
310
- f"{i * w / (len(eq) - 1):.1f},{h - (v - lo) / span * (h - 12) - 6:.1f}"
311
- for i, v in enumerate(eq)
312
- )
313
- base = h - (1.0 - lo) / span * (h - 12) - 6
314
- color = "#22c55e" if eq[-1] >= 1 else "#ef4444"
315
- return (f"<svg viewBox='0 0 {w} {h}' style='width:100%;height:{h}px'>"
316
- f"<line x1='0' x2='{w}' y1='{base:.1f}' y2='{base:.1f}' "
317
- f"stroke='#334155' stroke-dasharray='4 4'/>"
318
- f"<polyline fill='none' stroke='{color}' stroke-width='2.5' points='{pts}'/></svg>")
319
-
320
-
321
- def build_report_html(signals, trades, s, notes, horizon_label, asset):
322
- box = ("padding:18px;border-radius:16px;margin-top:14px;color:#dbeafe;"
323
- "background:rgba(10,18,32,.82);border:1px solid rgba(96,165,250,.18)")
324
- if not s.get("n"):
325
- msg = "<br>".join(html.escape(n) for n in notes) or "İşlem üretilemedi."
326
- return f"<div style='{box}'>Backtest sonucu yok.<br><small>{msg}</small></div>"
327
-
328
- def card(label, val, color="#e2e8f0"):
329
- return (f"<div style='flex:1;min-width:120px;padding:12px;border-radius:12px;"
330
- f"background:#0f1a2c'><div style='font-size:11px;color:#8295ad;"
331
- f"letter-spacing:1px'>{label}</div><div style='font-size:22px;"
332
- f"font-weight:800;color:{color}'>{val}</div></div>")
333
-
334
- up, dn = "#22c55e", "#ef4444"
335
- pf = "∞" if s["profit_factor"] == float("inf") else f"{s['profit_factor']:.2f}"
336
- cards = "".join([
337
- card("İŞLEM", s["n"]),
338
- card("İSABET", f"{s['win_rate'] * 100:.0f}%", up if s["win_rate"] > .5 else dn),
339
- card("ORT. GETİRİ", _pct(s["mean"]), up if s["mean"] > 0 else dn),
340
- card("TOPLAM", _pct(s["total"]), up if s["total"] > 0 else dn),
341
- card("PROFIT FACTOR", pf),
342
- card("MAX DD", f"-{s['max_dd'] * 100:.1f}%", dn),
343
- card("t-İSTATİSTİĞİ", f"{s['t_stat']:.2f}"),
344
- ])
345
-
346
- rows = []
347
- for t in reversed(trades[-60:]):
348
- c = up if t["ret"] > 0 else dn
349
- when = datetime.fromtimestamp(t["ts"], tz=timezone.utc).strftime("%d.%m %H:%M")
350
- arrow = "▲" if t["dir"] > 0 else "▼"
351
- rows.append(
352
- f"<tr><td>{when}</td><td>{t['symbol'].replace('USDT', '')}</td>"
353
- f"<td>{arrow} {html.escape(t['label'])}</td>"
354
- f"<td style='max-width:360px'>{html.escape(t['title'][:110])}</td>"
355
- f"<td>{t['entry']:,.2f}</td><td>{t['exit']:,.2f}</td>"
356
- f"<td style='color:{c};font-weight:700'>{_pct(t['ret'])}</td></tr>")
357
-
358
- caveat = ""
359
- if s["n"] < 30:
360
- caveat = ("<div style='margin-top:10px;color:#eab308;font-size:12px'>"
361
- f"⚠️ Yalnızca {s['n']} işlem var; bu örneklem istatistiksel olarak "
362
- "güvenilir sonuç vermek için küçük. t-istatistiği |2| altındaysa "
363
- "sonuç şansla açıklanabilir.</div>")
364
- f = lambda v: "-" if v is None else f"{v * 100:.0f}%"
365
- diag_html = (
366
- "<div style='margin-top:12px;padding:10px;border-radius:10px;background:#0f1a2c;"
367
- "font-size:12px;color:#94a3b8'><b style='color:#e2e8f0'>Tanı</b><br>"
368
- f"Maliyet ÖNCESİ isabet: {f(s['gross_win'])} • Long: {s['n_long']} işlem "
369
- f"(brüt isabet {f(s['long_gross_win'])}) • Short: {s['n_short']} işlem "
370
- f"(brüt isabet {f(s['short_gross_win'])})<br>"
371
- f"Aynı pencerede piyasanın ort. hareketi: {_pct(s['mkt_mean'])} • "
372
- f"Pencere: {s['span_h']:.0f} saat • Parite: {s['n_symbols']}</div>")
373
- note_html = diag_html + "".join(f"<div style='color:#8295ad;font-size:12px'>• {html.escape(n)}</div>"
374
- for n in notes)
375
- th = "text-align:left;color:#8295ad;font-size:11px;padding:6px 8px"
376
- return f"""
377
- <div style='{box}'>
378
- <div style='font-weight:800;letter-spacing:2px;font-size:15px'>HABER → FİYAT BACKTEST</div>
379
- <div style='color:#8295ad;font-size:12px;margin:5px 0 14px'>
380
- Varlık: {html.escape(asset)} • Ufuk: {horizon_label} • Giriş: haberden sonraki ilk mum açılışı
381
- </div>
382
- <div style='display:flex;gap:10px;flex-wrap:wrap'>{cards}</div>
383
- <div style='margin-top:16px'>{_equity_svg(s['equity'])}</div>
384
- {caveat}{note_html}
385
- </div>
386
- <div style='{box};overflow-x:auto'>
387
- <table style='width:100%;border-collapse:collapse;font-size:12px'>
388
- <tr><th style='{th}'>Zaman (UTC)</th><th style='{th}'>Parite</th><th style='{th}'>Sinyal</th>
389
- <th style='{th}'>Haber</th><th style='{th}'>Giriş</th><th style='{th}'>Çıkış</th>
390
- <th style='{th}'>Getiri</th></tr>
391
- {''.join(rows)}
392
- </table>
393
- </div>"""