GoshawkVortexAI commited on
Commit
86aa19b
·
verified ·
1 Parent(s): 5c95dcd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1082 -851
app.py CHANGED
@@ -5,55 +5,65 @@ warnings.filterwarnings("ignore")
5
  import time
6
  import numpy as np
7
  import pandas as pd
8
- import gradio as gr
9
  import ccxt
 
10
  import optuna
11
 
12
- from sklearn.ensemble import (
13
- ExtraTreesClassifier,
14
- HistGradientBoostingClassifier
15
- )
16
- from sklearn.linear_model import LogisticRegression
17
  from sklearn.pipeline import Pipeline
18
  from sklearn.preprocessing import StandardScaler
19
- from sklearn.model_selection import TimeSeriesSplit
20
  from sklearn.metrics import (
21
  accuracy_score,
22
  balanced_accuracy_score,
23
- roc_auc_score
24
  )
 
25
 
26
 
27
- # =========================================================
28
  # CONFIG
29
- # =========================================================
30
 
31
  SYMBOL = "BTC/USDT:USDT"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- FEATURES = [
34
- "rsi",
35
- "macd_pct",
36
- "macd_signal_pct",
37
- "macd_hist_pct",
38
- "supertrend_direction",
39
- "supertrend_distance",
40
- "vwap_distance",
41
- "atr_pct",
42
- "return_1",
43
- "return_3",
44
- "return_6",
45
- "volatility_14",
46
- "volume_ratio",
47
- "body_pct",
48
- "high_low_pct"
49
  ]
50
 
51
 
52
- # =========================================================
53
- # OKX DATA FETCH
54
- # =========================================================
55
 
56
- def fetch_data(timeframe="1h", candles=2000):
57
 
58
  exchange = ccxt.okx({
59
  "enableRateLimit": True,
@@ -63,67 +73,63 @@ def fetch_data(timeframe="1h", candles=2000):
63
  }
64
  })
65
 
 
 
 
 
 
 
 
66
  exchange.load_markets()
67
 
68
  if SYMBOL not in exchange.markets:
69
  raise ValueError(
70
- f"OKX perpetual sembolü bulunamadı: {SYMBOL}"
71
  )
72
 
73
- timeframe_ms = exchange.parse_timeframe(timeframe) * 1000
74
 
75
- since = (
76
- exchange.milliseconds()
77
- - (candles + 10) * timeframe_ms
78
- )
79
 
