GoshawkVortexAI commited on
Commit
91d6b91
·
verified ·
1 Parent(s): 86aa19b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +696 -772
app.py CHANGED
@@ -9,15 +9,7 @@ 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
  # ============================================================
@@ -28,42 +20,18 @@ 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,30 +41,19 @@ def create_exchange():
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,
@@ -108,7 +65,7 @@ def fetch_data(candle_count):
108
  if not batch:
109
  break
110
 
111
- all_candles.extend(batch)
112
 
113
  last_timestamp = batch[-1][0]
114
 
@@ -117,7 +74,7 @@ def fetch_data(candle_count):
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:
@@ -125,11 +82,11 @@ def fetch_data(candle_count):
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,419 +97,486 @@ def fetch_data(candle_count):
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(
@@ -564,7 +588,8 @@ def simulate_trades(
564
  stop_multiplier,
565
  horizon,
566
  fee_rate,
567
- slippage_bps
 
568
  ):
569
 
570
  trades = []
@@ -572,36 +597,40 @@ def simulate_trades(
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
@@ -614,11 +643,25 @@ def simulate_trades(
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
@@ -635,30 +678,22 @@ def simulate_trades(
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
 
@@ -669,32 +704,25 @@ def simulate_trades(
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:
@@ -705,33 +733,33 @@ def simulate_trades(
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):
@@ -740,13 +768,13 @@ def calculate_metrics(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
 
@@ -756,47 +784,30 @@ def calculate_metrics(trades):
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
 
@@ -804,52 +815,49 @@ def calculate_metrics(trades):
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,
@@ -860,49 +868,27 @@ def walk_forward_validation(
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,
@@ -911,6 +897,8 @@ def optimize_model(
911
  trials
912
  ):
913
 
 
 
914
  splitter = TimeSeriesSplit(
915
  n_splits=3,
916
  gap=horizon
@@ -918,17 +906,10 @@ def optimize_model(
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(
@@ -937,50 +918,58 @@ def optimize_model(
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",
@@ -999,222 +988,224 @@ def optimize_model(
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,
@@ -1230,59 +1221,29 @@ def run_quant(
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,
@@ -1290,193 +1251,157 @@ def run_quant(
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
@@ -1484,85 +1409,84 @@ Bu puan walk-forward ve holdout sonuçları arasındaki farktan üretilen sezgis
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,
 
9
  import gradio as gr
10
  import optuna
11
 
12
+ from hmmlearn.hmm import GaussianHMM
 
 
 
 
 
 
 
 
13
 
14
 
15
  # ============================================================
 
20
  TIMEFRAME = "1h"
21
  TIMEFRAME_MS = 60 * 60 * 1000
22
 
23
+ HMM_COLUMNS = [
24
+ "log_return",
25
+ "realized_volatility",
26
+ "range_return"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  ]
28
 
29
 
30
  # ============================================================
31
+ # OKX DATA
32
  # ============================================================
33
 
34
+ def fetch_data(candle_count):
35
 
36
  exchange = ccxt.okx({
37
  "enableRateLimit": True,
 
41
  }
42
  })
43
 
 
 
 
 
 
 
 
44
  exchange.load_markets()
45
 
46
  if SYMBOL not in exchange.markets:
47
+ raise ValueError(f"{SYMBOL} OKX piyasasında bulunamadı.")
 
 
48
 
49
  now = exchange.milliseconds()
50
 
51
  since = now - (candle_count + 20) * TIMEFRAME_MS
52
 
53
+ candles = []
54
  cursor = since
55
 
56
+ for _ in range(int(candle_count / 100) + 20):
 
 
57
 
58
  batch = exchange.fetch_ohlcv(
59
  SYMBOL,
 
65
  if not batch:
66
  break
67
 
68
+ candles.extend(batch)
69
 
70
  last_timestamp = batch[-1][0]
71
 
 
74
 
75
  cursor = last_timestamp + TIMEFRAME_MS
76
 
77
+ if len(candles) >= candle_count + 100:
78
  break
79
 
80
  if last_timestamp >= now - TIMEFRAME_MS:
 
82
 
83
  time.sleep(exchange.rateLimit / 1000)
84
 
85
+ if not candles:
86
  raise ValueError("OKX veri döndürmedi.")
87
 
88
  df = pd.DataFrame(
89
+ candles,
90
  columns=[
91
  "timestamp",
92
  "open",
 
97
  ]
98
  )
99
 
100
+ df = df.drop_duplicates("timestamp")
 
 
 
101
  df = df.sort_values("timestamp")
102
 
 
103
  now = exchange.milliseconds()
104
 
105
+ # Yalnızca tamamlanmış mumlar
106
  df = df[
107
  df["timestamp"] + TIMEFRAME_MS <= now
108
+ ].copy()
109
 
110
  df = df.tail(candle_count).reset_index(drop=True)
111
 
112
  for col in ["open", "high", "low", "close", "volume"]:
113
+ df[col] = pd.to_numeric(df[col], errors="coerce")
 
 
 
 
 
 
 
 
114
 
115
+ df = df.replace([np.inf, -np.inf], np.nan)
116
  df = df.dropna().reset_index(drop=True)
117
 
118
  df = df[
119
  (df["close"] > 0) &
120
+ (df["high"] >= df["low"])
 
121
  ].reset_index(drop=True)
122
 
123
  if len(df) < 500:
124
  raise ValueError(
125
+ f"Yeterli mum yok. Gelen: {len(df)}"
126
  )
127
 
128
  return df
129
 
130
 
131
  # ============================================================
132
+ # MATHEMATICAL FEATURES
133
+ # No RSI / MACD / EMA / VWAP
134
  # ============================================================
135
 
136
+ def create_features(raw):
137
 
138
+ df = raw.copy()
139
 
140
+ log_close = np.log(df["close"])
141
 
142
+ returns = log_close.diff()
143
 
144
+ df["log_return"] = returns
145
 
146
+ # HMM observations
147
+ df["realized_volatility"] = (
148
+ returns.rolling(24).std()
149
+ )
150
 
151
+ df["range_return"] = np.log(
152
+ df["high"] / df["low"]
 
153
  )
154
 
155
+ # Bayesian analysis auxiliary statistics
156
+ df["return_6"] = (
157
+ log_close - log_close.shift(6)
158
+ )
159
 
160
+ df["return_24"] = (
161
+ log_close - log_close.shift(24)
162
+ )
163
 
164
+ df["volatility_48"] = (
165
+ returns.rolling(48).std()
166
  )
167
 
168
+ df["volume_change"] = np.log1p(
169
+ df["volume"]
170
+ ).diff()
171
 
172
+ df["volume_volatility"] = (
173
+ df["volume_change"].rolling(24).std()
174
+ )
175
 
176
+ df["return_skew"] = (
177
+ returns.rolling(48).skew()
178
+ )
179
 
180
+ df["return_kurtosis"] = (
181
+ returns.rolling(48).kurt()
182
+ )
183
 
184
+ df["autocorrelation"] = (
185
+ returns.rolling(48).corr(
186
+ returns.shift(1)
187
+ )
188
+ )
189
 
190
+ df["price_zscore"] = (
191
+ (log_close - log_close.rolling(48).mean())
192
+ / log_close.rolling(48).std()
193
+ )
194
 
195
+ df["raw_i"] = np.arange(len(df))
196
 
197
+ return df.replace(
198
+ [np.inf, -np.inf],
199
+ np.nan
200
+ )
201
 
 
202
 
203
+ # ============================================================
204
+ # DATASET
205
+ # ============================================================
206
 
207
+ def prepare_dataset(raw, horizon):
 
208
 
209
+ df = create_features(raw)
210
 
211
+ future_close = df["close"].shift(-horizon)
 
 
 
 
212
 
213
+ df["target"] = np.where(
214
+ future_close.notna(),
215
+ (future_close > df["close"]).astype(float),
216
+ np.nan
217
+ )
218
 
219
+ required = (
220
+ HMM_COLUMNS +
221
+ [
222
+ "return_6",
223
+ "return_24",
224
+ "volatility_48",
225
+ "volume_change",
226
+ "volume_volatility",
227
+ "return_skew",
228
+ "return_kurtosis",
229
+ "autocorrelation",
230
+ "price_zscore"
231
+ ]
232
+ )
233
 
234
+ live = df.dropna(
235
+ subset=required
236
+ ).copy()
237
 
238
+ dataset = live.dropna(
239
+ subset=["target"]
240
+ ).copy()
241
 
242
+ live = live.reset_index(drop=True)
243
+ dataset = dataset.reset_index(drop=True)
244
 
245
+ if len(dataset) < 500:
246
+ raise ValueError(
247
+ f"Temiz veri yetersiz: {len(dataset)}"
248
+ )
249
 
250
+ return dataset, live
 
251
 
 
 
252
 
253
+ # ============================================================
254
+ # HMM SCALER
255
+ # ============================================================
256
 
257
+ class HMMRegime:
258
 
259
+ def __init__(self):
260
 
261
+ self.mean = None
262
+ self.std = None
263
+ self.model = None
264
+ self.order = None
265
 
266
+ def scale_fit(self, X):
267
 
268
+ self.mean = np.mean(X, axis=0)
269
 
270
+ self.std = np.std(X, axis=0)
271
 
272
+ self.std = np.where(
273
+ self.std < 1e-10,
274
+ 1.0,
275
+ self.std
276
+ )
277
 
278
+ return (X - self.mean) / self.std
279
 
280
+ def scale_transform(self, X):
281
+
282
+ return (X - self.mean) / self.std
283
+
284
+ def fit(self, train_df):
285
+
286
+ X = train_df[HMM_COLUMNS].values.astype(float)
287
+
288
+ X_scaled = self.scale_fit(X)
289
+
290
+ self.model = GaussianHMM(
291
+ n_components=3,
292
+ covariance_type="diag",
293
+ n_iter=150,
294
+ tol=0.001,
295
+ min_covar=1e-4,
296
+ random_state=42
297
  )
298
 
299
+ self.model.fit(X_scaled)
300
 
301
+ # Rejimleri ortalama getirilerine göre sırala.
302
+ # En düşük: bearish, orta: neutral, en yüksek: bullish.
303
+ self.order = np.argsort(
304
+ self.model.means_[:, 0]
305
+ )
306
 
307
+ return self
 
 
 
308
 
309
+ def filter(self, df, initial_alpha=None):
310
 
311
+ X = df[HMM_COLUMNS].values.astype(float)
312
 
313
+ X_scaled = self.scale_transform(X)
314
 
315
+ model = self.model
 
 
316
 
317
+ means = model.means_
318
 
319
+ covars = np.asarray(model.covars_)
 
 
320
 
321
+ if covars.ndim == 3:
322
+ variances = np.diagonal(
323
+ covars,
324
+ axis1=1,
325
+ axis2=2
326
+ )
327
+ else:
328
+ variances = covars
329
 
330
+ variances = np.maximum(
331
+ variances,
332
+ 1e-8
333
  )
334
 
335
+ n_components = model.n_components
 
 
336
 
337
+ dimension = X_scaled.shape[1]
338
 
339
+ # Gaussian emission log likelihood
340
+ log_emissions = np.zeros(
341
+ (len(X_scaled), n_components)
342
  )
343
 
344
+ for state in range(n_components):
 
 
 
345
 
346
+ diff = X_scaled - means[state]
 
 
347
 
348
+ log_emissions[:, state] = -0.5 * (
349
+ dimension * np.log(2 * np.pi)
350
+ + np.sum(np.log(variances[state]))
351
+ + np.sum(
352
+ (diff ** 2) / variances[state],
353
+ axis=1
354
+ )
355
+ )
356
 
357
+ # Numerically stable forward filtering
358
+ posterior = []
359
 
360
+ if initial_alpha is None:
361
 
362
+ alpha = model.startprob_.copy()
 
 
 
363
 
364
+ else:
 
 
365
 
366
+ alpha = np.asarray(
367
+ initial_alpha,
368
+ dtype=float
369
+ ).copy()
370
 
371
+ transition = model.transmat_
372
 
373
+ for i in range(len(X_scaled)):
 
 
374
 
375
+ if i > 0 or initial_alpha is not None:
 
 
 
376
 
377
+ alpha = alpha @ transition
 
 
 
378
 
379
+ log_alpha = (
380
+ np.log(np.maximum(alpha, 1e-300))
381
+ + log_emissions[i]
382
+ )
383
 
384
+ log_alpha -= np.max(log_alpha)
 
 
385
 
386
+ alpha = np.exp(log_alpha)
 
 
387
 
388
+ total = alpha.sum()
 
 
 
389
 
390
+ if total <= 0 or not np.isfinite(total):
 
 
391
 
392
+ alpha = np.ones(n_components) / n_components
393
 
394
+ else:
395
 
396
+ alpha /= total
 
 
397
 
398
+ posterior.append(alpha.copy())
 
 
399
 
400
+ posterior = np.asarray(posterior)
 
 
401
 
402
+ # Stable column order: Bearish, Neutral, Bullish
403
+ posterior = posterior[:, self.order]
 
 
 
 
404
 
405
+ last_alpha = (
406
+ posterior[-1].copy()
407
+ if len(posterior)
408
+ else None
409
+ )
410
 
411
+ return posterior, last_alpha
 
 
 
 
 
412
 
 
 
 
413
 
414
+ # ============================================================
415
+ # BAYESIAN REGIME SWITCHING
416
+ # ============================================================
417
 
418
+ def bayesian_regime_probability(
419
+ train_posterior,
420
+ train_targets,
421
+ future_posterior,
422
+ prior_alpha=2.0,
423
+ prior_beta=2.0
424
+ ):
425
 
426
+ # State order:
427
+ # 0 Bearish, 1 Neutral, 2 Bullish
 
428
 
429
+ regime_probabilities = []
430
 
431
+ for state in range(3):
 
 
432
 
433
+ weights = train_posterior[:, state]
434
 
435
+ positive = np.sum(
436
+ weights * train_targets
437
+ )
438
 
439
+ negative = np.sum(
440
+ weights * (1 - train_targets)
441
+ )
442
 
443
+ posterior_up = (
444
+ prior_alpha + positive
445
+ ) / (
446
+ prior_alpha + prior_beta
447
+ + positive + negative
448
+ )
449
 
450
+ regime_probabilities.append(
451
+ posterior_up
452
+ )
453
+
454
+ regime_probabilities = np.asarray(
455
+ regime_probabilities
456
  )
457
 
458
+ # Bayesian mixture over HMM regime probabilities
459
+ long_probabilities = (
460
+ future_posterior @ regime_probabilities
 
461
  )
462
 
463
+ return (
464
+ long_probabilities,
465
+ regime_probabilities
466
+ )
467
 
468
 
469
  # ============================================================
470
+ # FIT REGIME MODEL AND BAYESIAN PROBABILITY
471
  # ============================================================
472
 
473
+ def fit_predict_regimes(train_df, train_y, future_df):
474
 
475
+ hmm = HMMRegime().fit(train_df)
476
 
477
+ train_posterior, last_alpha = hmm.filter(
478
+ train_df
479
+ )
480
 
481
+ future_posterior, _ = hmm.filter(
482
+ future_df,
483
+ initial_alpha=last_alpha
 
484
  )
485
 
486
+ probabilities, regime_probabilities = (
487
+ bayesian_regime_probability(
488
+ train_posterior,
489
+ train_y,
490
+ future_posterior
491
+ )
492
  )
493
 
494
+ return {
495
+ "probabilities": probabilities,
496
+ "train_posterior": train_posterior,
497
+ "future_posterior": future_posterior,
498
+ "regime_probabilities": regime_probabilities,
499
+ "hmm": hmm
500
+ }
501
 
 
 
 
502
 
503
+ # ============================================================
504
+ # SEQUENTIAL PROBABILITY RATIO TEST
505
+ # ============================================================
506
 
507
+ def sprt_test(
508
+ raw,
509
+ raw_index,
510
+ window=24,
511
+ p1=0.60,
512
+ alpha=0.10,
513
+ beta=0.10
514
+ ):
515
 
516
+ if raw_index < window:
517
+ return "INSUFFICIENT", 0.0, 0.0
 
 
 
518
 
519
+ returns = np.log(
520
+ raw["close"].iloc[
521
+ raw_index - window + 1:raw_index + 1
522
+ ].values
523
+ )
524
 
525
+ returns = np.diff(returns)
526
 
527
+ if len(returns) == 0:
528
+ return "INSUFFICIENT", 0.0, 0.0
 
 
529
 
530
+ signs = np.sign(returns)
531
 
532
+ up_count = np.sum(signs > 0)
533
 
534
+ down_count = np.sum(signs < 0)
 
 
535
 
536
+ # Neutral returns are not counted.
537
+ p0 = 0.50
538
 
539
+ log_up = np.log(p1 / p0)
 
540
 
541
+ log_down = np.log(
542
+ (1 - p1) / (1 - p0)
543
+ )
 
 
 
 
 
544
 
545
+ long_llr = (
546
+ up_count * log_up
547
+ + down_count * log_down
548
+ )
549
 
550
+ short_llr = (
551
+ down_count * log_up
552
+ + up_count * log_down
553
+ )
554
 
555
+ upper_boundary = np.log(
556
+ (1 - beta) / alpha
557
+ )
558
 
559
+ if long_llr >= upper_boundary:
560
 
561
+ decision = "LONG"
562
+
563
+ elif short_llr >= upper_boundary:
564
 
565
+ decision = "SHORT"
566
 
567
+ else:
568
+
569
+ decision = "NO CONFIRMATION"
570
+
571
+ return (
572
+ decision,
573
+ float(long_llr),
574
+ float(short_llr)
575
+ )
576
 
577
 
578
  # ============================================================
579
+ # BACKTEST
580
  # ============================================================
581
 
582
  def simulate_trades(
 
588
  stop_multiplier,
589
  horizon,
590
  fee_rate,
591
+ slippage_bps,
592
+ sprt_window
593
  ):
594
 
595
  trades = []
 
597
  if len(row_indices) == 0:
598
  return trades
599
 
600
+ last_raw_index = int(
601
  dataset.iloc[row_indices[-1]]["raw_i"]
602
  )
603
 
604
  cost = (
605
+ 2 * fee_rate
606
+ + 2 * slippage_bps / 10000
607
  )
608
 
609
+ last_exit_index = -1
610
+
611
+ for row_index, probability in zip(
612
  row_indices,
613
  probabilities
614
  ):
615
 
616
+ row = dataset.iloc[row_index]
617
 
618
  raw_i = int(row["raw_i"])
619
 
 
620
  entry_i = raw_i + 1
621
 
 
622
  exit_limit = entry_i + horizon - 1
623
 
624
+ if exit_limit > last_raw_index:
625
  continue
626
 
627
  if exit_limit >= len(raw):
628
  continue
629
 
630
+ # Prevent overlapping positions.
631
+ if entry_i <= last_exit_index:
632
+ continue
633
+
634
  if probability >= threshold:
635
 
636
  direction = 1
 
643
 
644
  continue
645
 
646
+ sprt_decision, long_llr, short_llr = sprt_test(
647
+ raw,
648
+ raw_i,
649
+ window=sprt_window
650
+ )
651
+
652
+ if direction == 1 and sprt_decision != "LONG":
653
+ continue
654
+
655
+ if direction == -1 and sprt_decision != "SHORT":
656
+ continue
657
+
658
  entry_price = float(
659
  raw.iloc[entry_i]["open"]
660
  )
661
 
662
+ volatility = float(
663
+ row["realized_volatility"]
664
+ )
665
 
666
  if not np.isfinite(volatility) or volatility <= 0:
667
  continue
 
678
  0.05
679
  )
680
 
681
+ distance = entry_price * risk_pct
682
 
683
  if direction == 1:
684
 
685
+ stop = entry_price - distance
 
 
686
 
687
+ target = entry_price + 2 * distance
 
 
688
 
689
  else:
690
 
691
+ stop = entry_price + distance
 
 
692
 
693
+ target = entry_price - 2 * distance
 
 
694
 
695
  exit_price = None
696
+ reason = "TIME"
697
 
698
  for j in range(entry_i, exit_limit + 1):
699
 
 
704
 
705
  if direction == 1:
706
 
707
+ hit_stop = low <= stop
708
+ hit_target = high >= target
 
709
 
710
  else:
711
 
712
+ hit_stop = high >= stop
713
+ hit_target = low <= target
 
714
 
715
+ # Conservative same-candle assumption.
 
716
  if hit_stop:
717
 
718
+ exit_price = stop
719
+ reason = "STOP"
 
 
720
  break
721
 
722
  if hit_target:
723
 
724
+ exit_price = target
725
+ reason = "TARGET"
 
 
726
  break
727
 
728
  if exit_price is None:
 
733
 
734
  if direction == 1:
735
 
736
+ gross = exit_price / entry_price - 1
 
 
737
 
738
  else:
739
 
740
+ gross = entry_price / exit_price - 1
 
 
741
 
742
+ net = gross - cost
743
 
744
  trades.append({
745
  "direction": "LONG" if direction == 1 else "SHORT",
 
746
  "entry": entry_price,
747
  "exit": exit_price,
748
+ "probability": float(probability),
749
+ "long_llr": long_llr,
750
+ "short_llr": short_llr,
751
+ "reason": reason,
752
+ "gross_return": gross,
753
+ "net_return": net
754
  })
755
 
756
+ last_exit_index = exit_limit
757
+
758
  return trades
759
 
760
 
761
  # ============================================================
762
+ # METRICS
763
  # ============================================================
764
 
765
  def calculate_metrics(trades):
 
768
 
769
  return {
770
  "trades": 0,
771
+ "win_rate": 0,
772
+ "profit_factor": 0,
773
+ "net_return": 0,
774
+ "max_drawdown": 0
775
  }
776
 
777
+ returns = np.asarray([
778
  t["net_return"] for t in trades
779
  ])
780
 
 
784
 
785
  if len(losses):
786
 
787
+ pf = wins.sum() / abs(losses.sum())
 
 
788
 
789
  elif len(wins):
790
 
791
+ pf = 5.0
792
 
793
  else:
794
 
795
+ pf = 0.0
796
 
797
  equity = np.cumprod(1 + returns)
798
 
799
+ peaks = np.maximum.accumulate(
800
  np.insert(equity, 0, 1)
801
  )[1:]
802
 
803
+ dd = 1 - equity / peaks
 
 
 
 
 
804
 
805
  return {
806
  "trades": len(trades),
807
+ "win_rate": len(wins) / len(trades),
808
+ "profit_factor": float(min(pf, 5)),
809
+ "net_return": float(equity[-1] - 1),
810
+ "max_drawdown": float(dd.max())
 
 
 
 
 
 
 
 
 
 
811
  }
812
 
813
 
 
815
  # WALK-FORWARD
816
  # ============================================================
817
 
818
+ def walk_forward(
 
 
819
  dataset,
820
  raw,
821
  horizon,
 
822
  threshold,
823
  stop_multiplier,
824
  fee_rate,
825
+ slippage_bps,
826
+ sprt_window
827
  ):
828
 
829
+ from sklearn.model_selection import TimeSeriesSplit
830
+
831
  splitter = TimeSeriesSplit(
832
  n_splits=3,
833
  gap=horizon
834
  )
835
 
 
 
836
  all_trades = []
837
+ reports = []
 
838
 
839
  for fold, (train_idx, val_idx) in enumerate(
840
+ splitter.split(dataset),
841
  start=1
842
  ):
843
 
844
+ train_df = dataset.iloc[
845
+ train_idx
846
+ ].reset_index(drop=True)
847
 
848
+ val_df = dataset.iloc[
849
+ val_idx
850
+ ].reset_index(drop=True)
 
851
 
852
+ train_y = train_df["target"].values.astype(int)
 
 
 
853
 
854
+ result = fit_predict_regimes(
855
+ train_df,
856
+ train_y,
857
+ val_df
858
+ )
859
 
860
+ probabilities = result["probabilities"]
 
 
861
 
862
  trades = simulate_trades(
863
  raw,
 
868
  stop_multiplier,
869
  horizon,
870
  fee_rate,
871
+ slippage_bps,
872
+ sprt_window
873
  )
874
 
875
  metrics = calculate_metrics(trades)
876
 
 
 
 
 
 
 
 
 
877
  all_trades.extend(trades)
878
 
879
+ reports.append({
880
  "fold": fold,
 
 
 
 
881
  **metrics
882
  })
883
 
884
+ return calculate_metrics(all_trades), reports
 
 
 
 
 
 
 
 
 
885
 
886
 
887
  # ============================================================
888
  # OPTUNA
889
  # ============================================================
890
 
891
+ def optimize(
 
 
892
  dataset,
893
  raw,
894
  horizon,
 
897
  trials
898
  ):
899
 
900
+ from sklearn.model_selection import TimeSeriesSplit
901
+
902
  splitter = TimeSeriesSplit(
903
  n_splits=3,
904
  gap=horizon
 
906
 
907
  def objective(trial):
908
 
 
 
 
 
 
 
 
909
  threshold = trial.suggest_float(
910
  "threshold",
911
+ 0.55,
912
+ 0.80
913
  )
914
 
915
  stop_multiplier = trial.suggest_float(
 
918
  2.5
919
  )
920
 
921
+ sprt_window = trial.suggest_categorical(
922
+ "sprt_window",
923
+ [12, 18, 24, 36, 48]
924
+ )
925
 
926
+ scores = []
927
 
928
+ for train_idx, val_idx in splitter.split(dataset):
929
 
930
+ train_df = dataset.iloc[
931
+ train_idx
932
+ ].reset_index(drop=True)
933
+
934
+ val_df = dataset.iloc[
935
+ val_idx
936
+ ].reset_index(drop=True)
937
 
938
+ train_y = train_df["target"].values.astype(int)
939
+
940
+ result = fit_predict_regimes(
941
+ train_df,
942
+ train_y,
943
+ val_df
944
  )
945
 
946
  trades = simulate_trades(
947
  raw,
948
  dataset,
949
  val_idx,
950
+ result["probabilities"],
951
  threshold,
952
  stop_multiplier,
953
  horizon,
954
  fee_rate,
955
+ slippage_bps,
956
+ sprt_window
957
  )
958
 
959
  metrics = calculate_metrics(trades)
960
 
 
961
  score = (
962
  metrics["net_return"] * 100
963
  + min(metrics["profit_factor"], 3) * 1.5
964
  - metrics["max_drawdown"] * 40
 
965
  )
966
 
967
  if metrics["trades"] < 3:
968
  score -= 5
969
 
970
+ scores.append(score)
971
 
972
+ return float(np.mean(scores))
973
 
974
  study = optuna.create_study(
975
  direction="maximize",
 
988
 
989
 
990
  # ============================================================
991
+ # HOLDOUT
992
  # ============================================================
993
 
994
+ def holdout_test(
 
 
995
  dataset,
996
  raw,
997
  horizon,
 
998
  threshold,
999
  stop_multiplier,
1000
  fee_rate,
1001
+ slippage_bps,
1002
+ sprt_window
1003
  ):
1004
 
1005
+ n = len(dataset)
1006
 
1007
  split = int(n * 0.80)
1008
 
1009
  train_end = split - horizon
1010
 
1011
+ train_df = dataset.iloc[
1012
+ :train_end
1013
+ ].reset_index(drop=True)
 
 
 
1014
 
1015
+ test_df = dataset.iloc[
1016
+ split:
1017
+ ].reset_index(drop=True)
1018
 
1019
+ train_y = train_df["target"].values.astype(int)
1020
 
1021
+ result = fit_predict_regimes(
1022
+ train_df,
1023
+ train_y,
1024
+ test_df
1025
  )
1026
 
1027
+ test_indices = np.arange(
1028
+ split,
1029
+ n
1030
  )
1031
 
 
 
 
 
 
 
1032
  trades = simulate_trades(
1033
  raw,
1034
  dataset,
1035
+ test_indices,
1036
+ result["probabilities"],
1037
  threshold,
1038
  stop_multiplier,
1039
  horizon,
1040
  fee_rate,
1041
+ slippage_bps,
1042
+ sprt_window
1043
  )
1044
 
1045
  metrics = calculate_metrics(trades)
1046
 
1047
+ return metrics, result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1048
 
1049
 
1050
  # ============================================================
1051
+ # OVERFIT INDICATOR
1052
  # ============================================================
1053
 
1054
+ def overfit_indicator(wf, holdout):
 
 
 
 
 
 
 
1055
 
1056
+ # Heuristic generalization-gap indicator.
1057
+ # Not a statistically calibrated probability.
1058
 
1059
+ gap = max(
 
 
1060
  0,
1061
+ wf["win_rate"] - holdout["win_rate"]
1062
  )
1063
 
1064
  pf_gap = max(
1065
  0,
1066
+ wf["profit_factor"] - holdout["profit_factor"]
1067
  )
1068
 
1069
+ score = max(
1070
+ gap * 150,
1071
  pf_gap * 15
1072
  )
1073
 
1074
+ score = float(
1075
+ np.clip(score, 0, 100)
1076
  )
1077
 
1078
+ if score < 15:
 
1079
  label = "DÜŞÜK"
1080
 
1081
+ elif score < 35:
 
1082
  label = "ORTA"
1083
 
1084
+ elif score < 60:
 
1085
  label = "YÜKSEK"
1086
 
1087
  else:
 
1088
  label = "ÇOK YÜKSEK"
1089
 
1090
+ return score, label
1091
 
1092
 
1093
  # ============================================================
1094
+ # LIVE PREDICTION
1095
  # ============================================================
1096
 
1097
+ def live_prediction(
1098
+ dataset,
1099
+ raw,
 
1100
  horizon,
1101
+ params
1102
  ):
1103
 
1104
+ train_y = dataset["target"].values.astype(int)
1105
+
1106
+ latest = dataset.tail(1).reset_index(drop=True)
1107
 
1108
+ # Final model: all currently labeled data.
1109
+ result = fit_predict_regimes(
1110
+ dataset,
1111
+ train_y,
1112
+ latest
1113
  )
1114
 
1115
+ long_probability = float(
1116
+ result["probabilities"][-1]
1117
+ )
1118
 
1119
+ short_probability = 1 - long_probability
1120
 
1121
+ hmm_posterior = result["future_posterior"][-1]
 
 
1122
 
1123
+ threshold = params["threshold"]
1124
+
1125
+ stop_multiplier = params["stop_multiplier"]
1126
+
1127
+ sprt_window = params["sprt_window"]
1128
 
1129
+ raw_i = int(
1130
+ latest.iloc[0]["raw_i"]
1131
  )
1132
 
1133
+ sprt_decision, long_llr, short_llr = sprt_test(
1134
+ raw,
1135
+ raw_i,
1136
+ window=sprt_window
1137
+ )
1138
 
1139
+ if (
1140
+ long_probability >= threshold
1141
+ and sprt_decision == "LONG"
1142
+ ):
1143
 
1144
  signal = "LONG"
1145
 
1146
+ elif (
1147
+ short_probability >= threshold
1148
+ and sprt_decision == "SHORT"
1149
+ ):
1150
 
1151
+ signal = "SHORT"
1152
 
1153
+ else:
1154
 
1155
+ signal = "NO TRADE"
1156
 
1157
+ price = float(
1158
+ latest.iloc[0]["close"]
1159
+ )
1160
 
1161
+ volatility = float(
1162
+ latest.iloc[0]["realized_volatility"]
1163
+ )
1164
 
1165
+ risk_pct = np.clip(
1166
+ volatility * np.sqrt(horizon) * stop_multiplier,
1167
+ 0.002,
1168
+ 0.05
1169
+ )
1170
 
1171
+ distance = price * risk_pct
1172
 
1173
+ if signal == "LONG":
1174
+
1175
+ stop = price - distance
1176
+ target = price + 2 * distance
1177
 
1178
+ elif signal == "SHORT":
1179
+
1180
+ stop = price + distance
1181
+ target = price - 2 * distance
1182
+
1183
+ else:
1184
+
1185
+ stop = None
1186
  target = None
1187
 
1188
  return {
1189
  "signal": signal,
1190
+ "long": long_probability,
1191
+ "short": short_probability,
1192
  "price": price,
1193
  "stop": stop,
1194
  "target": target,
1195
+ "risk_pct": risk_pct,
1196
+ "regimes": hmm_posterior,
1197
+ "sprt": sprt_decision,
1198
+ "long_llr": long_llr,
1199
+ "short_llr": short_llr,
1200
+ "regime_bayes": result["regime_probabilities"]
1201
  }
1202
 
1203
 
1204
  # ============================================================
1205
+ # MAIN
1206
  # ============================================================
1207
 
1208
+ def run_app(
1209
  candle_count,
1210
  horizon,
1211
  trials,
 
1221
 
1222
  fee_rate = fee_percent / 100
1223
 
 
 
 
 
1224
  raw = fetch_data(
1225
  int(candle_count)
1226
  )
1227
 
1228
+ dataset, live = prepare_dataset(
 
 
 
 
1229
  raw,
1230
  int(horizon)
1231
  )
1232
 
1233
+ split = int(len(dataset) * 0.80)
 
 
 
 
 
 
 
 
 
1234
 
1235
+ development = dataset.iloc[
1236
+ :split - int(horizon)
 
 
 
 
 
 
 
 
1237
  ].reset_index(drop=True)
1238
 
1239
+ if len(development) < 300:
1240
  raise ValueError(
1241
+ "Optimizasyon için veri yetersiz."
1242
  )
1243
 
1244
+ # 1. OPTUNA
1245
+ best_params, best_score = optimize(
1246
+ development,
 
 
 
 
 
1247
  raw,
1248
  int(horizon),
1249
  fee_rate,
 
1251
  int(trials)
1252
  )
1253
 
1254
+ # 2. WALK FORWARD
1255
+ wf, fold_reports = walk_forward(
1256
+ development,
 
 
 
 
 
 
 
 
 
 
 
1257
  raw,
1258
  int(horizon),
1259
+ best_params["threshold"],
1260
+ best_params["stop_multiplier"],
 
1261
  fee_rate,
1262
+ float(slippage_bps),
1263
+ best_params["sprt_window"]
1264
  )
1265
 
1266
+ # 3. INDEPENDENT HOLDOUT
1267
+ holdout, holdout_result = holdout_test(
1268
+ dataset,
1269
+ raw,
1270
+ int(horizon),
1271
+ best_params["threshold"],
1272
+ best_params["stop_multiplier"],
1273
+ fee_rate,
1274
+ float(slippage_bps),
1275
+ best_params["sprt_window"]
 
 
 
 
 
 
 
1276
  )
1277
 
1278
+ # 4. OVERFIT
1279
+ overfit_score, overfit_label = overfit_indicator(
1280
+ wf,
1281
+ holdout
 
 
 
 
 
1282
  )
1283
 
1284
+ # 5. LIVE
1285
+ live_result = live_prediction(
1286
+ dataset,
1287
+ raw,
 
 
 
 
 
 
 
 
 
 
 
 
 
1288
  int(horizon),
1289
+ best_params
1290
  )
1291
 
 
 
 
 
1292
  last_time = pd.to_datetime(
1293
+ int(raw.iloc[-1]["timestamp"]),
1294
  unit="ms",
1295
  utc=True
1296
  )
1297
 
1298
+ regimes = live_result["regimes"]
1299
+
1300
+ bayes = live_result["regime_bayes"]
1301
+
1302
  report = f"""
1303
+ # OKX BTC/USDT QUANT ENGINE
1304
 
1305
+ <badge color="success">HMM + BAYESIAN + SPRT</badge>
1306
 
1307
+ ## Veri
1308
 
1309
  | Metrik | Sonuç |
1310
  |---|---:|
1311
+ | Ham mum | {len(raw)} |
1312
+ | Temiz örnek | {len(dataset)} |
1313
+ | Geliştirme örneği | {len(development)} |
1314
+ | Holdout örneği | {len(dataset)-split} |
1315
  | Zaman dilimi | 1H |
 
1316
  | Son tamamlanmış mum | {last_time} |
1317
 
1318
+ ## Canlı sinyal
1319
 
1320
+ # {live_result['signal']}
1321
 
1322
  | Metrik | Sonuç |
1323
  |---|---:|
1324
+ | BTC fiyatı | {live_result['price']:,.2f} USDT |
1325
+ | LONG olasılığı | %{live_result['long']*100:.2f} |
1326
+ | SHORT olasılığı | %{live_result['short']*100:.2f} |
1327
+ | SPRT kararı | {live_result['sprt']} |
1328
+ | LONG LLR | {live_result['long_llr']:.3f} |
1329
+ | SHORT LLR | {live_result['short_llr']:.3f} |
1330
+ | Risk mesafesi | %{live_result['risk_pct']*100:.3f} |
1331
+ | Stop-loss | {f"{live_result['stop']:,.2f}" if live_result['stop'] else "Yok"} |
1332
+ | 2R hedef | {f"{live_result['target']:,.2f}" if live_result['target'] else "Yok"} |
1333
+
1334
+ ## HMM gizli rejimleri
1335
+
1336
+ | Rejim | Olasılık |
1337
+ |---|---:|
1338
+ | Bearish | %{regimes[0]*100:.2f} |
1339
+ | Neutral | %{regimes[1]*100:.2f} |
1340
+ | Bullish | %{regimes[2]*100:.2f} |
1341
 
1342
+ ## Bayesian rejim olasılıkları
1343
 
1344
+ | Rejim | LONG olasılığı |
1345
  |---|---:|
1346
+ | Bearish | %{bayes[0]*100:.2f} |
1347
+ | Neutral | %{bayes[1]*100:.2f} |
1348
+ | Bullish | %{bayes[2]*100:.2f} |
 
 
 
1349
 
1350
+ ## Walk-forward backtest
1351
 
1352
  | Metrik | Sonuç |
1353
  |---|---:|
1354
+ | İşlem sayısı | {wf['trades']} |
1355
+ | Win rate | %{wf['win_rate']*100:.2f} |
1356
+ | Profit Factor | {wf['profit_factor']:.2f} |
1357
+ | Net getiri | %{wf['net_return']*100:.2f} |
1358
+ | Max drawdown | %{wf['max_drawdown']*100:.2f} |
 
 
 
1359
 
1360
+ ## Bağımsız holdout
1361
 
1362
+ | Metrik | Sonuç |
1363
+ |---|---:|
1364
+ | İşlem sayısı | {holdout['trades']} |
1365
+ | Win rate | %{holdout['win_rate']*100:.2f} |
1366
+ | Profit Factor | {holdout['profit_factor']:.2f} |
1367
+ | Net getiri | %{holdout['net_return']*100:.2f} |
1368
+ | Max drawdown | %{holdout['max_drawdown']*100:.2f} |
1369
 
1370
+ ## Overfit göstergesi
1371
 
1372
+ **{overfit_label} — {overfit_score:.1f}/100**
1373
 
1374
+ Bu puan walk-forward ve holdout sonuçları arasındaki farkı gösteren sezgisel bir göstergedir; overfit olasılığı değildir.
1375
+
1376
+ ## Optuna parametreleri
1377
 
1378
  | Parametre | Değer |
1379
  |---|---:|
1380
+ | Probability threshold | {best_params['threshold']:.4f} |
1381
+ | Stop multiplier | {best_params['stop_multiplier']:.4f} |
1382
+ | SPRT window | {best_params['sprt_window']} |
1383
+ | Optuna score | {best_score:.4f} |
 
1384
 
1385
+ ## Walk-forward fold detayları
1386
 
1387
+ | Fold | İşlem | Win rate | PF | Getiri |
1388
+ |---|---:|---:|---:|---:|
1389
  """
1390
 
1391
  for f in fold_reports:
1392
 
1393
  report += (
1394
  f"| {f['fold']} "
1395
+ f"| {f['trades']} "
1396
+ f"| %{f['win_rate']*100:.2f} "
1397
  f"| {f['profit_factor']:.2f} "
1398
+ f"| %{f['net_return']*100:.2f} |\n"
 
1399
  )
1400
 
1401
  report += """
 
1402
  ---
1403
 
1404
+ **Önemli:** Backtest; funding, gerçek emir defteri derinliği, likidasyon ve gerçek emir kaymasını eksiksiz modellemez. Sonuçlar gelecekteki performansı garanti etmez.
 
 
 
 
 
 
 
 
 
1405
  """
1406
 
1407
  return report
 
1409
  except Exception as e:
1410
 
1411
  return (
1412
+ "# HATA\n\n"
1413
+ f"`{type(e).__name__}: {str(e)}`"
 
1414
  )
1415
 
1416
 
1417
  # ============================================================
1418
+ # GRADIO
1419
  # ============================================================
1420
 
1421
  with gr.Blocks(
1422
+ title="OKX Bayesian HMM Quant",
1423
  theme=gr.themes.Soft()
1424
  ) as demo:
1425
 
1426
  gr.Markdown(
1427
  """
1428
+ # BTC/USDT Bayesian Regime Engine
1429
 
1430
+ **HMM + Bayesian Regime Switching + SPRT**
1431
 
1432
+ Klasik teknik indikatör yok.
1433
+ Yalnızca matematiksel ve olasılıksal modelleme.
1434
  """
1435
  )
1436
 
1437
  with gr.Row():
1438
 
1439
  candle_count = gr.Slider(
1440
+ 1000,
1441
+ 5000,
1442
  value=3000,
1443
  step=500,
1444
+ label="OKX mum sayısı"
1445
  )
1446
 
1447
  horizon = gr.Slider(
1448
+ 2,
1449
+ 24,
1450
  value=6,
1451
  step=1,
1452
+ label="Tahmin ufku (saat)"
1453
  )
1454
 
1455
  with gr.Row():
1456
 
1457
  trials = gr.Slider(
1458
+ 5,
1459
+ 50,
1460
  value=15,
1461
  step=5,
1462
+ label="Optuna denemesi"
1463
  )
1464
 
1465
  fee_percent = gr.Slider(
1466
+ 0,
1467
+ 0.20,
1468
  value=0.05,
1469
  step=0.01,
1470
+ label="Komisyon % / taraf"
1471
  )
1472
 
1473
  slippage_bps = gr.Slider(
1474
+ 0,
1475
+ 20,
1476
  value=2,
1477
  step=1,
1478
+ label="Kayma baz puan / taraf"
1479
  )
1480
 
1481
  run_button = gr.Button(
1482
+ "BAYESIAN QUANT ANALİZİ BAŞLAT",
1483
  variant="primary"
1484
  )
1485
 
1486
  output = gr.Markdown()
1487
 
1488
  run_button.click(
1489
+ fn=run_app,
1490
  inputs=[
1491
  candle_count,
1492
  horizon,