GoshawkVortexAI commited on
Commit
1da523c
·
verified ·
1 Parent(s): af0405b

Delete backtest.py

Browse files
Files changed (1) hide show
  1. backtest.py +0 -370
backtest.py DELETED
@@ -1,370 +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
- })
169
- trades.sort(key=lambda t: t["ts"])
170
- return trades
171
-
172
-
173
- def summarize(trades):
174
- n = len(trades)
175
- if n == 0:
176
- return {"n": 0}
177
- rets = [t["ret"] for t in trades]
178
- wins = sum(1 for t in trades if t["win"])
179
- mean = sum(rets) / n
180
- var = sum((r - mean) ** 2 for r in rets) / (n - 1) if n > 1 else 0.0
181
- std = math.sqrt(var)
182
- equity, peak, mdd, eq = 1.0, 1.0, 0.0, [1.0]
183
- for r in rets:
184
- equity *= (1 + r) # işlemler sırayla, eşit ağırlık
185
- peak = max(peak, equity)
186
- mdd = max(mdd, (peak - equity) / peak)
187
- eq.append(equity)
188
- gross_win = sum(r for r in rets if r > 0)
189
- gross_loss = -sum(r for r in rets if r < 0)
190
- return {
191
- "n": n,
192
- "win_rate": wins / n,
193
- "mean": mean,
194
- "total": equity - 1,
195
- "std": std,
196
- "t_stat": (mean / (std / math.sqrt(n))) if std > 0 and n > 1 else 0.0,
197
- "profit_factor": (gross_win / gross_loss) if gross_loss > 0 else float("inf"),
198
- "max_dd": mdd,
199
- "equity": eq,
200
- "best": max(rets), "worst": min(rets),
201
- }
202
-
203
-
204
- # ------------------------------------------------------------
205
- # ÜST SEVİYE: haberleri çek → etiketle → fiyatla → raporla
206
- # ------------------------------------------------------------
207
-
208
- def _symbol_for(asset):
209
- a = (asset or "").upper().replace("/", "").replace("-", "")
210
- if not a or a == "ALL":
211
- return None
212
- return a if a.endswith(("USDT", "USDC")) else a + "USDT"
213
-
214
-
215
- def collect_signals(classify_fn, detect_fn, asset="ALL", max_news=60,
216
- news_fn=None, log=None):
217
- """Haberleri çekip varlıklara ve duygu etiketine bağlar."""
218
- news = (news_fn or fetch_rss_news)(max_news * 3)
219
- want = _symbol_for(asset)
220
- signals = []
221
- for n in news:
222
- if len(signals) >= max_news:
223
- break
224
- try:
225
- assets = detect_fn(n["title"]) or []
226
- except Exception:
227
- assets = []
228
- # detect_assets çıktısı str veya (str, ...) olabilir → normalize et
229
- syms = []
230
- for a in assets:
231
- a = a[0] if isinstance(a, (tuple, list)) and a else a
232
- sym = _symbol_for(str(a))
233
- if sym:
234
- syms.append(sym)
235
- if want:
236
- if want not in syms:
237
- continue
238
- syms = [want]
239
- if not syms:
240
- continue
241
- try:
242
- label, score = classify_fn(n["title"])
243
- except Exception as e: # noqa: BLE001
244
- if log is not None:
245
- log.append(f"Etiketleme hatası: {e}")
246
- continue
247
- signals.append({"title": n["title"], "ts": n["ts"], "symbol": syms[0],
248
- "label": label, "score": score,
249
- "source": n.get("source", "")})
250
- return signals
251
-
252
-
253
- def run_backtest(classify_fn, detect_fn, asset="ALL", max_news=60,
254
- horizon_label="4 saat", fee_bps=10, slippage_bps=5,
255
- long_only=False, news_fn=None, price_fn=None):
256
- """Tam akış. Döner: (signals, trades, summary, notes)."""
257
- notes = []
258
- horizon_h = HORIZONS.get(horizon_label, 4)
259
- signals = collect_signals(classify_fn, detect_fn, asset, max_news,
260
- news_fn=news_fn, log=notes)
261
- if not signals:
262
- return [], [], {"n": 0}, notes + ["Eşleşen haber bulunamadı."]
263
-
264
- price_fn = price_fn or fetch_klines
265
- candles = {}
266
- t0 = min(s["ts"] for s in signals) * 1000 - 3_600_000
267
- t1 = max(time.time() * 1000, max(s["ts"] for s in signals) * 1000)
268
- for sym in sorted({s["symbol"] for s in signals}):
269
- try:
270
- candles[sym] = price_fn(sym, t0, t1 + horizon_h * 3_600_000)
271
- except Exception as e: # noqa: BLE001
272
- notes.append(str(e))
273
-
274
- trades = match_and_evaluate(signals, candles, horizon_h, fee_bps,
275
- slippage_bps, long_only)
276
- skipped = sum(1 for s in signals if direction_of(s["label"]) != 0) - len(trades)
277
- if skipped > 0:
278
- notes.append(f"{skipped} sinyal atlandı (ufuk henüz dolmadı veya fiyat yok).")
279
- return signals, trades, summarize(trades), notes
280
-
281
-
282
- # ------------------------------------------------------------
283
- # HTML RAPOR (uygulamanın koyu temasına uygun)
284
- # ------------------------------------------------------------
285
-
286
- def _pct(x, d=2):
287
- return f"{x * 100:+.{d}f}%"
288
-
289
-
290
- def _equity_svg(eq, w=640, h=140):
291
- if len(eq) < 2:
292
- return ""
293
- lo, hi = min(eq), max(eq)
294
- span = (hi - lo) or 1.0
295
- pts = " ".join(
296
- f"{i * w / (len(eq) - 1):.1f},{h - (v - lo) / span * (h - 12) - 6:.1f}"
297
- for i, v in enumerate(eq)
298
- )
299
- base = h - (1.0 - lo) / span * (h - 12) - 6
300
- color = "#22c55e" if eq[-1] >= 1 else "#ef4444"
301
- return (f"<svg viewBox='0 0 {w} {h}' style='width:100%;height:{h}px'>"
302
- f"<line x1='0' x2='{w}' y1='{base:.1f}' y2='{base:.1f}' "
303
- f"stroke='#334155' stroke-dasharray='4 4'/>"
304
- f"<polyline fill='none' stroke='{color}' stroke-width='2.5' points='{pts}'/></svg>")
305
-
306
-
307
- def build_report_html(signals, trades, s, notes, horizon_label, asset):
308
- box = ("padding:18px;border-radius:16px;margin-top:14px;color:#dbeafe;"
309
- "background:rgba(10,18,32,.82);border:1px solid rgba(96,165,250,.18)")
310
- if not s.get("n"):
311
- msg = "<br>".join(html.escape(n) for n in notes) or "İşlem üretilemedi."
312
- return f"<div style='{box}'>Backtest sonucu yok.<br><small>{msg}</small></div>"
313
-
314
- def card(label, val, color="#e2e8f0"):
315
- return (f"<div style='flex:1;min-width:120px;padding:12px;border-radius:12px;"
316
- f"background:#0f1a2c'><div style='font-size:11px;color:#8295ad;"
317
- f"letter-spacing:1px'>{label}</div><div style='font-size:22px;"
318
- f"font-weight:800;color:{color}'>{val}</div></div>")
319
-
320
- up, dn = "#22c55e", "#ef4444"
321
- pf = "∞" if s["profit_factor"] == float("inf") else f"{s['profit_factor']:.2f}"
322
- cards = "".join([
323
- card("İŞLEM", s["n"]),
324
- card("İSABET", f"{s['win_rate'] * 100:.0f}%", up if s["win_rate"] > .5 else dn),
325
- card("ORT. GETİRİ", _pct(s["mean"]), up if s["mean"] > 0 else dn),
326
- card("TOPLAM", _pct(s["total"]), up if s["total"] > 0 else dn),
327
- card("PROFIT FACTOR", pf),
328
- card("MAX DD", f"-{s['max_dd'] * 100:.1f}%", dn),
329
- card("t-İSTATİSTİĞİ", f"{s['t_stat']:.2f}"),
330
- ])
331
-
332
- rows = []
333
- for t in reversed(trades[-60:]):
334
- c = up if t["ret"] > 0 else dn
335
- when = datetime.fromtimestamp(t["ts"], tz=timezone.utc).strftime("%d.%m %H:%M")
336
- arrow = "▲" if t["dir"] > 0 else "▼"
337
- rows.append(
338
- f"<tr><td>{when}</td><td>{t['symbol'].replace('USDT', '')}</td>"
339
- f"<td>{arrow} {html.escape(t['label'])}</td>"
340
- f"<td style='max-width:360px'>{html.escape(t['title'][:110])}</td>"
341
- f"<td>{t['entry']:,.2f}</td><td>{t['exit']:,.2f}</td>"
342
- f"<td style='color:{c};font-weight:700'>{_pct(t['ret'])}</td></tr>")
343
-
344
- caveat = ""
345
- if s["n"] < 30:
346
- caveat = ("<div style='margin-top:10px;color:#eab308;font-size:12px'>"
347
- f"⚠️ Yalnızca {s['n']} işlem var; bu örneklem istatistiksel olarak "
348
- "güvenilir sonuç vermek için küçük. t-istatistiği |2| altındaysa "
349
- "sonuç şansla açıklanabilir.</div>")
350
- note_html = "".join(f"<div style='color:#8295ad;font-size:12px'>• {html.escape(n)}</div>"
351
- for n in notes)
352
- th = "text-align:left;color:#8295ad;font-size:11px;padding:6px 8px"
353
- return f"""
354
- <div style='{box}'>
355
- <div style='font-weight:800;letter-spacing:2px;font-size:15px'>HABER → FİYAT BACKTEST</div>
356
- <div style='color:#8295ad;font-size:12px;margin:5px 0 14px'>
357
- Varlık: {html.escape(asset)} • Ufuk: {horizon_label} • Giriş: haberden sonraki ilk mum açılışı
358
- </div>
359
- <div style='display:flex;gap:10px;flex-wrap:wrap'>{cards}</div>
360
- <div style='margin-top:16px'>{_equity_svg(s['equity'])}</div>
361
- {caveat}{note_html}
362
- </div>
363
- <div style='{box};overflow-x:auto'>
364
- <table style='width:100%;border-collapse:collapse;font-size:12px'>
365
- <tr><th style='{th}'>Zaman (UTC)</th><th style='{th}'>Parite</th><th style='{th}'>Sinyal</th>
366
- <th style='{th}'>Haber</th><th style='{th}'>Giriş</th><th style='{th}'>Çıkış</th>
367
- <th style='{th}'>Getiri</th></tr>
368
- {''.join(rows)}
369
- </table>
370
- </div>"""