80
- all_rows = {}
81
- requests = 0
82
- max_requests = max(20, candles // 80 + 10)
83
 
84
- while len(all_rows) < candles and requests < max_requests:
85
 
86
- requests += 1
87
 
88
  batch = exchange.fetch_ohlcv(
89
  SYMBOL,
90
- timeframe=timeframe,
91
- since=since,
92
  limit=100
93
  )
94
 
95
  if not batch:
96
  break
97
 
98
- old_count = len(all_rows)
99
-
100
- for row in batch:
101
- all_rows[int(row[0])] = row
102
 
103
- new_count = len(all_rows)
104
 
105
- last_timestamp = max(int(row[0]) for row in batch)
106
-
107
- new_since = last_timestamp + timeframe_ms
108
-
109
- if new_since <= since:
110
  break
111
 
112
- since = new_since
113
 
114
- if new_count == old_count:
115
  break
116
 
117
- if len(batch) < 100:
118
  break
119
 
120
  time.sleep(exchange.rateLimit / 1000)
121
 
122
- if not all_rows:
123
  raise ValueError("OKX veri döndürmedi.")
124
 
125
  df = pd.DataFrame(
126
- list(all_rows.values()),
127
  columns=[
128
  "timestamp",
129
  "open",
@@ -134,643 +140,853 @@ def fetch_data(timeframe="1h", candles=2000):
134
  ]
135
  )
136
 
 
 
 
 
137
  df = df.sort_values("timestamp")
138
- df = df.drop_duplicates("timestamp")
139
 
140
- df["timestamp"] = pd.to_datetime(
141
- df["timestamp"],
142
- unit="ms",
143
- utc=True
144
- )
 
145
 
146
- df = df.set_index("timestamp")
147
 
148
- for col in [
149
- "open",
150
- "high",
151
- "low",
152
- "close",
153
- "volume"
154
- ]:
155
  df[col] = pd.to_numeric(
156
  df[col],
157
  errors="coerce"
158
  )
159
 
160
- df = df.dropna()
 
 
 
161
 
162
- # Henüz tamamlanmamış mum
163
- if len(df) > 1:
164
- df = df.iloc[:-1]
165
 
166
- df = df.tail(candles)
 
 
 
 
167
 
168
- if len(df) < 200:
169
  raise ValueError(
170
- f"OKX yalnızca {len(df)} mum döndürdü. "
171
- "Daha yüksek mum sayısı veya farklı timeframe deneyin."
172
  )
173
 
174
  return df
175
 
176
 
177
- # =========================================================
178
- # INDICATORS
179
- # =========================================================
180
 
181
- def calculate_indicators(df):
182
 
183
- df = df.copy()
184
 
185
- # ATR 10
186
- previous_close = df["close"].shift(1)
187
 
188
- tr = pd.concat([
189
- df["high"] - df["low"],
190
- (df["high"] - previous_close).abs(),
191
- (df["low"] - previous_close).abs()
192
- ], axis=1).max(axis=1)
193
 
194
- df["atr"] = tr.ewm(
195
- alpha=1 / 10,
196
- adjust=False,
197
- min_periods=10
198
- ).mean()
199
 
200
- # RSI 14
201
- delta = df["close"].diff()
202
 
203
- gain = delta.clip(lower=0)
204
- loss = -delta.clip(upper=0)
 
 
205
 
206
- avg_gain = gain.ewm(
207
- alpha=1 / 14,
208
- adjust=False,
209
- min_periods=14
210
- ).mean()
211
 
212
- avg_loss = loss.ewm(
213
- alpha=1 / 14,
214
- adjust=False,
215
- min_periods=14
216
- ).mean()
217
 
218
- rs = avg_gain / avg_loss.replace(0, np.nan)
 
 
219
 
220
- df["rsi"] = 100 - 100 / (1 + rs)
221
 
222
- # MACD 12,26,9
223
- ema12 = df["close"].ewm(
224
- span=12,
225
- adjust=False
226
- ).mean()
227
 
228
- ema26 = df["close"].ewm(
229
- span=26,
230
- adjust=False
231
- ).mean()
232
 
233
- df["macd"] = ema12 - ema26
234
 
235
- df["macd_signal"] = df["macd"].ewm(
236
- span=9,
237
- adjust=False
238
- ).mean()
239
 
240
- df["macd_hist"] = (
241
- df["macd"] - df["macd_signal"]
242
- )
243
 
244
- # SUPERTREND 10,3
245
- hl2 = (df["high"] + df["low"]) / 2
246
 
247
- basic_upper = hl2 + 3 * df["atr"]
248
- basic_lower = hl2 - 3 * df["atr"]
249
 
250
- final_upper = basic_upper.copy()
251
- final_lower = basic_lower.copy()
252
 
253
- direction = pd.Series(
254
- 1.0,
255
- index=df.index
256
- )
257
 
258
- supertrend = pd.Series(
259
- np.nan,
260
- index=df.index
261
- )
262
 
263
- for i in range(1, len(df)):
264
 
265
- previous_close_value = df["close"].iloc[i - 1]
 
 
 
 
266
 
267
- if (
268
- basic_upper.iloc[i] < final_upper.iloc[i - 1]
269
- or previous_close_value > final_upper.iloc[i - 1]
270
- ):
271
- final_upper.iloc[i] = basic_upper.iloc[i]
272
- else:
273
- final_upper.iloc[i] = final_upper.iloc[i - 1]
274
 
275
- if (
276
- basic_lower.iloc[i] > final_lower.iloc[i - 1]
277
- or previous_close_value < final_lower.iloc[i - 1]
278
- ):
279
- final_lower.iloc[i] = basic_lower.iloc[i]
280
- else:
281
- final_lower.iloc[i] = final_lower.iloc[i - 1]
282
 
283
- current_close = df["close"].iloc[i]
284
 
285
- if direction.iloc[i - 1] == -1:
286
 
287
- direction.iloc[i] = (
288
- 1 if current_close > final_upper.iloc[i]
289
- else -1
290
- )
291
 
292
- else:
 
293
 
294
- direction.iloc[i] = (
295
- -1 if current_close < final_lower.iloc[i]
296
- else 1
297
- )
 
 
 
 
 
 
 
 
 
 
 
298
 
299
- supertrend.iloc[i] = (
300
- final_lower.iloc[i]
301
- if direction.iloc[i] == 1
302
- else final_upper.iloc[i]
 
 
 
 
 
 
303
  )
304
 
305
- df["supertrend"] = supertrend
306
- df["supertrend_direction"] = direction
307
 
308
- # VWAP - UTC daily reset
309
- typical_price = (
310
- df["high"] + df["low"] + df["close"]
311
- ) / 3
312
 
313
- daily_group = df.index.date
 
 
 
314
 
315
- cumulative_pv = (
316
- (typical_price * df["volume"])
317
- .groupby(daily_group)
318
- .cumsum()
319
- )
320
 
321
- cumulative_volume = (
322
- df["volume"]
323
- .groupby(daily_group)
324
- .cumsum()
325
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
- df["vwap"] = (
328
- cumulative_pv /
329
- cumulative_volume.replace(0, np.nan)
 
 
 
 
330
  )
331
 
332
- # FEATURES
333
- df["macd_pct"] = df["macd"] / df["close"] * 100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
- df["macd_signal_pct"] = (
336
- df["macd_signal"] / df["close"] * 100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  )
338
 
339
- df["macd_hist_pct"] = (
340
- df["macd_hist"] / df["close"] * 100
 
341
  )
342
 
343
- df["supertrend_distance"] = (
344
- (df["close"] - df["supertrend"])
345
- / df["close"] * 100
 
 
 
 
 
 
 
 
 
 
 
346
  )
347
 
348
- df["vwap_distance"] = (
349
- (df["close"] - df["vwap"])
350
- / df["close"] * 100
 
 
 
 
 
 
351
  )
352
 
353
- df["atr_pct"] = df["atr"] / df["close"] * 100
 
 
354
 
355
- df["return_1"] = df["close"].pct_change(1) * 100
356
- df["return_3"] = df["close"].pct_change(3) * 100
357
- df["return_6"] = df["close"].pct_change(6) * 100
 
 
 
358
 
359
- df["volatility_14"] = (
360
- df["return_1"].rolling(14).std()
 
 
 
 
361
  )
362
 
363
- volume_avg = df["volume"].rolling(20).mean()
 
 
364
 
365
- df["volume_ratio"] = (
366
- df["volume"] /
367
- volume_avg.replace(0, np.nan)
368
  )
369
 
370
- df["body_pct"] = (
371
- (df["close"] - df["open"])
372
- / df["open"] * 100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  )
374
 
375
- df["high_low_pct"] = (
376
- (df["high"] - df["low"])
377
- / df["close"] * 100
 
378
  )
379
 
380
- return df
381
 
382
 
383
- # =========================================================
384
  # DATASET
385
- # =========================================================
386
 
387
  def prepare_dataset(raw, horizon):
388
 
389
- data = calculate_indicators(raw)
390
 
391
- future_close = data["close"].shift(-horizon)
392
 
393
- data["target"] = (
394
- future_close > data["close"]
395
- ).astype(int)
396
 
397
- # Gelecek hedefi bulunmayan son mumları çıkar.
398
- data = data.iloc[:-horizon].copy()
 
 
 
399
 
400
- data = data.replace(
401
  [np.inf, -np.inf],
402
  np.nan
403
  )
404
 
405
- data = data.dropna(
406
- subset=FEATURES + ["target", "atr"]
407
- )
 
 
 
 
 
 
408
 
409
- if len(data) < 150:
 
 
410
  raise ValueError(
411
- f"Temiz veri sayısı {len(data)}. "
412
- "En az 150 örnek gerekli. Mum sayısını artırın "
413
- "veya daha büyük timeframe deneyin."
414
  )
415
 
416
- return data
417
 
 
418
 
419
- # =========================================================
420
- # MODEL FACTORY
421
- # =========================================================
 
422
 
423
- def create_models(params):
424
 
425
- extra = ExtraTreesClassifier(
426
- n_estimators=params["extra_trees"],
427
- max_depth=params["extra_depth"],
428
- min_samples_leaf=params["extra_leaf"],
429
- max_features=0.8,
430
- class_weight="balanced",
431
- random_state=42,
432
- n_jobs=-1
433
- )
434
 
435
- hist = HistGradientBoostingClassifier(
436
- max_iter=params["hgb_iter"],
437
- learning_rate=params["hgb_lr"],
438
- max_leaf_nodes=params["hgb_leaves"],
439
- l2_regularization=params["hgb_l2"],
440
- random_state=42
441
- )
442
 
443
- logistic = Pipeline([
 
 
444
  ("scaler", StandardScaler()),
445
- ("model", LogisticRegression(
446
- C=params["logistic_c"],
447
- max_iter=1500,
448
- class_weight="balanced",
 
 
449
  random_state=42
450
  ))
451
  ])
452
 
453
- return [extra, hist, logistic]
454
-
455
 
456
- def ensemble_predict(X_train, y_train, X_test, params):
457
 
458
- probabilities = []
459
 
460
- for model in create_models(params):
461
 
462
- model.fit(X_train, y_train)
463
-
464
- probability = model.predict_proba(X_test)[:, 1]
465
 
466
- probabilities.append(probability)
467
 
468
- return np.mean(probabilities, axis=0)
469
 
470
 
471
- # =========================================================
472
- # BACKTEST
473
- # =========================================================
474
 
475
- def run_backtest(
476
  raw,
477
- test,
 
478
  probabilities,
479
- horizon,
480
  threshold,
481
- atr_multiplier,
482
- fee_pct
 
 
483
  ):
484
 
485
  trades = []
486
 
487
- fee_rate = fee_pct / 100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
 
489
- next_allowed = -1
490
 
491
- raw_positions = raw.index.get_indexer(test.index)
492
 
493
- for i, (timestamp, row) in enumerate(test.iterrows()):
 
494
 
495
- position = raw_positions[i]
 
496
 
497
- if position < 0 or position < next_allowed:
498
  continue
499
 
500
- probability = float(probabilities[i])
 
501
 
502
  if probability >= threshold:
503
- side = "LONG"
 
504
 
505
  elif probability <= 1 - threshold:
506
- side = "SHORT"
 
507
 
508
  else:
 
509
  continue
510
 
511
- entry = float(row["close"])
512
- atr = float(row["atr"])
 
 
 
513
 
514
- if not np.isfinite(atr) or atr <= 0:
515
  continue
516
 
517
- risk = atr * atr_multiplier
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
 
519
- if side == "LONG":
520
- stop = entry - risk
521
- target = entry + 2 * risk
522
  else:
523
- stop = entry + risk
524
- target = entry - 2 * risk
525
 
526
- future = raw.iloc[
527
- position + 1:
528
- position + horizon + 1
529
- ]
530
 
531
- if len(future) < horizon:
532
- continue
 
 
 
 
533
 
534
- exit_price = float(future["close"].iloc[-1])
535
- exit_reason = "HORIZON"
536
 
537
- for candle in future.itertuples():
538
 
539
- high = float(candle.high)
540
- low = float(candle.low)
541
 
542
- if side == "LONG":
543
 
544
- if low <= stop:
545
- exit_price = stop
546
- exit_reason = "STOP"
547
- break
548
 
549
- if high >= target:
550
- exit_price = target
551
- exit_reason = "TARGET"
552
- break
553
 
554
  else:
555
 
556
- if high >= stop:
557
- exit_price = stop
558
- exit_reason = "STOP"
559
- break
 
 
 
 
 
560
 
561
- if low <= target:
562
- exit_price = target
563
- exit_reason = "TARGET"
564
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
 
566
- if side == "LONG":
567
- gross = (exit_price - entry) / entry
568
  else:
569
- gross = (entry - exit_price) / entry
570
 
571
- # Giriş ve çıkış komisyonu
572
- net = gross - 2 * fee_rate
 
 
 
573
 
574
  trades.append({
575
- "time": timestamp,
576
- "side": side,
577
- "entry": entry,
578
  "exit": exit_price,
579
- "probability": probability,
580
- "net_pct": net * 100,
581
- "result": "WIN" if net > 0 else "LOSS",
582
- "exit_reason": exit_reason
583
  })
584
 
585
- next_allowed = position + horizon
 
 
 
 
 
 
 
586
 
587
  if not trades:
588
 
589
  return {
590
- "count": 0,
591
- "winrate": 0,
592
- "profit_factor": 0,
593
- "total_return": 0,
594
- "max_drawdown": 0,
595
- "trades": []
596
  }
597
 
598
- trade_df = pd.DataFrame(trades)
 
 
599
 
600
- wins = trade_df.loc[
601
- trade_df["net_pct"] > 0,
602
- "net_pct"
603
- ]
604
 
605
- losses = trade_df.loc[
606
- trade_df["net_pct"] < 0,
607
- "net_pct"
608
- ]
609
 
610
- gross_profit = wins.sum()
611
- gross_loss = abs(losses.sum())
612
 
613
- if gross_loss > 0:
614
- pf = gross_profit / gross_loss
615
- elif gross_profit > 0:
616
- pf = float("inf")
617
- else:
618
- pf = 0
 
619
 
620
- winrate = len(wins) / len(trade_df) * 100
621
 
622
- returns = trade_df["net_pct"].values / 100
623
 
624
  equity = np.cumprod(1 + returns)
625
- peak = np.maximum.accumulate(equity)
626
 
627
- drawdown = (equity - peak) / peak * 100
 
 
 
 
628
 
629
- total_return = (equity[-1] - 1) * 100
 
 
 
630
 
631
  return {
632
- "count": len(trade_df),
633
- "winrate": winrate,
634
- "profit_factor": pf,
635
- "total_return": total_return,
636
- "max_drawdown": abs(float(drawdown.min())),
637
- "trades": trades
 
 
 
 
 
 
 
 
 
638
  }
639
 
640
 
641
- # =========================================================
642
- # OPTUNA
643
- # =========================================================
644
 
645
- def optimize_model(
646
  X,
647
  y,
 
648
  raw,
649
  horizon,
650
- trials,
651
- fee_pct
 
 
 
652
  ):
653
 
654
- if len(X) < 200:
655
- raise ValueError(
656
- "Optuna için yeterli geliştirme verisi yok. "
657
- "Mum sayısını artırın."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  )
659
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
  splitter = TimeSeriesSplit(
661
  n_splits=3,
662
  gap=horizon
663
  )
664
 
665
- splits = list(splitter.split(X))
666
-
667
  def objective(trial):
668
 
669
- params = {
670
- "extra_trees": trial.suggest_int(
671
- "extra_trees", 100, 300
672
- ),
673
- "extra_depth": trial.suggest_int(
674
- "extra_depth", 3, 12
675
- ),
676
- "extra_leaf": trial.suggest_int(
677
- "extra_leaf", 2, 10
678
- ),
679
- "hgb_iter": trial.suggest_int(
680
- "hgb_iter", 80, 200
681
- ),
682
- "hgb_lr": trial.suggest_float(
683
- "hgb_lr", 0.01, 0.15, log=True
684
- ),
685
- "hgb_leaves": trial.suggest_int(
686
- "hgb_leaves", 7, 20
687
- ),
688
- "hgb_l2": trial.suggest_float(
689
- "hgb_l2", 0.1, 10, log=True
690
- ),
691
- "logistic_c": trial.suggest_float(
692
- "logistic_c", 0.01, 3, log=True
693
- )
694
- }
695
 
696
  threshold = trial.suggest_float(
697
- "threshold", 0.53, 0.75
 
 
698
  )
699
 
700
- atr_multiplier = trial.suggest_float(
701
- "atr_multiplier", 1.0, 2.5
 
 
702
  )
703
 
704
  fold_scores = []
705
 
706
- for train_idx, val_idx in splits:
707
-
708
- X_train = X.iloc[train_idx]
709
- X_val = X.iloc[val_idx]
710
 
711
- y_train = y.iloc[train_idx]
712
 
713
- if y_train.nunique() < 2:
714
- continue
715
-
716
- probs = ensemble_predict(
717
- X_train,
718
- y_train,
719
- X_val,
720
- params
721
  )
722
 
723
- val_data = pd.DataFrame(
724
- index=X_val.index
 
725
  )
726
 
727
- val_data["close"] = raw.loc[
728
- X_val.index, "close"
729
- ]
730
-
731
- val_data["atr"] = raw.loc[
732
- X_val.index, "atr"
733
- ]
734
-
735
- result = run_backtest(
736
- raw=raw,
737
- test=val_data,
738
- probabilities=probs,
739
- horizon=horizon,
740
- threshold=threshold,
741
- atr_multiplier=atr_multiplier,
742
- fee_pct=fee_pct
743
  )
744
 
745
- if result["count"] < 3:
746
- fold_scores.append(-10.0)
747
- continue
748
-
749
- pf = result["profit_factor"]
750
-
751
- if np.isinf(pf):
752
- pf = 3.0
753
 
 
754
  score = (
755
- result["total_return"]
756
- + min(pf, 3.0) * 3
757
- - result["max_drawdown"] * 0.5
 
758
  )
759
 
760
- fold_scores.append(score)
 
761
 
762
- if not fold_scores:
763
- return -100.0
764
 
765
  return float(np.mean(fold_scores))
766
 
767
  study = optuna.create_study(
768
  direction="maximize",
769
- sampler=optuna.samplers.TPESampler(seed=42)
770
- )
771
-
772
- optuna.logging.set_verbosity(
773
- optuna.logging.WARNING
774
  )
775
 
776
  study.optimize(
@@ -779,572 +995,587 @@ def optimize_model(
779
  show_progress_bar=False
780
  )
781
 
782
- best = study.best_params
783
-
784
- params = {
785
- "extra_trees": best["extra_trees"],
786
- "extra_depth": best["extra_depth"],
787
- "extra_leaf": best["extra_leaf"],
788
- "hgb_iter": best["hgb_iter"],
789
- "hgb_lr": best["hgb_lr"],
790
- "hgb_leaves": best["hgb_leaves"],
791
- "hgb_l2": best["hgb_l2"],
792
- "logistic_c": best["logistic_c"]
793
- }
794
 
795
- return (
796
- params,
797
- best["threshold"],
798
- best["atr_multiplier"],
799
- study.best_value,
800
- len(study.trials)
801
- )
802
 
 
 
 
803
 
804
- # =========================================================
805
- # WALK-FORWARD VALIDATION
806
- # =========================================================
 
 
 
 
 
 
 
 
 
807
 
808
- def walk_forward_validation(X, y, params, horizon):
809
 
810
- splitter = TimeSeriesSplit(
811
- n_splits=3,
812
- gap=horizon
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
813
  )
814
 
815
- scores = []
 
 
 
816
 
817
- for train_idx, test_idx in splitter.split(X):
818
 
819
- X_train = X.iloc[train_idx]
820
- X_test = X.iloc[test_idx]
 
821
 
822
- y_train = y.iloc[train_idx]
823
- y_test = y.iloc[test_idx]
 
 
 
 
 
 
 
 
 
824
 
825
- if y_train.nunique() < 2:
826
- continue
 
 
 
 
827
 
828
- probs = ensemble_predict(
829
- X_train,
830
- y_train,
831
- X_test,
832
- params
833
  )
 
834
 
835
- predictions = (
836
- probs >= 0.5
837
- ).astype(int)
 
838
 
839
- scores.append(
840
- balanced_accuracy_score(
841
- y_test,
842
- predictions
843
- )
844
- )
845
 
846
- return float(np.mean(scores)) if scores else 0.5
847
 
 
 
 
848
 
849
- # =========================================================
850
- # OVERFIT RISK INDEX
851
- # =========================================================
 
852
 
853
- def calculate_overfit_index(walk_forward_score, holdout_score):
854
 
855
- # Pozitif fark, geliştirme döneminin test döneminden
856
- # daha iyi görünmesi anlamına gelir.
857
- gap = max(
858
- 0.0,
859
- walk_forward_score - holdout_score
 
 
 
 
860
  )
861
 
862
- # 20 yüzde puanlık farkta 100 puan risk.
863
- risk = min(100.0, gap * 500)
 
 
 
 
 
 
 
 
 
 
 
864
 
865
  if risk < 15:
 
866
  label = "DÜŞÜK"
 
867
  elif risk < 35:
 
868
  label = "ORTA"
 
869
  elif risk < 60:
 
870
  label = "YÜKSEK"
 
871
  else:
 
872
  label = "ÇOK YÜKSEK"
873
 
874
- return risk, label, gap
875
 
876
 
877
- # =========================================================
878
- # MAIN ANALYSIS
879
- # =========================================================
880
 
881
- def analyze(
882
- timeframe,
883
- candles,
 
884
  horizon,
885
- trials,
886
- fee_pct
887
  ):
888
 
889
- try:
890
 
891
- candles = int(candles)
892
- horizon = int(horizon)
893
- trials = int(trials)
894
- fee_pct = float(fee_pct)
895
 
896
- if candles < 800:
897
- return "En az 800 mum seç."
898
 
899
- if horizon < 1 or horizon > 48:
900
- return "Horizon 1-48 arasında olmalı."
901
 
902
- if trials < 3 or trials > 100:
903
- return "Trial sayısı 3-100 arasında olmalı."
 
904
 
905
- if fee_pct < 0 or fee_pct > 1:
906
- return "Komisyon değeri geçersiz."
 
 
 
907
 
908
- # FETCH
909
- raw = fetch_data(
910
- timeframe=timeframe,
911
- candles=candles
912
- )
913
 
914
- indicators = calculate_indicators(raw)
915
 
916
- data = prepare_dataset(
917
- raw,
918
- horizon
919
- )
920
 
921
- X = data[FEATURES]
922
- y = data["target"].astype(int)
923
 
924
- if y.nunique() < 2:
925
- return "Veri setinde iki hedef sınıfı bulunmalı."
926
 
927
- # HOLDOUT SPLIT
928
- split = int(len(data) * 0.80)
929
 
930
- # Horizon kadar purge: train etiketleri
931
- # holdout dönemine taşmasın.
932
- dev_end = split - horizon
933
 
934
- if dev_end < 200:
935
- return (
936
- "Geliştirme verisi yetersiz. "
937
- "Mum sayısını artırın veya horizon'u azaltın."
938
- )
939
 
940
- X_dev = X.iloc[:dev_end]
941
- y_dev = y.iloc[:dev_end]
942
 
943
- X_test = X.iloc[split:]
944
- y_test = y.iloc[split:]
945
 
946
- if y_dev.nunique() < 2 or y_test.nunique() < 2:
947
- return (
948
- "Eğitim veya hold-out döneminde iki sınıf da "
949
- "bulunmalı. Daha fazla mum deneyin."
950
- )
951
 
952
- # OPTUNA
953
- params, threshold, atr_mult, best_score, n_trials = (
954
- optimize_model(
955
- X=X_dev,
956
- y=y_dev,
957
- raw=indicators,
958
- horizon=horizon,
959
- trials=trials,
960
- fee_pct=fee_pct
961
- )
962
- )
963
 
964
- # WALK FORWARD
965
- wf_score = walk_forward_validation(
966
- X_dev,
967
- y_dev,
968
- params,
969
- horizon
970
- )
971
 
972
- # HOLDOUT PREDICTION
973
- test_probs = ensemble_predict(
974
- X_dev,
975
- y_dev,
976
- X_test,
977
- params
978
- )
979
 
980
- test_predictions = (
981
- test_probs >= 0.5
982
- ).astype(int)
 
 
 
 
 
 
983
 
984
- accuracy = accuracy_score(
985
- y_test,
986
- test_predictions
987
- )
988
 
989
- balanced = balanced_accuracy_score(
990
- y_test,
991
- test_predictions
992
- )
993
 
994
- try:
995
- auc = roc_auc_score(
996
- y_test,
997
- test_probs
998
- )
999
- except Exception:
1000
- auc = 0.5
 
 
1001
 
1002
- # HOLDOUT BACKTEST
1003
- test_data = pd.DataFrame(
1004
- index=X_test.index
1005
  )
1006
 
1007
- test_data["close"] = indicators.loc[
1008
- X_test.index, "close"
1009
- ]
1010
 
1011
- test_data["atr"] = indicators.loc[
1012
- X_test.index, "atr"
1013
- ]
1014
 
1015
- backtest = run_backtest(
1016
- raw=indicators,
1017
- test=test_data,
1018
- probabilities=test_probs,
1019
- horizon=horizon,
1020
- threshold=threshold,
1021
- atr_multiplier=atr_mult,
1022
- fee_pct=fee_pct
1023
  )
1024
 
1025
- # OVERFIT INDEX
1026
- overfit_score, overfit_label, overfit_gap = (
1027
- calculate_overfit_index(
1028
- wf_score,
1029
- balanced
1030
- )
 
1031
  )
1032
 
1033
- # FINAL MODEL
1034
- # Holdout evaluation tamamlandıktan sonra
1035
- # güncel sinyal için tüm bilinen etiketler kullanılır.
1036
- final_models = create_models(params)
1037
 
1038
- latest_indicators = indicators.dropna(
1039
- subset=FEATURES + ["atr"]
1040
- )
1041
 
1042
- latest = latest_indicators.iloc[-1]
1043
 
1044
- latest_X = pd.DataFrame(
1045
- [latest[FEATURES].values],
1046
- columns=FEATURES
1047
- )
1048
 
1049
- final_probabilities = []
 
 
1050
 
1051
- for model in final_models:
 
 
1052
 
1053
- model.fit(X, y)
 
 
1054
 
1055
- final_probabilities.append(
1056
- model.predict_proba(latest_X)[:, 1][0]
 
1057
  )
1058
 
1059
- p_up = float(np.mean(final_probabilities))
1060
- p_down = 1 - p_up
1061
-
1062
- if p_up >= threshold:
1063
- signal = "LONG"
1064
- elif p_down >= threshold:
1065
- signal = "SHORT"
1066
- else:
1067
- signal = "NÖTR"
1068
 
1069
- entry = float(latest["close"])
1070
- atr = float(latest["atr"])
 
 
 
 
 
 
 
 
1071
 
1072
- if signal == "LONG":
1073
- stop = entry - atr * atr_mult
1074
- target = entry + 2 * atr * atr_mult
1075
 
1076
- elif signal == "SHORT":
1077
- stop = entry + atr * atr_mult
1078
- target = entry - 2 * atr * atr_mult
1079
 
1080
- else:
1081
- stop = None
1082
- target = None
1083
 
1084
- # INDICATORS
1085
- rsi = float(latest["rsi"])
 
1086
 
1087
- macd_state = (
1088
- "BULLISH"
1089
- if latest["macd"] > latest["macd_signal"]
1090
- else "BEARISH"
 
 
 
 
 
 
 
1091
  )
1092
 
1093
- supertrend_state = (
1094
- "BULLISH"
1095
- if latest["supertrend_direction"] == 1
1096
- else "BEARISH"
 
 
 
 
 
 
 
 
 
 
 
 
 
1097
  )
1098
 
1099
- vwap_state = (
1100
- "ÜSTÜNDE"
1101
- if latest["vwap_distance"] > 0
1102
- else "ALTINDA"
 
 
 
 
 
1103
  )
1104
 
1105
- # 4 INDICATOR CONFLUENCE
1106
- votes = [
1107
- 1 if latest["supertrend_direction"] == 1 else -1,
1108
- 1 if latest["macd"] > latest["macd_signal"] else -1,
1109
- 1 if rsi >= 50 else -1,
1110
- 1 if latest["vwap_distance"] > 0 else -1
1111
- ]
1112
 
1113
- bullish_votes = sum(v == 1 for v in votes)
1114
- bearish_votes = 4 - bullish_votes
1115
 
1116
- if bullish_votes >= 3:
1117
- confluence = "GÜÇLÜ BOĞA UYUMU"
1118
- elif bearish_votes >= 3:
1119
- confluence = "GÜÇLÜ AYI UYUMU"
1120
- else:
1121
- confluence = "KARARSIZ / KARIŞIK"
1122
 
1123
- pf = backtest["profit_factor"]
1124
 
1125
- pf_text = (
1126
- "∞" if np.isinf(pf)
1127
- else f"{pf:.2f}"
 
 
 
1128
  )
1129
 
1130
- stop_text = (
1131
- f"{stop:,.2f}" if stop is not None else "—"
1132
- )
1133
 
1134
- target_text = (
1135
- f"{target:,.2f}" if target is not None else "—"
 
 
1136
  )
1137
 
1138
- trade_table = "İşlem oluşmadı."
 
1139
 
1140
- if backtest["trades"]:
1141
 
1142
- trade_df = pd.DataFrame(
1143
- backtest["trades"]
1144
- )
1145
 
1146
- trade_table = trade_df.tail(15).to_markdown(
1147
- index=False
1148
- )
 
 
 
 
 
 
1149
 
1150
- output = f"""
1151
- # OKX BTC/USDT AI ANALİZİ
1152
 
1153
- ## Güncel sinyal: {signal}
1154
 
1155
  | Metrik | Sonuç |
1156
  |---|---:|
1157
- | OKX BTC fiyatı | {entry:,.2f} USDT |
1158
- | LONG olasılığı | {p_up * 100:.2f}% |
1159
- | SHORT olasılığı | {p_down * 100:.2f}% |
1160
- | Sinyal eşiği | {threshold:.3f} |
1161
- | Zaman dilimi | {timeframe} |
1162
- | Tahmin horizon | {horizon} mum |
1163
-
1164
- ## Dört indikatör
1165
-
1166
- | İndikatör | Durum |
1167
- |---|---|
1168
- | RSI 14 | {rsi:.2f} |
1169
- | MACD | {macd_state} |
1170
- | Supertrend 10,3 | {supertrend_state} |
1171
- | VWAP | {vwap_state} |
1172
- | İndikatör uyumu | {confluence} |
1173
- | VWAP uzaklığı | {latest['vwap_distance']:.3f}% |
1174
-
1175
- ## İşlem seviyeleri
1176
-
1177
- | Seviye | Fiyat |
1178
- |---|---:|
1179
- | Giriş | {entry:,.2f} |
1180
- | Stop Loss | {stop_text} |
1181
- | 2R hedef | {target_text} |
1182
- | ATR çarpanı | {atr_mult:.2f} |
1183
 
1184
- ## Optuna
1185
 
1186
  | Metrik | Sonuç |
1187
  |---|---:|
1188
- | Trial sayısı | {n_trials} |
1189
- | En iyi objective | {best_score:.4f} |
1190
- | Sinyal eşiği | {threshold:.3f} |
1191
- | ATR çarpanı | {atr_mult:.2f} |
 
 
1192
 
1193
- ## Bağımsız hold-out test
1194
 
1195
  | Metrik | Sonuç |
1196
  |---|---:|
1197
- | Accuracy | {accuracy * 100:.2f}% |
1198
- | Balanced Accuracy | {balanced * 100:.2f}% |
1199
- | AUC | {auc:.4f} |
1200
- | Walk-forward Balanced Accuracy | {wf_score * 100:.2f}% |
 
 
 
 
1201
 
1202
- ## OVERFIT RISK INDEX
1203
 
1204
- # {overfit_score:.1f} / 100 — {overfit_label}
1205
 
1206
- | Ölçüm | Sonuç |
1207
- |---|---:|
1208
- | Walk-forward başarı | {wf_score * 100:.2f}% |
1209
- | Hold-out başarı | {balanced * 100:.2f}% |
1210
- | Başarı farkı | {overfit_gap * 100:.2f} yüzde puan |
1211
 
1212
- Risk yorumu:
1213
- - 0–14: Düşük gözlenen genelleme farkı
1214
- - 15–34: Orta
1215
- - 35–59: Yüksek
1216
- - 60–100: Çok yüksek
1217
 
1218
- ## Hold-out backtest
1219
 
1220
- | Metrik | Sonuç |
1221
  |---|---:|
1222
- | İşlem sayısı | {backtest['count']} |
1223
- | Win Rate | {backtest['winrate']:.2f}% |
1224
- | Profit Factor | {pf_text} |
1225
- | Bileşik net getiri | {backtest['total_return']:.2f}% |
1226
- | Maksimum drawdown | {backtest['max_drawdown']:.2f}% |
1227
- | Komisyon | %{fee_pct:.3f} / taraf |
1228
 
1229
- ## Son backtest işlemleri
1230
 
1231
- {trade_table}
 
 
 
 
1232
 
1233
- ## Optuna model parametreleri
 
 
 
 
 
 
 
1234
 
1235
- ```text
1236
- {params}
1237
- ```
1238
 
1239
  ---
1240
 
1241
- **Overfit uyarısı:** Bu risk endeksi kesin bir overfit testi değildir. Walk-forward ve hold-out Balanced Accuracy arasındaki farktan türetilen bir göstergedir. Küçük fark, modelin overfit olmadığını kanıtlamaz.
1242
 
1243
- **Backtest uyarısı:** Mum içindeki fiyat sırası bilinmediği için aynı mumda stop ve hedef görülürse stop önce varsayılır. Funding, spread, slippage ve gerçek emir gerçekleşmesi dahil değildir. Sonuçlar canlı kazanç garantisi değildir.
 
 
 
 
 
 
 
1244
  """
1245
 
1246
- return output
1247
 
1248
  except Exception as e:
1249
 
1250
  return (
1251
- "# ANALİZ HATASI\n\n"
1252
  f"`{type(e).__name__}: {str(e)}`\n\n"
1253
- "OKX bağlantısını, mum sayısını ve timeframe ayarını kontrol et."
1254
  )
1255
 
1256
 
1257
- # =========================================================
1258
  # GRADIO UI
1259
- # =========================================================
1260
 
1261
  with gr.Blocks(
1262
- title="OKX BTC AI Optuna Overfit Monitor"
1263
- ) as app:
 
1264
 
1265
  gr.Markdown(
1266
  """
1267
- # OKX BTC AI — OPTUNA + BACKTEST + OVERFIT MONITOR
1268
 
1269
- OKX perpetual swap verisiyle çalışan ML ensemble.
1270
 
1271
- **Supertrend + RSI + MACD + VWAP + Optuna + Walk-forward**
 
1272
  """
1273
  )
1274
 
1275
  with gr.Row():
1276
 
1277
- timeframe = gr.Dropdown(
1278
- choices=[
1279
- "5m",
1280
- "15m",
1281
- "30m",
1282
- "1h",
1283
- "4h",
1284
- "1d"
1285
- ],
1286
- value="1h",
1287
- label="Timeframe"
1288
- )
1289
-
1290
- candles = gr.Dropdown(
1291
- choices=[
1292
- 1000,
1293
- 1500,
1294
- 2000,
1295
- 3000,
1296
- 4000,
1297
- 5000
1298
- ],
1299
- value=2000,
1300
- label="OKX mum sayısı"
1301
  )
1302
 
1303
- with gr.Row():
1304
-
1305
  horizon = gr.Slider(
1306
- minimum=1,
1307
  maximum=24,
1308
  value=6,
1309
  step=1,
1310
- label="Tahmin horizon (mum)"
1311
  )
1312
 
 
 
1313
  trials = gr.Slider(
1314
- minimum=3,
1315
- maximum=100,
1316
- value=20,
1317
- step=1,
1318
- label="Optuna trial sayısı"
1319
  )
1320
 
1321
- fee = gr.Slider(
1322
  minimum=0,
1323
  maximum=0.20,
1324
  value=0.05,
1325
  step=0.01,
1326
- label="Komisyon (%) / taraf"
 
 
 
 
 
 
 
 
1327
  )
1328
 
1329
  run_button = gr.Button(
1330
- "OKX AI ANALİZ + OPTUNA BACKTEST",
1331
  variant="primary"
1332
  )
1333
 
1334
  output = gr.Markdown()
1335
 
1336
  run_button.click(
1337
- fn=analyze,
1338
  inputs=[
1339
- timeframe,
1340
- candles,
1341
  horizon,
1342
  trials,
1343
- fee
 
1344
  ],
1345
  outputs=output
1346
  )
1347
 
1348
 
1349
  if __name__ == "__main__":
1350
- app.launch()
 
 
 
 
5
  import time
6
  import numpy as np
7
  import pandas as pd
 
8
  import ccxt
9
+ import gradio as gr
10
  import optuna
11
 
 
 
 
 
 
12
  from sklearn.pipeline import Pipeline
13
  from sklearn.preprocessing import StandardScaler
14
+ from sklearn.linear_model import LogisticRegression
15
  from sklearn.metrics import (
16
  accuracy_score,
17
  balanced_accuracy_score,
18
+ brier_score_loss
19
  )
20
+ from sklearn.model_selection import TimeSeriesSplit
21
 
22
 
23
+ # ============================================================
24
  # CONFIG
25
+ # ============================================================
26
 
27
  SYMBOL = "BTC/USDT:USDT"
28
+ TIMEFRAME = "1h"
29
+ TIMEFRAME_MS = 60 * 60 * 1000
30
+
31
+ FEATURE_COLUMNS = [
32
+ "ret_1", "ret_2", "ret_3", "ret_6",
33
+ "ret_12", "ret_24", "ret_48", "ret_96",
34
+
35
+ "rv_12", "rv_24", "rv_48", "rv_96",
36
+ "vol_ratio",
37
+
38
+ "zscore_24", "zscore_48",
39
+
40
+ "range_pct", "body_pct",
41
+ "upper_wick", "lower_wick",
42
+ "close_position",
43
+
44
+ "parkinson_24",
45
 
46
+ "skew_48", "kurt_48",
47
+ "autocorr_48",
48
+
49
+ "hurst_64",
50
+ "entropy_48",
51
+
52
+ "kalman_gap",
53
+ "kalman_trend_6",
54
+ "kalman_innovation",
55
+
56
+ "volume_change",
57
+ "volume_zscore",
58
+ "volume_price_corr"
 
 
 
59
  ]
60
 
61
 
62
+ # ============================================================
63
+ # OKX CONNECTION
64
+ # ============================================================
65
 
66
+ def create_exchange():
67
 
68
  exchange = ccxt.okx({
69
  "enableRateLimit": True,
 
73
  }
74
  })
75
 
76
+ return exchange
77
+
78
+
79
+ def fetch_data(candle_count):
80
+
81
+ exchange = create_exchange()
82
+
83
  exchange.load_markets()
84
 
85
  if SYMBOL not in exchange.markets:
86
  raise ValueError(
87
+ f"OKX piyasasında {SYMBOL} bulunamadı."
88
  )
89
 
90
+ now = exchange.milliseconds()
91
 
92
+ since = now - (candle_count + 20) * TIMEFRAME_MS
 
 
 
93
 
94
+ all_candles = []
95
+ cursor = since
 
96
 
97
+ max_requests = int(candle_count / 100) + 15
98
 
99
+ for _ in range(max_requests):
100
 
101
  batch = exchange.fetch_ohlcv(
102
  SYMBOL,
103
+ timeframe=TIMEFRAME,
104
+ since=cursor,
105
  limit=100
106
  )
107
 
108
  if not batch:
109
  break
110
 
111
+ all_candles.extend(batch)
 
 
 
112
 
113
+ last_timestamp = batch[-1][0]
114
 
115
+ if last_timestamp <= cursor:
 
 
 
 
116
  break
117
 
118
+ cursor = last_timestamp + TIMEFRAME_MS
119
 
120
+ if len(all_candles) >= candle_count + 100:
121
  break
122
 
123
+ if last_timestamp >= now - TIMEFRAME_MS:
124
  break
125
 
126
  time.sleep(exchange.rateLimit / 1000)
127
 
128
+ if not all_candles:
129
  raise ValueError("OKX veri döndürmedi.")
130
 
131
  df = pd.DataFrame(
132
+ all_candles,
133
  columns=[
134
  "timestamp",
135
  "open",
 
140
  ]
141
  )
142
 
143
+ df = df.drop_duplicates(
144
+ subset=["timestamp"]
145
+ )
146
+
147
  df = df.sort_values("timestamp")
 
148
 
149
+ # Henüz tamamlanmamış mumları çıkar.
150
+ now = exchange.milliseconds()
151
+
152
+ df = df[
153
+ df["timestamp"] + TIMEFRAME_MS <= now
154
+ ]
155
 
156
+ df = df.tail(candle_count).reset_index(drop=True)
157
 
158
+ for col in ["open", "high", "low", "close", "volume"]:
 
 
 
 
 
 
159
  df[col] = pd.to_numeric(
160
  df[col],
161
  errors="coerce"
162
  )
163
 
164
+ df = df.replace(
165
+ [np.inf, -np.inf],
166
+ np.nan
167
+ )
168
 
169
+ df = df.dropna().reset_index(drop=True)
 
 
170
 
171
+ df = df[
172
+ (df["close"] > 0) &
173
+ (df["high"] >= df["low"]) &
174
+ (df["volume"] >= 0)
175
+ ].reset_index(drop=True)
176
 
177
+ if len(df) < 500:
178
  raise ValueError(
179
+ f"Yeterli OKX verisi yok. Gelen mum: {len(df)}"
 
180
  )
181
 
182
  return df
183
 
184
 
185
+ # ============================================================
186
+ # MATHEMATICAL FUNCTIONS
187
+ # ============================================================
188
 
189
+ def safe_div(a, b):
190
 
191
+ return a / (b.replace(0, np.nan) if isinstance(b, pd.Series) else b)
192
 
 
 
193
 
194
+ def shannon_entropy(values):
 
 
 
 
195
 
196
+ x = np.asarray(values)
 
 
 
 
197
 
198
+ signs = np.sign(x)
 
199
 
200
+ counts = np.bincount(
201
+ (signs + 1).astype(int),
202
+ minlength=3
203
+ )
204
 
205
+ probabilities = counts / counts.sum()
 
 
 
 
206
 
207
+ probabilities = probabilities[
208
+ probabilities > 0
209
+ ]
 
 
210
 
211
+ entropy = -np.sum(
212
+ probabilities * np.log(probabilities)
213
+ )
214
 
215
+ return entropy / np.log(3)
216
 
 
 
 
 
 
217
 
218
+ def hurst_exponent(values):
 
 
 
219
 
220
+ x = np.asarray(values)
221
 
222
+ if len(x) < 32:
223
+ return np.nan
 
 
224
 
225
+ lags = np.array([1, 2, 4, 8])
 
 
226
 
227
+ tau = []
 
228
 
229
+ for lag in lags:
 
230
 
231
+ differences = x[lag:] - x[:-lag]
 
232
 
233
+ value = np.std(differences)
 
 
 
234
 
235
+ if value <= 0:
236
+ return np.nan
 
 
237
 
238
+ tau.append(value)
239
 
240
+ slope = np.polyfit(
241
+ np.log(lags),
242
+ np.log(tau),
243
+ 1
244
+ )[0]
245
 
246
+ return float(np.clip(slope, 0, 1))
 
 
 
 
 
 
247
 
 
 
 
 
 
 
 
248
 
249
+ def kalman_filter(log_prices):
250
 
251
+ prices = np.asarray(log_prices)
252
 
253
+ n = len(prices)
 
 
 
254
 
255
+ filtered = np.zeros(n)
256
+ innovations = np.zeros(n)
257
 
258
+ q = 0.000001
259
+ r = 0.000025
260
+
261
+ estimate = prices[0]
262
+ variance = 0.001
263
+
264
+ for i in range(n):
265
+
266
+ predicted = estimate
267
+
268
+ predicted_variance = variance + q
269
+
270
+ innovation = prices[i] - predicted
271
+
272
+ total_variance = predicted_variance + r
273
 
274
+ gain = predicted_variance / total_variance
275
+
276
+ estimate = predicted + gain * innovation
277
+
278
+ variance = (1 - gain) * predicted_variance
279
+
280
+ filtered[i] = estimate
281
+
282
+ innovations[i] = (
283
+ innovation / np.sqrt(total_variance)
284
  )
285
 
286
+ return filtered, innovations
 
287
 
 
 
 
 
288
 
289
+ # ============================================================
290
+ # QUANT FEATURE ENGINEERING
291
+ # NO RSI / MACD / EMA / SUPERTREND / VWAP
292
+ # ============================================================
293
 
294
+ def calculate_quant_features(df):
 
 
 
 
295
 
296
+ data = df.copy()
297
+
298
+ log_close = np.log(data["close"])
299
+
300
+ log_open = np.log(data["open"])
301
+ log_high = np.log(data["high"])
302
+ log_low = np.log(data["low"])
303
+
304
+ returns = log_close.diff()
305
+
306
+ # --------------------------------------------------------
307
+ # LOG RETURNS
308
+ # --------------------------------------------------------
309
+
310
+ for period in [1, 2, 3, 6, 12, 24, 48, 96]:
311
+
312
+ data[f"ret_{period}"] = (
313
+ log_close - log_close.shift(period)
314
+ )
315
+
316
+ # --------------------------------------------------------
317
+ # REALIZED VOLATILITY
318
+ # --------------------------------------------------------
319
+
320
+ for window in [12, 24, 48, 96]:
321
 
322
+ data[f"rv_{window}"] = (
323
+ returns.rolling(window).std()
324
+ )
325
+
326
+ data["vol_ratio"] = safe_div(
327
+ data["rv_24"],
328
+ data["rv_96"]
329
  )
330
 
331
+ # --------------------------------------------------------
332
+ # Z-SCORE
333
+ # --------------------------------------------------------
334
+
335
+ for window in [24, 48]:
336
+
337
+ mean = log_close.rolling(window).mean()
338
+
339
+ std = log_close.rolling(window).std()
340
+
341
+ data[f"zscore_{window}"] = safe_div(
342
+ log_close - mean,
343
+ std
344
+ )
345
+
346
+ # --------------------------------------------------------
347
+ # CANDLE DISTRIBUTION MATHEMATICS
348
+ # --------------------------------------------------------
349
+
350
+ data["range_pct"] = log_high - log_low
351
 
352
+ data["body_pct"] = log_close - log_open
353
+
354
+ candle_range = (
355
+ data["high"] - data["low"]
356
+ ).replace(0, np.nan)
357
+
358
+ data["upper_wick"] = (
359
+ data["high"] -
360
+ data[["open", "close"]].max(axis=1)
361
+ ) / candle_range
362
+
363
+ data["lower_wick"] = (
364
+ data[["open", "close"]].min(axis=1) -
365
+ data["low"]
366
+ ) / candle_range
367
+
368
+ data["close_position"] = (
369
+ data["close"] - data["low"]
370
+ ) / candle_range
371
+
372
+ # --------------------------------------------------------
373
+ # PARKINSON VOLATILITY ESTIMATOR
374
+ # --------------------------------------------------------
375
+
376
+ squared_range = (
377
+ np.log(data["high"] / data["low"]) ** 2
378
  )
379
 
380
+ data["parkinson_24"] = np.sqrt(
381
+ squared_range.rolling(24).mean()
382
+ / (4 * np.log(2))
383
  )
384
 
385
+ # --------------------------------------------------------
386
+ # DISTRIBUTION STATISTICS
387
+ # --------------------------------------------------------
388
+
389
+ data["skew_48"] = returns.rolling(48).skew()
390
+
391
+ data["kurt_48"] = returns.rolling(48).kurt()
392
+
393
+ # --------------------------------------------------------
394
+ # AUTOCORRELATION
395
+ # --------------------------------------------------------
396
+
397
+ data["autocorr_48"] = (
398
+ returns.rolling(48).corr(returns.shift(1))
399
  )
400
 
401
+ # --------------------------------------------------------
402
+ # HURST EXPONENT
403
+ # --------------------------------------------------------
404
+
405
+ data["hurst_64"] = log_close.rolling(
406
+ 64
407
+ ).apply(
408
+ hurst_exponent,
409
+ raw=True
410
  )
411
 
412
+ # --------------------------------------------------------
413
+ # SHANNON ENTROPY
414
+ # --------------------------------------------------------
415
 
416
+ data["entropy_48"] = returns.rolling(
417
+ 48
418
+ ).apply(
419
+ shannon_entropy,
420
+ raw=True
421
+ )
422
 
423
+ # --------------------------------------------------------
424
+ # KALMAN FILTER
425
+ # --------------------------------------------------------
426
+
427
+ filtered, innovations = kalman_filter(
428
+ log_close.values
429
  )
430
 
431
+ data["kalman_gap"] = (
432
+ log_close.values - filtered
433
+ )
434
 
435
+ data["kalman_trend_6"] = (
436
+ pd.Series(filtered).diff(6).values
 
437
  )
438
 
439
+ data["kalman_innovation"] = innovations
440
+
441
+ # --------------------------------------------------------
442
+ # VOLUME MATHEMATICS
443
+ # --------------------------------------------------------
444
+
445
+ log_volume = np.log1p(data["volume"])
446
+
447
+ data["volume_change"] = log_volume.diff()
448
+
449
+ volume_mean = log_volume.rolling(48).mean()
450
+
451
+ volume_std = log_volume.rolling(48).std()
452
+
453
+ data["volume_zscore"] = safe_div(
454
+ log_volume - volume_mean,
455
+ volume_std
456
  )
457
 
458
+ data["volume_price_corr"] = (
459
+ returns.rolling(48).corr(
460
+ data["volume_change"]
461
+ )
462
  )
463
 
464
+ return data
465
 
466
 
467
+ # ============================================================
468
  # DATASET
469
+ # ============================================================
470
 
471
  def prepare_dataset(raw, horizon):
472
 
473
+ features = calculate_quant_features(raw)
474
 
475
+ features["raw_i"] = np.arange(len(features))
476
 
477
+ future_close = features["close"].shift(-horizon)
 
 
478
 
479
+ features["target"] = np.where(
480
+ future_close.notna(),
481
+ (future_close > features["close"]).astype(float),
482
+ np.nan
483
+ )
484
 
485
+ features = features.replace(
486
  [np.inf, -np.inf],
487
  np.nan
488
  )
489
 
490
+ live_features = features.dropna(
491
+ subset=FEATURE_COLUMNS
492
+ ).copy()
493
+
494
+ dataset = live_features.dropna(
495
+ subset=["target"]
496
+ ).copy()
497
+
498
+ dataset = dataset.reset_index(drop=True)
499
 
500
+ live_features = live_features.reset_index(drop=True)
501
+
502
+ if len(dataset) < 500:
503
  raise ValueError(
504
+ "Temiz eğitim verisi 500 örneğin altında. "
505
+ f"Mevcut: {len(dataset)}"
 
506
  )
507
 
508
+ X = dataset[FEATURE_COLUMNS].astype(float)
509
 
510
+ y = dataset["target"].astype(int)
511
 
512
+ if y.nunique() < 2:
513
+ raise ValueError(
514
+ "Eğitim verisinde iki sınıf da bulunmalı."
515
+ )
516
 
517
+ return X, y, dataset, live_features
518
 
 
 
 
 
 
 
 
 
 
519
 
520
+ # ============================================================
521
+ # MODEL
522
+ # ============================================================
 
 
 
 
523
 
524
+ def create_model(C):
525
+
526
+ model = Pipeline([
527
  ("scaler", StandardScaler()),
528
+
529
+ ("classifier", LogisticRegression(
530
+ C=C,
531
+ max_iter=2000,
532
+ solver="lbfgs",
533
+ class_weight=None,
534
  random_state=42
535
  ))
536
  ])
537
 
538
+ return model
 
539
 
 
540
 
541
+ def get_probability(model, X):
542
 
543
+ probabilities = model.predict_proba(X)
544
 
545
+ classes = list(
546
+ model.named_steps["classifier"].classes_
547
+ )
548
 
549
+ positive_index = classes.index(1)
550
 
551
+ return probabilities[:, positive_index]
552
 
553
 
554
+ # ============================================================
555
+ # 2R BACKTEST ENGINE
556
+ # ============================================================
557
 
558
+ def simulate_trades(
559
  raw,
560
+ dataset,
561
+ row_indices,
562
  probabilities,
 
563
  threshold,
564
+ stop_multiplier,
565
+ horizon,
566
+ fee_rate,
567
+ slippage_bps
568
  ):
569
 
570
  trades = []
571
 
572
+ if len(row_indices) == 0:
573
+ return trades
574
+
575
+ last_validation_raw_i = int(
576
+ dataset.iloc[row_indices[-1]]["raw_i"]
577
+ )
578
+
579
+ cost = (
580
+ 2 * fee_rate +
581
+ 2 * slippage_bps / 10000
582
+ )
583
+
584
+ for row_position, probability in zip(
585
+ row_indices,
586
+ probabilities
587
+ ):
588
 
589
+ row = dataset.iloc[row_position]
590
 
591
+ raw_i = int(row["raw_i"])
592
 
593
+ # Sinyalden sonraki mumda giriş.
594
+ entry_i = raw_i + 1
595
 
596
+ # Tam horizon boyunca veri bulunmalı.
597
+ exit_limit = entry_i + horizon - 1
598
 
599
+ if exit_limit > last_validation_raw_i:
600
  continue
601
 
602
+ if exit_limit >= len(raw):
603
+ continue
604
 
605
  if probability >= threshold:
606
+
607
+ direction = 1
608
 
609
  elif probability <= 1 - threshold:
610
+
611
+ direction = -1
612
 
613
  else:
614
+
615
  continue
616
 
617
+ entry_price = float(
618
+ raw.iloc[entry_i]["open"]
619
+ )
620
+
621
+ volatility = float(row["rv_24"])
622
 
623
+ if not np.isfinite(volatility) or volatility <= 0:
624
  continue
625
 
626
+ risk_pct = (
627
+ volatility *
628
+ np.sqrt(horizon) *
629
+ stop_multiplier
630
+ )
631
+
632
+ risk_pct = np.clip(
633
+ risk_pct,
634
+ 0.002,
635
+ 0.05
636
+ )
637
+
638
+ risk_distance = entry_price * risk_pct
639
+
640
+ if direction == 1:
641
+
642
+ stop_price = (
643
+ entry_price - risk_distance
644
+ )
645
+
646
+ target_price = (
647
+ entry_price + 2 * risk_distance
648
+ )
649
 
 
 
 
650
  else:
 
 
651
 
652
+ stop_price = (
653
+ entry_price + risk_distance
654
+ )
 
655
 
656
+ target_price = (
657
+ entry_price - 2 * risk_distance
658
+ )
659
+
660
+ exit_price = None
661
+ exit_reason = "TIME"
662
 
663
+ for j in range(entry_i, exit_limit + 1):
 
664
 
665
+ candle = raw.iloc[j]
666
 
667
+ high = float(candle["high"])
668
+ low = float(candle["low"])
669
 
670
+ if direction == 1:
671
 
672
+ hit_stop = low <= stop_price
 
 
 
673
 
674
+ hit_target = high >= target_price
 
 
 
675
 
676
  else:
677
 
678
+ hit_stop = high >= stop_price
679
+
680
+ hit_target = low <= target_price
681
+
682
+ # Aynı mumda stop ve hedef:
683
+ # Muhafazakâr varsayım: STOP önce.
684
+ if hit_stop:
685
+
686
+ exit_price = stop_price
687
 
688
+ exit_reason = "STOP"
689
+
690
+ break
691
+
692
+ if hit_target:
693
+
694
+ exit_price = target_price
695
+
696
+ exit_reason = "TARGET"
697
+
698
+ break
699
+
700
+ if exit_price is None:
701
+
702
+ exit_price = float(
703
+ raw.iloc[exit_limit]["close"]
704
+ )
705
+
706
+ if direction == 1:
707
+
708
+ gross_return = (
709
+ exit_price / entry_price
710
+ ) - 1
711
 
 
 
712
  else:
 
713
 
714
+ gross_return = (
715
+ entry_price / exit_price
716
+ ) - 1
717
+
718
+ net_return = gross_return - cost
719
 
720
  trades.append({
721
+ "direction": "LONG" if direction == 1 else "SHORT",
722
+ "probability": float(probability),
723
+ "entry": entry_price,
724
  "exit": exit_price,
725
+ "reason": exit_reason,
726
+ "gross_return": gross_return,
727
+ "net_return": net_return
 
728
  })
729
 
730
+ return trades
731
+
732
+
733
+ # ============================================================
734
+ # BACKTEST METRICS
735
+ # ============================================================
736
+
737
+ def calculate_metrics(trades):
738
 
739
  if not trades:
740
 
741
  return {
742
+ "trades": 0,
743
+ "win_rate": 0.0,
744
+ "profit_factor": 0.0,
745
+ "net_return": 0.0,
746
+ "max_drawdown": 0.0
 
747
  }
748
 
749
+ returns = np.array([
750
+ t["net_return"] for t in trades
751
+ ])
752
 
753
+ wins = returns[returns > 0]
 
 
 
754
 
755
+ losses = returns[returns < 0]
 
 
 
756
 
757
+ if len(losses):
 
758
 
759
+ profit_factor = (
760
+ wins.sum() / abs(losses.sum())
761
+ )
762
+
763
+ elif len(wins):
764
+
765
+ profit_factor = 5.0
766
 
767
+ else:
768
 
769
+ profit_factor = 0.0
770
 
771
  equity = np.cumprod(1 + returns)
 
772
 
773
+ peak = np.maximum.accumulate(
774
+ np.insert(equity, 0, 1)
775
+ )[1:]
776
+
777
+ drawdowns = 1 - equity / peak
778
 
779
+ max_drawdown = (
780
+ float(np.max(drawdowns))
781
+ if len(drawdowns) else 0.0
782
+ )
783
 
784
  return {
785
+ "trades": len(trades),
786
+
787
+ "win_rate": (
788
+ len(wins) / len(trades)
789
+ ),
790
+
791
+ "profit_factor": float(
792
+ min(profit_factor, 5.0)
793
+ ),
794
+
795
+ "net_return": float(
796
+ equity[-1] - 1
797
+ ),
798
+
799
+ "max_drawdown": max_drawdown
800
  }
801
 
802
 
803
+ # ============================================================
804
+ # WALK-FORWARD
805
+ # ============================================================
806
 
807
+ def walk_forward_validation(
808
  X,
809
  y,
810
+ dataset,
811
  raw,
812
  horizon,
813
+ C,
814
+ threshold,
815
+ stop_multiplier,
816
+ fee_rate,
817
+ slippage_bps
818
  ):
819
 
820
+ splitter = TimeSeriesSplit(
821
+ n_splits=3,
822
+ gap=horizon
823
+ )
824
+
825
+ all_predictions = []
826
+ all_targets = []
827
+ all_trades = []
828
+
829
+ fold_reports = []
830
+
831
+ for fold, (train_idx, val_idx) in enumerate(
832
+ splitter.split(X),
833
+ start=1
834
+ ):
835
+
836
+ model = create_model(C)
837
+
838
+ model.fit(
839
+ X.iloc[train_idx],
840
+ y.iloc[train_idx]
841
  )
842
 
843
+ probabilities = get_probability(
844
+ model,
845
+ X.iloc[val_idx]
846
+ )
847
+
848
+ targets = y.iloc[val_idx].values
849
+
850
+ predictions = (
851
+ probabilities >= 0.5
852
+ ).astype(int)
853
+
854
+ trades = simulate_trades(
855
+ raw,
856
+ dataset,
857
+ val_idx,
858
+ probabilities,
859
+ threshold,
860
+ stop_multiplier,
861
+ horizon,
862
+ fee_rate,
863
+ slippage_bps
864
+ )
865
+
866
+ metrics = calculate_metrics(trades)
867
+
868
+ all_predictions.extend(
869
+ predictions.tolist()
870
+ )
871
+
872
+ all_targets.extend(
873
+ targets.tolist()
874
+ )
875
+
876
+ all_trades.extend(trades)
877
+
878
+ fold_reports.append({
879
+ "fold": fold,
880
+ "balanced_accuracy": balanced_accuracy_score(
881
+ targets,
882
+ predictions
883
+ ),
884
+ **metrics
885
+ })
886
+
887
+ overall = calculate_metrics(all_trades)
888
+
889
+ overall["balanced_accuracy"] = (
890
+ balanced_accuracy_score(
891
+ all_targets,
892
+ all_predictions
893
+ )
894
+ )
895
+
896
+ return overall, fold_reports
897
+
898
+
899
+ # ============================================================
900
+ # OPTUNA
901
+ # ============================================================
902
+
903
+ def optimize_model(
904
+ X,
905
+ y,
906
+ dataset,
907
+ raw,
908
+ horizon,
909
+ fee_rate,
910
+ slippage_bps,
911
+ trials
912
+ ):
913
+
914
  splitter = TimeSeriesSplit(
915
  n_splits=3,
916
  gap=horizon
917
  )
918
 
 
 
919
  def objective(trial):
920
 
921
+ C = trial.suggest_float(
922
+ "C",
923
+ 0.01,
924
+ 30.0,
925
+ log=True
926
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
927
 
928
  threshold = trial.suggest_float(
929
+ "threshold",
930
+ 0.52,
931
+ 0.75
932
  )
933
 
934
+ stop_multiplier = trial.suggest_float(
935
+ "stop_multiplier",
936
+ 0.7,
937
+ 2.5
938
  )
939
 
940
  fold_scores = []
941
 
942
+ for train_idx, val_idx in splitter.split(X):
 
 
 
943
 
944
+ model = create_model(C)
945
 
946
+ model.fit(
947
+ X.iloc[train_idx],
948
+ y.iloc[train_idx]
 
 
 
 
 
949
  )
950
 
951
+ probabilities = get_probability(
952
+ model,
953
+ X.iloc[val_idx]
954
  )
955
 
956
+ trades = simulate_trades(
957
+ raw,
958
+ dataset,
959
+ val_idx,
960
+ probabilities,
961
+ threshold,
962
+ stop_multiplier,
963
+ horizon,
964
+ fee_rate,
965
+ slippage_bps
 
 
 
 
 
 
966
  )
967
 
968
+ metrics = calculate_metrics(trades)
 
 
 
 
 
 
 
969
 
970
+ # Aşırı az işlem yapan modelleri cezalandır.
971
  score = (
972
+ metrics["net_return"] * 100
973
+ + min(metrics["profit_factor"], 3) * 1.5
974
+ - metrics["max_drawdown"] * 40
975
+ + min(metrics["trades"], 20) * 0.05
976
  )
977
 
978
+ if metrics["trades"] < 3:
979
+ score -= 5
980
 
981
+ fold_scores.append(score)
 
982
 
983
  return float(np.mean(fold_scores))
984
 
985
  study = optuna.create_study(
986
  direction="maximize",
987
+ sampler=optuna.samplers.TPESampler(
988
+ seed=42
989
+ )
 
 
990
  )
991
 
992
  study.optimize(
 
995
  show_progress_bar=False
996
  )
997
 
998
+ return study.best_params, study.best_value
 
 
 
 
 
 
 
 
 
 
 
999
 
 
 
 
 
 
 
 
1000
 
1001
+ # ============================================================
1002
+ # HOLDOUT TEST
1003
+ # ============================================================
1004
 
1005
+ def independent_holdout(
1006
+ X,
1007
+ y,
1008
+ dataset,
1009
+ raw,
1010
+ horizon,
1011
+ C,
1012
+ threshold,
1013
+ stop_multiplier,
1014
+ fee_rate,
1015
+ slippage_bps
1016
+ ):
1017
 
1018
+ n = len(X)
1019
 
1020
+ split = int(n * 0.80)
1021
+
1022
+ train_end = split - horizon
1023
+
1024
+ if train_end < 300:
1025
+ raise ValueError(
1026
+ "Holdout için yeterli eğitim verisi yok."
1027
+ )
1028
+
1029
+ train_idx = np.arange(0, train_end)
1030
+
1031
+ test_idx = np.arange(split, n)
1032
+
1033
+ model = create_model(C)
1034
+
1035
+ model.fit(
1036
+ X.iloc[train_idx],
1037
+ y.iloc[train_idx]
1038
  )
1039
 
1040
+ probabilities = get_probability(
1041
+ model,
1042
+ X.iloc[test_idx]
1043
+ )
1044
 
1045
+ targets = y.iloc[test_idx].values
1046
 
1047
+ predictions = (
1048
+ probabilities >= 0.5
1049
+ ).astype(int)
1050
 
1051
+ trades = simulate_trades(
1052
+ raw,
1053
+ dataset,
1054
+ test_idx,
1055
+ probabilities,
1056
+ threshold,
1057
+ stop_multiplier,
1058
+ horizon,
1059
+ fee_rate,
1060
+ slippage_bps
1061
+ )
1062
 
1063
+ metrics = calculate_metrics(trades)
1064
+
1065
+ metrics["accuracy"] = accuracy_score(
1066
+ targets,
1067
+ predictions
1068
+ )
1069
 
1070
+ metrics["balanced_accuracy"] = (
1071
+ balanced_accuracy_score(
1072
+ targets,
1073
+ predictions
 
1074
  )
1075
+ )
1076
 
1077
+ metrics["brier"] = brier_score_loss(
1078
+ targets,
1079
+ probabilities
1080
+ )
1081
 
1082
+ return metrics, model, probabilities
 
 
 
 
 
1083
 
 
1084
 
1085
+ # ============================================================
1086
+ # OVERFIT RISK INDICATOR
1087
+ # ============================================================
1088
 
1089
+ def calculate_overfit_risk(
1090
+ wf_metrics,
1091
+ holdout_metrics
1092
+ ):
1093
 
1094
+ wf_ba = wf_metrics["balanced_accuracy"]
1095
 
1096
+ holdout_ba = holdout_metrics["balanced_accuracy"]
1097
+
1098
+ wf_pf = wf_metrics["profit_factor"]
1099
+
1100
+ holdout_pf = holdout_metrics["profit_factor"]
1101
+
1102
+ accuracy_gap = max(
1103
+ 0,
1104
+ wf_ba - holdout_ba
1105
  )
1106
 
1107
+ pf_gap = max(
1108
+ 0,
1109
+ wf_pf - holdout_pf
1110
+ )
1111
+
1112
+ risk = max(
1113
+ accuracy_gap * 500,
1114
+ pf_gap * 15
1115
+ )
1116
+
1117
+ risk = float(
1118
+ np.clip(risk, 0, 100)
1119
+ )
1120
 
1121
  if risk < 15:
1122
+
1123
  label = "DÜŞÜK"
1124
+
1125
  elif risk < 35:
1126
+
1127
  label = "ORTA"
1128
+
1129
  elif risk < 60:
1130
+
1131
  label = "YÜKSEK"
1132
+
1133
  else:
1134
+
1135
  label = "ÇOK YÜKSEK"
1136
 
1137
+ return risk, label
1138
 
1139
 
1140
+ # ============================================================
1141
+ # LIVE SIGNAL
1142
+ # ============================================================
1143
 
1144
+ def generate_live_signal(
1145
+ model,
1146
+ latest,
1147
+ threshold,
1148
  horizon,
1149
+ stop_multiplier
 
1150
  ):
1151
 
1152
+ X_live = latest[FEATURE_COLUMNS].astype(float)
1153
 
1154
+ probability_long = float(
1155
+ get_probability(model, X_live)[0]
1156
+ )
 
1157
 
1158
+ probability_short = 1 - probability_long
 
1159
 
1160
+ price = float(latest.iloc[0]["close"])
 
1161
 
1162
+ volatility = float(
1163
+ latest.iloc[0]["rv_24"]
1164
+ )
1165
 
1166
+ risk_pct = (
1167
+ volatility *
1168
+ np.sqrt(horizon) *
1169
+ stop_multiplier
1170
+ )
1171
 
1172
+ risk_pct = float(
1173
+ np.clip(risk_pct, 0.002, 0.05)
1174
+ )
 
 
1175
 
1176
+ risk_distance = price * risk_pct
1177
 
1178
+ if probability_long >= threshold:
 
 
 
1179
 
1180
+ signal = "LONG"
 
1181
 
1182
+ stop = price - risk_distance
 
1183
 
1184
+ target = price + 2 * risk_distance
 
1185
 
1186
+ elif probability_short >= threshold:
 
 
1187
 
1188
+ signal = "SHORT"
 
 
 
 
1189
 
1190
+ stop = price + risk_distance
 
1191
 
1192
+ target = price - 2 * risk_distance
 
1193
 
1194
+ else:
 
 
 
 
1195
 
1196
+ signal = "NO TRADE"
 
 
 
 
 
 
 
 
 
 
1197
 
1198
+ stop = None
 
 
 
 
 
 
1199
 
1200
+ target = None
 
 
 
 
 
 
1201
 
1202
+ return {
1203
+ "signal": signal,
1204
+ "long": probability_long,
1205
+ "short": probability_short,
1206
+ "price": price,
1207
+ "stop": stop,
1208
+ "target": target,
1209
+ "risk_pct": risk_pct
1210
+ }
1211
 
 
 
 
 
1212
 
1213
+ # ============================================================
1214
+ # MAIN APPLICATION
1215
+ # ============================================================
 
1216
 
1217
+ def run_quant(
1218
+ candle_count,
1219
+ horizon,
1220
+ trials,
1221
+ fee_percent,
1222
+ slippage_bps
1223
+ ):
1224
+
1225
+ try:
1226
 
1227
+ optuna.logging.set_verbosity(
1228
+ optuna.logging.WARNING
 
1229
  )
1230
 
1231
+ fee_rate = fee_percent / 100
 
 
1232
 
1233
+ # ----------------------------------------------------
1234
+ # FETCH
1235
+ # ----------------------------------------------------
1236
 
1237
+ raw = fetch_data(
1238
+ int(candle_count)
 
 
 
 
 
 
1239
  )
1240
 
1241
+ # ----------------------------------------------------
1242
+ # FEATURES
1243
+ # ----------------------------------------------------
1244
+
1245
+ X, y, dataset, live_features = prepare_dataset(
1246
+ raw,
1247
+ int(horizon)
1248
  )
1249
 
1250
+ # raw_i değerleri orijinal mum pozisyonlarıdır.
1251
+ # Backtest bu indeksleri kullanır.
 
 
1252
 
1253
+ # ----------------------------------------------------
1254
+ # DEVELOPMENT / HOLDOUT SEPARATION
1255
+ # ----------------------------------------------------
1256
 
1257
+ split = int(len(X) * 0.80)
1258
 
1259
+ development_end = split - int(horizon)
 
 
 
1260
 
1261
+ X_dev = X.iloc[:development_end].reset_index(
1262
+ drop=True
1263
+ )
1264
 
1265
+ y_dev = y.iloc[:development_end].reset_index(
1266
+ drop=True
1267
+ )
1268
 
1269
+ dev_dataset = dataset.iloc[
1270
+ :development_end
1271
+ ].reset_index(drop=True)
1272
 
1273
+ if len(X_dev) < 500:
1274
+ raise ValueError(
1275
+ "Optimizasyon için geliştirme verisi yetersiz."
1276
  )
1277
 
1278
+ # ----------------------------------------------------
1279
+ # OPTUNA
1280
+ # ----------------------------------------------------
 
 
 
 
 
 
1281
 
1282
+ best_params, best_score = optimize_model(
1283
+ X_dev,
1284
+ y_dev,
1285
+ dev_dataset,
1286
+ raw,
1287
+ int(horizon),
1288
+ fee_rate,
1289
+ float(slippage_bps),
1290
+ int(trials)
1291
+ )
1292
 
1293
+ C = best_params["C"]
 
 
1294
 
1295
+ threshold = best_params["threshold"]
 
 
1296
 
1297
+ stop_multiplier = best_params["stop_multiplier"]
 
 
1298
 
1299
+ # ----------------------------------------------------
1300
+ # WALK FORWARD
1301
+ # ----------------------------------------------------
1302
 
1303
+ wf_metrics, fold_reports = walk_forward_validation(
1304
+ X_dev,
1305
+ y_dev,
1306
+ dev_dataset,
1307
+ raw,
1308
+ int(horizon),
1309
+ C,
1310
+ threshold,
1311
+ stop_multiplier,
1312
+ fee_rate,
1313
+ float(slippage_bps)
1314
  )
1315
 
1316
+ # ----------------------------------------------------
1317
+ # INDEPENDENT HOLDOUT
1318
+ # ----------------------------------------------------
1319
+
1320
+ holdout_metrics, holdout_model, holdout_probs = (
1321
+ independent_holdout(
1322
+ X,
1323
+ y,
1324
+ dataset,
1325
+ raw,
1326
+ int(horizon),
1327
+ C,
1328
+ threshold,
1329
+ stop_multiplier,
1330
+ fee_rate,
1331
+ float(slippage_bps)
1332
+ )
1333
  )
1334
 
1335
+ # ----------------------------------------------------
1336
+ # OVERFIT RISK
1337
+ # ----------------------------------------------------
1338
+
1339
+ overfit_score, overfit_label = (
1340
+ calculate_overfit_risk(
1341
+ wf_metrics,
1342
+ holdout_metrics
1343
+ )
1344
  )
1345
 
1346
+ # ----------------------------------------------------
1347
+ # FINAL LIVE MODEL
1348
+ # ----------------------------------------------------
 
 
 
 
1349
 
1350
+ final_model = create_model(C)
 
1351
 
1352
+ final_model.fit(
1353
+ X,
1354
+ y
1355
+ )
 
 
1356
 
1357
+ latest = live_features.tail(1)
1358
 
1359
+ signal = generate_live_signal(
1360
+ final_model,
1361
+ latest,
1362
+ threshold,
1363
+ int(horizon),
1364
+ stop_multiplier
1365
  )
1366
 
1367
+ # ----------------------------------------------------
1368
+ # REPORT
1369
+ # ----------------------------------------------------
1370
 
1371
+ last_time = pd.to_datetime(
1372
+ latest.iloc[0]["timestamp"],
1373
+ unit="ms",
1374
+ utc=True
1375
  )
1376
 
1377
+ report = f"""
1378
+ # BTC/USDT QUANT ENGINE
1379
 
1380
+ <badge color="success">OKX CONNECTED</badge>
1381
 
1382
+ ## 1. Veri
 
 
1383
 
1384
+ | Metrik | Sonuç |
1385
+ |---|---:|
1386
+ | Ham mum sayısı | {len(raw)} |
1387
+ | Temiz eğitim örneği | {len(X)} |
1388
+ | Geliştirme örneği | {len(X_dev)} |
1389
+ | Holdout örneği | {len(X) - split} |
1390
+ | Zaman dilimi | 1H |
1391
+ | Tahmin ufku | {horizon} saat |
1392
+ | Son tamamlanmış mum | {last_time} |
1393
 
1394
+ ## 2. Canlı tahmin
 
1395
 
1396
+ # {signal['signal']}
1397
 
1398
  | Metrik | Sonuç |
1399
  |---|---:|
1400
+ | BTC fiyatı | {signal['price']:,.2f} USDT |
1401
+ | LONG olasılığı | %{signal['long'] * 100:.2f} |
1402
+ | SHORT olasılığı | %{signal['short'] * 100:.2f} |
1403
+ | Stop mesafesi | %{signal['risk_pct'] * 100:.3f} |
1404
+ | Stop-loss | {f"{signal['stop']:,.2f}" if signal['stop'] else "Yok"} |
1405
+ | 2R hedef | {f"{signal['target']:,.2f}" if signal['target'] else "Yok"} |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1406
 
1407
+ ## 3. Walk-forward sonuçları
1408
 
1409
  | Metrik | Sonuç |
1410
  |---|---:|
1411
+ | Balanced Accuracy | %{wf_metrics['balanced_accuracy'] * 100:.2f} |
1412
+ | Win Rate | %{wf_metrics['win_rate'] * 100:.2f} |
1413
+ | Profit Factor | {wf_metrics['profit_factor']:.2f} |
1414
+ | Net getiri | %{wf_metrics['net_return'] * 100:.2f} |
1415
+ | Max Drawdown | %{wf_metrics['max_drawdown'] * 100:.2f} |
1416
+ | İşlem sayısı | {wf_metrics['trades']} |
1417
 
1418
+ ## 4. Bağımsız holdout backtest
1419
 
1420
  | Metrik | Sonuç |
1421
  |---|---:|
1422
+ | Accuracy | %{holdout_metrics['accuracy'] * 100:.2f} |
1423
+ | Balanced Accuracy | %{holdout_metrics['balanced_accuracy'] * 100:.2f} |
1424
+ | Brier Score | {holdout_metrics['brier']:.4f} |
1425
+ | Win Rate | %{holdout_metrics['win_rate'] * 100:.2f} |
1426
+ | Profit Factor | {holdout_metrics['profit_factor']:.2f} |
1427
+ | Net getiri | %{holdout_metrics['net_return'] * 100:.2f} |
1428
+ | Max Drawdown | %{holdout_metrics['max_drawdown'] * 100:.2f} |
1429
+ | İşlem sayısı | {holdout_metrics['trades']} |
1430
 
1431
+ ## 5. Overfit analizi
1432
 
1433
+ # {overfit_label}
1434
 
1435
+ **Overfit risk puanı: {overfit_score:.1f}/100**
 
 
 
 
1436
 
1437
+ Bu puan walk-forward ve holdout sonuçları arasındaki farktan üretilen sezgisel bir göstergedir. İstatistiksel olarak kalibre edilmiş bir olasılık değildir.
 
 
 
 
1438
 
1439
+ ## 6. Optuna parametreleri
1440
 
1441
+ | Parametre | Değer |
1442
  |---|---:|
1443
+ | Logistic C | {C:.5f} |
1444
+ | Long/Short eşik | %{threshold * 100:.2f} |
1445
+ | Stop çarpanı | {stop_multiplier:.3f} |
1446
+ | Optuna en iyi skor | {best_score:.4f} |
1447
+ | Deneme sayısı | {trials} |
 
1448
 
1449
+ ## 7. Walk-forward fold sonuçları
1450
 
1451
+ | Fold | Balanced Acc. | Win Rate | PF | Net Getiri | İşlem |
1452
+ |---|---:|---:|---:|---:|---:|
1453
+ """
1454
+
1455
+ for f in fold_reports:
1456
 
1457
+ report += (
1458
+ f"| {f['fold']} "
1459
+ f"| %{f['balanced_accuracy'] * 100:.2f} "
1460
+ f"| %{f['win_rate'] * 100:.2f} "
1461
+ f"| {f['profit_factor']:.2f} "
1462
+ f"| %{f['net_return'] * 100:.2f} "
1463
+ f"| {f['trades']} |\\n"
1464
+ )
1465
 
1466
+ report += """
 
 
1467
 
1468
  ---
1469
 
1470
+ ### Model hakkında
1471
 
1472
+ - Teknik indikatör kullanılmadı.
1473
+ - Tahmin modeli Logistic Regression.
1474
+ - Özellikler fiyat, hacim ve istatistiksel matematikten üretildi.
1475
+ - Optuna yalnızca geliştirme verisinde çalıştı.
1476
+ - Holdout, optimizasyon sırasında kullanılmadı.
1477
+ - Stop ve hedef 1:2 risk/getiri oranına göre hesaplandı.
1478
+
1479
+ **Risk notu:** Bu backtestte fonlama, gerçek spread, likidasyon, piyasa derinliği ve emir kayması tam olarak modellenmez. Sonuçlar gerçek işlem performansını garanti etmez.
1480
  """
1481
 
1482
+ return report
1483
 
1484
  except Exception as e:
1485
 
1486
  return (
1487
+ "# UYGULAMA HATASI\n\n"
1488
  f"`{type(e).__name__}: {str(e)}`\n\n"
1489
+ "OKX bağlantısını, veri sayısını ve parametreleri kontrol et."
1490
  )
1491
 
1492
 
1493
+ # ============================================================
1494
  # GRADIO UI
1495
+ # ============================================================
1496
 
1497
  with gr.Blocks(
1498
+ title="OKX BTC Quant Engine",
1499
+ theme=gr.themes.Soft()
1500
+ ) as demo:
1501
 
1502
  gr.Markdown(
1503
  """
1504
+ # BTC/USDT Quant Engine
1505
 
1506
+ **Pure Mathematics · OKX Swap · 1 Hour**
1507
 
1508
+ RSI, MACD, EMA, VWAP veya Supertrend yok.
1509
+ Sadece matematiksel fiyat ve hacim analizi.
1510
  """
1511
  )
1512
 
1513
  with gr.Row():
1514
 
1515
+ candle_count = gr.Slider(
1516
+ minimum=1000,
1517
+ maximum=5000,
1518
+ value=3000,
1519
+ step=500,
1520
+ label="OKX Mum Sayısı"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1521
  )
1522
 
 
 
1523
  horizon = gr.Slider(
1524
+ minimum=2,
1525
  maximum=24,
1526
  value=6,
1527
  step=1,
1528
+ label="Tahmin Ufku (Saat)"
1529
  )
1530
 
1531
+ with gr.Row():
1532
+
1533
  trials = gr.Slider(
1534
+ minimum=5,
1535
+ maximum=50,
1536
+ value=15,
1537
+ step=5,
1538
+ label="Optuna Deneme Sayısı"
1539
  )
1540
 
1541
+ fee_percent = gr.Slider(
1542
  minimum=0,
1543
  maximum=0.20,
1544
  value=0.05,
1545
  step=0.01,
1546
+ label="Komisyon (%) / Taraf"
1547
+ )
1548
+
1549
+ slippage_bps = gr.Slider(
1550
+ minimum=0,
1551
+ maximum=20,
1552
+ value=2,
1553
+ step=1,
1554
+ label="Kayma (baz puan) / Taraf"
1555
  )
1556
 
1557
  run_button = gr.Button(
1558
+ "QUANT ANALİZİ BAŞLAT",
1559
  variant="primary"
1560
  )
1561
 
1562
  output = gr.Markdown()
1563
 
1564
  run_button.click(
1565
+ fn=run_quant,
1566
  inputs=[
1567
+ candle_count,
 
1568
  horizon,
1569
  trials,
1570
+ fee_percent,
1571
+ slippage_bps
1572
  ],
1573
  outputs=output
1574
  )
1575
 
1576
 
1577
  if __name__ == "__main__":
1578
+
1579
+ demo.queue()
1580
+
1581
+ demo.launch()