GoshawkVortexAI commited on
Commit
d49e978
·
verified ·
1 Parent(s): 6be2c62

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +944 -655
app.py CHANGED
@@ -10,6 +10,7 @@ import gradio as gr
10
  import optuna
11
 
12
  from hmmlearn.hmm import GaussianHMM
 
13
 
14
 
15
  # ============================================================
@@ -20,12 +21,25 @@ SYMBOL = "BTC/USDT:USDT"
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
@@ -44,16 +58,19 @@ def fetch_data(candle_count):
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,
@@ -69,7 +86,7 @@ def fetch_data(candle_count):
69
 
70
  last_timestamp = batch[-1][0]
71
 
72
- if last_timestamp <= cursor:
73
  break
74
 
75
  cursor = last_timestamp + TIMEFRAME_MS
@@ -80,7 +97,9 @@ def fetch_data(candle_count):
80
  if last_timestamp >= now - TIMEFRAME_MS:
81
  break
82
 
83
- time.sleep(exchange.rateLimit / 1000)
 
 
84
 
85
  if not candles:
86
  raise ValueError("OKX veri döndürmedi.")
@@ -97,40 +116,54 @@ def fetch_data(candle_count):
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):
@@ -141,9 +174,9 @@ def create_features(raw):
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
  )
@@ -152,33 +185,14 @@ def create_features(raw):
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"] = (
@@ -187,18 +201,26 @@ def create_features(raw):
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
@@ -208,397 +230,531 @@ 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(
583
  raw,
584
  dataset,
585
- row_indices,
586
  probabilities,
587
  threshold,
588
  stop_multiplier,
589
  horizon,
590
  fee_rate,
591
- slippage_bps,
592
- sprt_window
593
  ):
594
 
595
  trades = []
596
 
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 = (
@@ -606,29 +762,32 @@ def simulate_trades(
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:
@@ -643,18 +802,6 @@ def simulate_trades(
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
  )
@@ -663,22 +810,23 @@ def simulate_trades(
663
  row["realized_volatility"]
664
  )
665
 
666
- if not np.isfinite(volatility) or volatility <= 0:
 
 
 
667
  continue
668
 
669
- risk_pct = (
670
- volatility *
671
- np.sqrt(horizon) *
672
- stop_multiplier
673
- )
674
-
675
  risk_pct = np.clip(
676
- risk_pct,
 
 
677
  0.002,
678
  0.05
679
  )
680
 
681
- distance = entry_price * risk_pct
 
 
682
 
683
  if direction == 1:
684
 
@@ -693,36 +841,47 @@ def simulate_trades(
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
 
700
  candle = raw.iloc[j]
701
 
702
  high = float(candle["high"])
 
703
  low = float(candle["low"])
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,27 +892,32 @@ def simulate_trades(
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
 
@@ -768,15 +932,16 @@ 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
 
781
  wins = returns[returns > 0]
782
 
@@ -784,192 +949,182 @@ def calculate_metrics(trades):
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
 
814
  # ============================================================
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,
864
- dataset,
865
- val_idx,
866
- probabilities,
867
- threshold,
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,
895
- fee_rate,
896
- slippage_bps,
897
- trials
898
- ):
899
-
900
- from sklearn.model_selection import TimeSeriesSplit
901
-
902
- splitter = TimeSeriesSplit(
903
- n_splits=3,
904
- gap=horizon
905
  )
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(
916
  "stop_multiplier",
917
- 0.7,
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",
@@ -980,7 +1135,7 @@ def optimize(
980
 
981
  study.optimize(
982
  objective,
983
- n_trials=trials,
984
  show_progress_bar=False
985
  )
986
 
@@ -988,18 +1143,107 @@ def optimize(
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)
@@ -1012,19 +1256,23 @@ def holdout_test(
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
  )
@@ -1032,62 +1280,78 @@ def holdout_test(
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
  # ============================================================
@@ -1096,57 +1360,46 @@ def overfit_indicator(wf, holdout):
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
 
@@ -1154,16 +1407,20 @@ def live_prediction(
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
  )
@@ -1173,36 +1430,52 @@ def live_prediction(
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(
@@ -1219,7 +1492,9 @@ def run_app(
1219
  optuna.logging.WARNING
1220
  )
1221
 
1222
- fee_rate = fee_percent / 100
 
 
1223
 
1224
  raw = fetch_data(
1225
  int(candle_count)
@@ -1230,20 +1505,9 @@ def run_app(
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,157 +1515,181 @@ def run_app(
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,44 +1697,45 @@ Bu puan walk-forward ve holdout sonuçları arasındaki farkı gösteren sezgise
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)"
@@ -1455,31 +1744,31 @@ Yalnızca matematiksel ve olasılıksal modelleme.
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
 
 
10
  import optuna
11
 
12
  from hmmlearn.hmm import GaussianHMM
13
+ from sklearn.preprocessing import StandardScaler
14
 
15
 
16
  # ============================================================
 
21
  TIMEFRAME = "1h"
22
  TIMEFRAME_MS = 60 * 60 * 1000
23
 
24
+ MACRO_FEATURES = [
25
  "log_return",
26
  "realized_volatility",
27
  "range_return"
28
  ]
29
 
30
+ MICRO_FEATURES = [
31
+ "return_zscore",
32
+ "autocorrelation",
33
+ "volume_zscore"
34
+ ]
35
+
36
+ N_MACRO = 3
37
+ N_MICRO = 2
38
+ N_JOINT = N_MACRO * N_MICRO
39
+
40
+ MIN_TRAIN_ROWS = 300
41
+ MIN_RELIABLE_TRADES = 30
42
+
43
 
44
  # ============================================================
45
  # OKX DATA
 
58
  exchange.load_markets()
59
 
60
  if SYMBOL not in exchange.markets:
61
+ raise ValueError(
62
+ f"{SYMBOL} OKX piyasasında bulunamadı."
63
+ )
64
 
65
  now = exchange.milliseconds()
66
 
67
+ cursor = now - (
68
+ int(candle_count) + 30
69
+ ) * TIMEFRAME_MS
70
 
71
  candles = []
 
72
 
73
+ for _ in range(60):
74
 
75
  batch = exchange.fetch_ohlcv(
76
  SYMBOL,
 
86
 
87
  last_timestamp = batch[-1][0]
88
 
89
+ if last_timestamp < cursor:
90
  break
91
 
92
  cursor = last_timestamp + TIMEFRAME_MS
 
97
  if last_timestamp >= now - TIMEFRAME_MS:
98
  break
99
 
100
+ time.sleep(
101
+ exchange.rateLimit / 1000
102
+ )
103
 
104
  if not candles:
105
  raise ValueError("OKX veri döndürmedi.")
 
116
  ]
117
  )
118
 
119
+ df = (
120
+ df.drop_duplicates("timestamp")
121
+ .sort_values("timestamp")
122
+ .reset_index(drop=True)
123
+ )
124
 
125
  now = exchange.milliseconds()
126
 
127
+ # Yalnızca kapanmış mumlar
128
  df = df[
129
  df["timestamp"] + TIMEFRAME_MS <= now
130
  ].copy()
131
 
132
+ for col in [
133
+ "open", "high", "low", "close", "volume"
134
+ ]:
135
+ df[col] = pd.to_numeric(
136
+ df[col],
137
+ errors="coerce"
138
+ )
139
 
140
+ df = df.replace(
141
+ [np.inf, -np.inf],
142
+ np.nan
143
+ ).dropna()
144
 
145
  df = df[
146
  (df["close"] > 0) &
147
+ (df["high"] >= df["low"]) &
148
+ (df["low"] > 0)
149
+ ]
150
+
151
+ df = df.tail(
152
+ int(candle_count)
153
+ ).reset_index(drop=True)
154
 
155
  if len(df) < 500:
156
  raise ValueError(
157
+ f"Yeterli veri yok: {len(df)} mum."
158
  )
159
 
160
+ df["raw_i"] = np.arange(len(df))
161
+
162
  return df
163
 
164
 
165
  # ============================================================
166
+ # CAUSAL MATHEMATICAL FEATURES
 
167
  # ============================================================
168
 
169
  def create_features(raw):
 
174
 
175
  returns = log_close.diff()
176
 
177
+ # Macro layer
178
  df["log_return"] = returns
179
 
 
180
  df["realized_volatility"] = (
181
  returns.rolling(24).std()
182
  )
 
185
  df["high"] / df["low"]
186
  )
187
 
188
+ # Micro layer
189
+ rolling_mean = returns.rolling(48).mean()
 
 
 
 
 
 
 
 
 
 
190
 
191
+ rolling_std = returns.rolling(48).std()
 
 
192
 
193
+ df["return_zscore"] = (
194
+ (returns - rolling_mean) /
195
+ rolling_std.replace(0, np.nan)
 
 
 
 
 
 
 
196
  )
197
 
198
  df["autocorrelation"] = (
 
201
  )
202
  )
203
 
204
+ log_volume = np.log1p(
205
+ df["volume"].clip(lower=0)
 
206
  )
207
 
208
+ volume_mean = log_volume.rolling(48).mean()
209
+
210
+ volume_std = log_volume.rolling(48).std()
211
 
212
+ df["volume_zscore"] = (
213
+ (log_volume - volume_mean) /
214
+ volume_std.replace(0, np.nan)
215
+ )
216
+
217
+ df = df.replace(
218
  [np.inf, -np.inf],
219
  np.nan
220
  )
221
 
222
+ return df
223
+
224
 
225
  # ============================================================
226
  # DATASET
 
230
 
231
  df = create_features(raw)
232
 
 
 
 
 
 
 
 
 
233
  required = (
234
+ MACRO_FEATURES +
235
+ MICRO_FEATURES
 
 
 
 
 
 
 
 
 
 
236
  )
237
 
238
  live = df.dropna(
239
  subset=required
240
  ).copy()
241
 
242
+ future_close = live["close"].shift(
243
+ -int(horizon)
244
+ )
245
+
246
+ live["target"] = np.where(
247
+ future_close.notna(),
248
+ (
249
+ future_close > live["close"]
250
+ ).astype(float),
251
+ np.nan
252
+ )
253
+
254
  dataset = live.dropna(
255
  subset=["target"]
256
  ).copy()
257
 
 
258
  dataset = dataset.reset_index(drop=True)
259
 
260
+ live = live.reset_index(drop=True)
261
+
262
+ if len(dataset) < MIN_TRAIN_ROWS:
263
  raise ValueError(
264
+ f"Yeterli eğitim verisi yok: {len(dataset)}"
265
  )
266
 
267
  return dataset, live
268
 
269
 
270
  # ============================================================
271
+ # GAUSSIAN HMM FORWARD FILTER
272
  # ============================================================
273
 
274
+ def forward_filter(
275
+ model,
276
+ X_scaled,
277
+ initial_alpha=None
278
+ ):
279
 
280
+ means = model.means_
281
+
282
+ covars = np.asarray(
283
+ model.covars_
284
+ )
285
+
286
+ if covars.ndim == 3:
287
+
288
+ variances = np.diagonal(
289
+ covars,
290
+ axis1=1,
291
+ axis2=2
292
+ )
293
 
294
+ else:
295
+
296
+ variances = covars
297
+
298
+ variances = np.maximum(
299
+ variances,
300
+ 1e-8
301
+ )
302
 
303
+ n_states = model.n_components
304
+
305
+ n_features = X_scaled.shape[1]
306
+
307
+ log_emissions = np.zeros(
308
+ (len(X_scaled), n_states)
309
+ )
310
 
311
+ # Gaussian emission likelihood
312
+ for state in range(n_states):
313
 
314
+ diff = (
315
+ X_scaled - means[state]
316
+ )
317
 
318
+ log_emissions[:, state] = -0.5 * (
319
+ n_features * np.log(2 * np.pi)
320
+ + np.sum(
321
+ np.log(variances[state])
322
+ )
323
+ + np.sum(
324
+ diff ** 2 / variances[state],
325
+ axis=1
326
+ )
327
  )
328
 
329
+ transition = model.transmat_
330
 
331
+ if initial_alpha is None:
332
 
333
+ alpha = model.startprob_.copy()
334
 
335
+ first_observation = 0
336
+
337
+ else:
338
+
339
+ alpha = np.asarray(
340
+ initial_alpha,
341
+ dtype=float
342
+ ).copy()
343
 
344
+ first_observation = 0
345
 
346
+ posterior = []
347
+
348
+ for i in range(len(X_scaled)):
349
+
350
+ if i > 0 or initial_alpha is not None:
351
+
352
+ alpha = alpha @ transition
353
+
354
+ log_alpha = (
355
+ np.log(
356
+ np.maximum(alpha, 1e-300)
357
+ )
358
+ + log_emissions[i]
359
+ )
360
+
361
+ log_alpha -= np.max(log_alpha)
362
+
363
+ alpha = np.exp(log_alpha)
364
+
365
+ total = alpha.sum()
366
+
367
+ if (
368
+ total <= 0
369
+ or not np.isfinite(total)
370
+ ):
371
+
372
+ alpha = (
373
+ np.ones(n_states) / n_states
374
+ )
375
+
376
+ else:
377
+
378
+ alpha /= total
379
+
380
+ posterior.append(
381
+ alpha.copy()
382
+ )
383
+
384
+ posterior = np.asarray(posterior)
385
+
386
+ return posterior, alpha.copy()
387
+
388
+
389
+ # ============================================================
390
+ # HMM LAYER
391
+ # ============================================================
392
+
393
+ class HMMLayer:
394
+
395
+ def __init__(
396
+ self,
397
+ n_states,
398
+ random_state=42
399
+ ):
400
+
401
+ self.n_states = n_states
402
+
403
+ self.scaler = StandardScaler()
404
 
405
  self.model = GaussianHMM(
406
+ n_components=n_states,
407
  covariance_type="diag",
408
  n_iter=150,
409
  tol=0.001,
410
  min_covar=1e-4,
411
+ random_state=random_state
412
  )
413
 
414
+ self.fitted = False
415
 
416
+ def fit(self, X):
 
 
 
 
 
 
417
 
418
+ X = np.asarray(
419
+ X,
420
+ dtype=float
421
+ )
422
 
423
+ X_scaled = self.scaler.fit_transform(
424
+ X
425
+ )
426
 
427
+ self.model.fit(X_scaled)
428
 
429
+ self.fitted = True
430
 
431
+ return self
432
 
433
+ def filter(
434
+ self,
435
+ X,
436
+ initial_alpha=None
437
+ ):
438
 
439
+ if not self.fitted:
440
+ raise RuntimeError(
441
+ "HMM katmanı eğitilmedi."
 
 
442
  )
 
 
443
 
444
+ X = np.asarray(
445
+ X,
446
+ dtype=float
447
  )
448
 
449
+ X_scaled = self.scaler.transform(
450
+ X
451
+ )
452
 
453
+ return forward_filter(
454
+ self.model,
455
+ X_scaled,
456
+ initial_alpha
457
  )
458
 
 
459
 
460
+ # ============================================================
461
+ # HIERARCHICAL HMM
462
+ # ============================================================
463
 
464
+ class HierarchicalHMM:
 
 
 
 
 
 
 
465
 
466
+ def __init__(self):
 
467
 
468
+ self.macro = HMMLayer(
469
+ N_MACRO,
470
+ random_state=42
471
+ )
472
 
473
+ self.micro = HMMLayer(
474
+ N_MICRO,
475
+ random_state=73
476
+ )
477
 
478
+ self.compatibility = None
479
 
480
+ def fit(self, train_df):
 
 
 
481
 
482
+ macro_X = train_df[
483
+ MACRO_FEATURES
484
+ ].values
485
 
486
+ micro_X = train_df[
487
+ MICRO_FEATURES
488
+ ].values
489
 
490
+ self.macro.fit(macro_X)
491
 
492
+ self.micro.fit(micro_X)
493
 
494
+ macro_post, self.last_macro = (
495
+ self.macro.filter(macro_X)
496
+ )
 
497
 
498
+ micro_post, self.last_micro = (
499
+ self.micro.filter(micro_X)
500
+ )
501
 
502
+ # Macro-micro compatibility matrix.
503
+ # Estimated only from training observations.
504
+ compatibility = np.zeros(
505
+ (N_MACRO, N_MICRO)
506
+ )
507
 
508
+ for m in range(N_MACRO):
509
 
510
+ for u in range(N_MICRO):
511
 
512
+ compatibility[m, u] = (
513
+ np.sum(
514
+ macro_post[:, m]
515
+ * micro_post[:, u]
516
+ )
517
+ + 0.5
518
+ )
519
 
520
+ compatibility /= (
521
+ compatibility.sum(
522
+ axis=1,
523
+ keepdims=True
524
+ )
525
+ )
526
 
527
+ self.compatibility = compatibility
528
 
529
+ train_joint = self.combine(
530
+ macro_post,
531
+ micro_post
532
+ )
533
 
534
+ return train_joint
535
 
536
+ def combine(
537
+ self,
538
+ macro_post,
539
+ micro_post
540
+ ):
541
 
542
+ joint = np.zeros(
543
+ (
544
+ len(macro_post),
545
+ N_MACRO,
546
+ N_MICRO
547
+ )
548
  )
549
 
550
+ for m in range(N_MACRO):
551
 
552
+ weighted_micro = (
553
+ micro_post *
554
+ self.compatibility[m]
555
+ )
556
 
557
+ denominator = (
558
+ weighted_micro.sum(
559
+ axis=1,
560
+ keepdims=True
561
+ )
562
+ )
563
 
564
+ conditional_micro = (
565
+ weighted_micro /
566
+ np.maximum(
567
+ denominator,
568
+ 1e-12
569
+ )
570
+ )
571
 
572
+ joint[:, m, :] = (
573
+ macro_post[:, m, None]
574
+ * conditional_micro
575
+ )
576
 
577
+ joint = joint.reshape(
578
+ len(joint),
579
+ N_JOINT
580
+ )
581
 
582
+ joint /= np.maximum(
583
+ joint.sum(axis=1, keepdims=True),
584
+ 1e-12
585
+ )
586
 
587
+ return joint
588
 
589
+ def predict_future(self, future_df):
590
+
591
+ macro_X = future_df[
592
+ MACRO_FEATURES
593
+ ].values
594
+
595
+ micro_X = future_df[
596
+ MICRO_FEATURES
597
+ ].values
598
 
599
+ macro_post, last_macro = (
600
+ self.macro.filter(
601
+ macro_X,
602
+ initial_alpha=self.last_macro
603
+ )
604
  )
605
 
606
+ micro_post, last_micro = (
607
+ self.micro.filter(
608
+ micro_X,
609
+ initial_alpha=self.last_micro
610
+ )
611
  )
612
 
613
+ joint = self.combine(
614
+ macro_post,
615
+ micro_post
616
  )
617
 
618
+ self.last_macro = last_macro
 
 
619
 
620
+ self.last_micro = last_micro
 
 
 
621
 
622
+ return joint
 
 
 
623
 
624
 
625
  # ============================================================
626
+ # BAYESIAN REGIME MODEL
627
  # ============================================================
628
 
629
+ class BayesianRegimeModel:
630
 
631
+ def __init__(self):
632
 
633
+ self.prior_alpha = 2.0
 
 
634
 
635
+ self.prior_beta = 2.0
 
 
 
636
 
637
+ self.state_probabilities = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
638
 
639
+ def fit(
640
+ self,
641
+ joint_posterior,
642
+ targets
643
+ ):
644
 
645
+ targets = np.asarray(
646
+ targets,
647
+ dtype=float
648
+ )
649
 
650
+ probabilities = []
 
 
 
 
 
 
 
651
 
652
+ for state in range(N_JOINT):
 
653
 
654
+ weights = joint_posterior[:, state]
 
 
 
 
655
 
656
+ positive = np.sum(
657
+ weights * targets
658
+ )
659
 
660
+ negative = np.sum(
661
+ weights * (1 - targets)
662
+ )
663
 
664
+ posterior_up = (
665
+ self.prior_alpha + positive
666
+ ) / (
667
+ self.prior_alpha
668
+ + self.prior_beta
669
+ + positive
670
+ + negative
671
+ )
672
 
673
+ probabilities.append(
674
+ posterior_up
675
+ )
676
 
677
+ self.state_probabilities = np.asarray(
678
+ probabilities
679
+ )
680
 
681
+ return self
 
682
 
683
+ def predict(self, joint_posterior):
684
 
685
+ return (
686
+ joint_posterior
687
+ @ self.state_probabilities
688
+ )
689
 
 
 
 
 
690
 
691
+ # ============================================================
692
+ # COMPLETE MODEL FIT
693
+ # ============================================================
 
694
 
695
+ def fit_predict(
696
+ train_df,
697
+ future_df
698
+ ):
699
 
700
+ if len(train_df) < MIN_TRAIN_ROWS:
701
+ raise ValueError(
702
+ "HMM eğitimi için yeterli veri yok."
703
+ )
704
 
705
+ hierarchy = HierarchicalHMM()
706
 
707
+ train_joint = hierarchy.fit(
708
+ train_df
709
+ )
710
 
711
+ bayesian = BayesianRegimeModel()
712
 
713
+ bayesian.fit(
714
+ train_joint,
715
+ train_df["target"].values
716
+ )
717
 
718
+ future_joint = hierarchy.predict_future(
719
+ future_df
720
+ )
721
 
722
+ probabilities = bayesian.predict(
723
+ future_joint
 
 
724
  )
725
 
726
+ return {
727
+ "probabilities": probabilities,
728
+ "joint": future_joint,
729
+ "macro_micro_compatibility": hierarchy.compatibility,
730
+ "state_probabilities": bayesian.state_probabilities,
731
+ "hierarchy": hierarchy
732
+ }
733
+
734
 
735
  # ============================================================
736
+ # BACKTEST ENGINE
737
  # ============================================================
738
 
739
  def simulate_trades(
740
  raw,
741
  dataset,
742
+ indices,
743
  probabilities,
744
  threshold,
745
  stop_multiplier,
746
  horizon,
747
  fee_rate,
748
+ slippage_bps
 
749
  ):
750
 
751
  trades = []
752
 
753
+ if len(indices) == 0:
754
  return trades
755
 
756
+ last_raw_i = int(
757
+ dataset.iloc[indices[-1]]["raw_i"]
758
  )
759
 
760
  cost = (
 
762
  + 2 * slippage_bps / 10000
763
  )
764
 
765
+ last_exit_i = -1
766
 
767
+ for row_i, probability in zip(
768
+ indices,
769
  probabilities
770
  ):
771
 
772
+ row = dataset.iloc[row_i]
773
 
774
+ signal_raw_i = int(
775
+ row["raw_i"]
776
+ )
777
 
778
+ entry_i = signal_raw_i + 1
779
 
780
+ exit_limit = (
781
+ entry_i + int(horizon) - 1
782
+ )
783
 
784
+ if exit_limit > last_raw_i:
785
  continue
786
 
787
  if exit_limit >= len(raw):
788
  continue
789
 
790
+ if entry_i <= last_exit_i:
 
791
  continue
792
 
793
  if probability >= threshold:
 
802
 
803
  continue
804
 
 
 
 
 
 
 
 
 
 
 
 
 
805
  entry_price = float(
806
  raw.iloc[entry_i]["open"]
807
  )
 
810
  row["realized_volatility"]
811
  )
812
 
813
+ if (
814
+ not np.isfinite(volatility)
815
+ or volatility <= 0
816
+ ):
817
  continue
818
 
 
 
 
 
 
 
819
  risk_pct = np.clip(
820
+ volatility
821
+ * np.sqrt(horizon)
822
+ * stop_multiplier,
823
  0.002,
824
  0.05
825
  )
826
 
827
+ distance = (
828
+ entry_price * risk_pct
829
+ )
830
 
831
  if direction == 1:
832
 
 
841
  target = entry_price - 2 * distance
842
 
843
  exit_price = None
844
+
845
  reason = "TIME"
846
 
847
+ for j in range(
848
+ entry_i,
849
+ exit_limit + 1
850
+ ):
851
 
852
  candle = raw.iloc[j]
853
 
854
  high = float(candle["high"])
855
+
856
  low = float(candle["low"])
857
 
858
  if direction == 1:
859
 
860
  hit_stop = low <= stop
861
+
862
  hit_target = high >= target
863
 
864
  else:
865
 
866
  hit_stop = high >= stop
867
+
868
  hit_target = low <= target
869
 
870
+ # Conservative same-candle rule.
871
  if hit_stop:
872
 
873
  exit_price = stop
874
+
875
  reason = "STOP"
876
+
877
  break
878
 
879
  if hit_target:
880
 
881
  exit_price = target
882
+
883
  reason = "TARGET"
884
+
885
  break
886
 
887
  if exit_price is None:
 
892
 
893
  if direction == 1:
894
 
895
+ gross = (
896
+ exit_price / entry_price - 1
897
+ )
898
 
899
  else:
900
 
901
+ gross = (
902
+ entry_price / exit_price - 1
903
+ )
904
 
905
  net = gross - cost
906
 
907
  trades.append({
908
+ "direction": (
909
+ "LONG" if direction == 1
910
+ else "SHORT"
911
+ ),
912
  "entry": entry_price,
913
  "exit": exit_price,
914
  "probability": float(probability),
 
 
915
  "reason": reason,
916
  "gross_return": gross,
917
  "net_return": net
918
  })
919
 
920
+ last_exit_i = exit_limit
921
 
922
  return trades
923
 
 
932
 
933
  return {
934
  "trades": 0,
935
+ "win_rate": 0.0,
936
+ "profit_factor": 0.0,
937
+ "net_return": 0.0,
938
+ "max_drawdown": 0.0
939
  }
940
 
941
+ returns = np.asarray(
942
+ [t["net_return"] for t in trades],
943
+ dtype=float
944
+ )
945
 
946
  wins = returns[returns > 0]
947
 
 
949
 
950
  if len(losses):
951
 
952
+ pf = (
953
+ wins.sum()
954
+ / abs(losses.sum())
955
+ )
956
 
957
  elif len(wins):
958
 
959
+ pf = 999.0
960
 
961
  else:
962
 
963
  pf = 0.0
964
 
965
+ equity = np.cumprod(
966
+ 1 + returns
967
+ )
968
 
969
  peaks = np.maximum.accumulate(
970
+ np.concatenate([
971
+ [1.0],
972
+ equity
973
+ ])
974
  )[1:]
975
 
976
+ drawdown = (
977
+ 1 - equity / peaks
978
+ )
979
 
980
  return {
981
  "trades": len(trades),
982
+ "win_rate": float(
983
+ np.mean(returns > 0)
984
+ ),
985
+ "profit_factor": float(pf),
986
+ "net_return": float(
987
+ equity[-1] - 1
988
+ ),
989
+ "max_drawdown": float(
990
+ np.max(drawdown)
991
+ )
992
  }
993
 
994
 
995
  # ============================================================
996
+ # OPTUNA DEVELOPMENT
997
  # ============================================================
998
 
999
+ def optimize_parameters(
1000
  dataset,
1001
  raw,
1002
  horizon,
 
 
1003
  fee_rate,
1004
  slippage_bps,
1005
+ trials
1006
  ):
1007
 
1008
+ n = len(dataset)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1009
 
1010
+ dev_end = int(n * 0.60)
 
 
 
 
1011
 
1012
+ development = dataset.iloc[
1013
+ :dev_end
1014
+ ].reset_index(drop=True)
1015
 
1016
+ if len(development) < MIN_TRAIN_ROWS + 150:
1017
+ raise ValueError(
1018
+ "Optimizasyon için daha fazla veri gerekli."
 
 
 
 
 
 
 
 
1019
  )
1020
 
1021
+ # Three chronological development folds.
1022
+ boundaries = np.linspace(
1023
+ int(len(development) * 0.50),
1024
+ len(development),
1025
+ 4,
1026
+ dtype=int
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1027
  )
1028
 
1029
  def objective(trial):
1030
 
1031
  threshold = trial.suggest_float(
1032
  "threshold",
1033
+ 0.54,
1034
+ 0.72
1035
  )
1036
 
1037
  stop_multiplier = trial.suggest_float(
1038
  "stop_multiplier",
1039
+ 0.8,
1040
+ 2.2
1041
  )
1042
 
1043
+ fold_scores = []
 
 
 
1044
 
1045
+ total_trades = 0
1046
 
1047
+ for k in range(3):
1048
 
1049
+ val_start = boundaries[k]
1050
+
1051
+ val_end = boundaries[k + 1]
1052
+
1053
+ train_end = val_start - horizon
1054
+
1055
+ if train_end < MIN_TRAIN_ROWS:
1056
+ continue
1057
 
1058
+ train_df = development.iloc[
1059
+ :train_end
1060
  ].reset_index(drop=True)
1061
 
1062
+ # Includes the purge/gap observations.
1063
+ future_df = development.iloc[
1064
+ train_end:val_end
1065
+ ].reset_index(drop=True)
1066
 
1067
+ result = fit_predict(
1068
  train_df,
1069
+ future_df
1070
+ )
1071
+
1072
+ offset = val_start - train_end
1073
+
1074
+ val_probabilities = (
1075
+ result["probabilities"][offset:]
1076
+ )
1077
+
1078
+ # Map development-local indices to original dataset.
1079
+ global_start = val_start
1080
+
1081
+ global_end = val_end
1082
+
1083
+ indices = np.arange(
1084
+ global_start,
1085
+ global_end
1086
  )
1087
 
1088
  trades = simulate_trades(
1089
  raw,
1090
  dataset,
1091
+ indices,
1092
+ val_probabilities,
1093
  threshold,
1094
  stop_multiplier,
1095
  horizon,
1096
  fee_rate,
1097
+ slippage_bps
1098
+ )
1099
+
1100
+ metrics = calculate_metrics(
1101
+ trades
1102
  )
1103
 
1104
+ total_trades += metrics["trades"]
1105
 
1106
  score = (
1107
  metrics["net_return"] * 100
1108
+ + min(metrics["profit_factor"], 4) * 1.5
1109
+ + metrics["win_rate"] * 2
1110
+ - metrics["max_drawdown"] * 20
1111
  )
1112
 
1113
+ fold_scores.append(score)
 
1114
 
1115
+ if not fold_scores:
1116
+ return -100.0
1117
 
1118
+ # Avoid selecting a model that makes almost no trades.
1119
+ if total_trades < 8:
1120
+ return (
1121
+ float(np.mean(fold_scores))
1122
+ - (8 - total_trades) * 2
1123
+ )
1124
+
1125
+ return float(
1126
+ np.mean(fold_scores)
1127
+ )
1128
 
1129
  study = optuna.create_study(
1130
  direction="maximize",
 
1135
 
1136
  study.optimize(
1137
  objective,
1138
+ n_trials=int(trials),
1139
  show_progress_bar=False
1140
  )
1141
 
 
1143
 
1144
 
1145
  # ============================================================
1146
+ # WALK-FORWARD EVALUATION
1147
  # ============================================================
1148
 
1149
+ def walk_forward_evaluation(
1150
  dataset,
1151
  raw,
1152
  horizon,
1153
+ params,
 
1154
  fee_rate,
1155
+ slippage_bps
1156
+ ):
1157
+
1158
+ n = len(dataset)
1159
+
1160
+ start = int(n * 0.60)
1161
+
1162
+ end = int(n * 0.80)
1163
+
1164
+ boundaries = np.linspace(
1165
+ start,
1166
+ end,
1167
+ 5,
1168
+ dtype=int
1169
+ )
1170
+
1171
+ all_trades = []
1172
+
1173
+ fold_reports = []
1174
+
1175
+ for fold in range(4):
1176
+
1177
+ val_start = boundaries[fold]
1178
+
1179
+ val_end = boundaries[fold + 1]
1180
+
1181
+ train_end = val_start - horizon
1182
+
1183
+ train_df = dataset.iloc[
1184
+ :train_end
1185
+ ].reset_index(drop=True)
1186
+
1187
+ future_df = dataset.iloc[
1188
+ train_end:val_end
1189
+ ].reset_index(drop=True)
1190
+
1191
+ result = fit_predict(
1192
+ train_df,
1193
+ future_df
1194
+ )
1195
+
1196
+ offset = val_start - train_end
1197
+
1198
+ probabilities = (
1199
+ result["probabilities"][offset:]
1200
+ )
1201
+
1202
+ indices = np.arange(
1203
+ val_start,
1204
+ val_end
1205
+ )
1206
+
1207
+ trades = simulate_trades(
1208
+ raw,
1209
+ dataset,
1210
+ indices,
1211
+ probabilities,
1212
+ params["threshold"],
1213
+ params["stop_multiplier"],
1214
+ horizon,
1215
+ fee_rate,
1216
+ slippage_bps
1217
+ )
1218
+
1219
+ metrics = calculate_metrics(
1220
+ trades
1221
+ )
1222
+
1223
+ fold_reports.append({
1224
+ "fold": fold + 1,
1225
+ **metrics
1226
+ })
1227
+
1228
+ all_trades.extend(trades)
1229
+
1230
+ return (
1231
+ calculate_metrics(all_trades),
1232
+ fold_reports
1233
+ )
1234
+
1235
+
1236
+ # ============================================================
1237
+ # INDEPENDENT HOLDOUT
1238
+ # ============================================================
1239
+
1240
+ def independent_holdout(
1241
+ dataset,
1242
+ raw,
1243
+ horizon,
1244
+ params,
1245
+ fee_rate,
1246
+ slippage_bps
1247
  ):
1248
 
1249
  n = len(dataset)
 
1256
  :train_end
1257
  ].reset_index(drop=True)
1258
 
1259
+ # Includes purge observations before holdout.
1260
+ future_df = dataset.iloc[
1261
+ train_end:n
1262
  ].reset_index(drop=True)
1263
 
1264
+ result = fit_predict(
 
 
1265
  train_df,
1266
+ future_df
1267
+ )
1268
+
1269
+ offset = split - train_end
1270
+
1271
+ probabilities = (
1272
+ result["probabilities"][offset:]
1273
  )
1274
 
1275
+ indices = np.arange(
1276
  split,
1277
  n
1278
  )
 
1280
  trades = simulate_trades(
1281
  raw,
1282
  dataset,
1283
+ indices,
1284
+ probabilities,
1285
+ params["threshold"],
1286
+ params["stop_multiplier"],
1287
  horizon,
1288
  fee_rate,
1289
+ slippage_bps
 
1290
  )
1291
 
1292
+ return (
1293
+ calculate_metrics(trades),
1294
+ trades
1295
+ )
1296
 
1297
 
1298
  # ============================================================
1299
+ # OVERFIT DIAGNOSTIC
1300
  # ============================================================
1301
 
1302
+ def overfit_diagnostic(wf, holdout):
1303
 
1304
+ if (
1305
+ wf["trades"] < MIN_RELIABLE_TRADES
1306
+ or holdout["trades"] < MIN_RELIABLE_TRADES
1307
+ ):
1308
 
1309
+ return (
1310
+ None,
1311
+ "YETERSİZ VERİ",
1312
+ "Her iki test bölümünde de en az 30 işlem gerekli."
1313
+ )
1314
+
1315
+ win_gap = abs(
1316
  wf["win_rate"] - holdout["win_rate"]
1317
  )
1318
 
1319
+ pf_gap = abs(
1320
+ min(wf["profit_factor"], 5)
1321
+ - min(holdout["profit_factor"], 5)
1322
+ ) / 5
1323
+
1324
+ return_gap = abs(
1325
+ wf["net_return"] - holdout["net_return"]
1326
  )
1327
 
1328
+ score = (
1329
+ win_gap * 40
1330
+ + pf_gap * 35
1331
+ + min(return_gap, 1) * 25
1332
  )
1333
 
1334
  score = float(
1335
  np.clip(score, 0, 100)
1336
  )
1337
 
1338
+ if score < 20:
1339
+ label = "DÜŞÜK FARK"
1340
 
1341
+ elif score < 40:
1342
+ label = "ORTA FARK"
1343
 
1344
+ elif score < 65:
1345
+ label = "YÜKSEK FARK"
1346
 
1347
  else:
1348
+ label = "ÇOK YÜKSEK FARK"
1349
 
1350
+ return (
1351
+ score,
1352
+ label,
1353
+ "Bu değer gerçek overfit olasılığı değil, test bölümleri arasındaki performans farkıdır."
1354
+ )
1355
 
1356
 
1357
  # ============================================================
 
1360
 
1361
  def live_prediction(
1362
  dataset,
1363
+ live,
1364
  horizon,
1365
  params
1366
  ):
1367
 
1368
+ train_df = dataset.copy()
1369
 
1370
+ train_end_raw_i = int(
1371
+ train_df["raw_i"].iloc[-1]
 
 
 
 
 
1372
  )
1373
 
1374
+ future_df = live[
1375
+ live["raw_i"] > train_end_raw_i
1376
+ ].copy()
 
 
 
 
 
 
1377
 
1378
+ if future_df.empty:
1379
+ raise ValueError(
1380
+ "Canlı tahmin için yeni tamamlanmış mum bulunamadı."
1381
+ )
1382
 
1383
+ future_df = future_df.reset_index(
1384
+ drop=True
1385
+ )
1386
 
1387
+ result = fit_predict(
1388
+ train_df,
1389
+ future_df
1390
  )
1391
 
1392
+ probability = float(
1393
+ result["probabilities"][-1]
 
 
1394
  )
1395
 
1396
+ threshold = params["threshold"]
1397
+
1398
+ if probability >= threshold:
 
1399
 
1400
  signal = "LONG"
1401
 
1402
+ elif probability <= 1 - threshold:
 
 
 
1403
 
1404
  signal = "SHORT"
1405
 
 
1407
 
1408
  signal = "NO TRADE"
1409
 
1410
+ latest = future_df.iloc[-1]
1411
+
1412
  price = float(
1413
+ latest["close"]
1414
  )
1415
 
1416
  volatility = float(
1417
+ latest["realized_volatility"]
1418
  )
1419
 
1420
  risk_pct = np.clip(
1421
+ volatility
1422
+ * np.sqrt(horizon)
1423
+ * params["stop_multiplier"],
1424
  0.002,
1425
  0.05
1426
  )
 
1430
  if signal == "LONG":
1431
 
1432
  stop = price - distance
1433
+
1434
  target = price + 2 * distance
1435
 
1436
  elif signal == "SHORT":
1437
 
1438
  stop = price + distance
1439
+
1440
  target = price - 2 * distance
1441
 
1442
  else:
1443
 
1444
  stop = None
1445
+
1446
  target = None
1447
 
1448
+ joint = result["joint"][-1]
1449
+
1450
+ macro_prob = joint.reshape(
1451
+ N_MACRO,
1452
+ N_MICRO
1453
+ ).sum(axis=1)
1454
+
1455
+ micro_prob = joint.reshape(
1456
+ N_MACRO,
1457
+ N_MICRO
1458
+ ).sum(axis=0)
1459
+
1460
  return {
1461
  "signal": signal,
1462
+ "long_probability": probability,
1463
+ "short_probability": 1 - probability,
1464
  "price": price,
1465
  "stop": stop,
1466
  "target": target,
1467
  "risk_pct": risk_pct,
1468
+ "macro": macro_prob,
1469
+ "micro": micro_prob,
1470
+ "joint": joint,
1471
+ "state_probabilities": result[
1472
+ "state_probabilities"
1473
+ ]
1474
  }
1475
 
1476
 
1477
  # ============================================================
1478
+ # MAIN APPLICATION
1479
  # ============================================================
1480
 
1481
  def run_app(
 
1492
  optuna.logging.WARNING
1493
  )
1494
 
1495
+ fee_rate = (
1496
+ float(fee_percent) / 100
1497
+ )
1498
 
1499
  raw = fetch_data(
1500
  int(candle_count)
 
1505
  int(horizon)
1506
  )
1507
 
1508
+ # 1. Optimize only on first 60%.
1509
+ params, best_score = optimize_parameters(
1510
+ dataset,
 
 
 
 
 
 
 
 
 
 
 
1511
  raw,
1512
  int(horizon),
1513
  fee_rate,
 
1515
  int(trials)
1516
  )
1517
 
1518
+ # 2. Walk-forward: 60%-80%.
1519
+ wf, fold_reports = walk_forward_evaluation(
1520
+ dataset,
1521
  raw,
1522
  int(horizon),
1523
+ params,
 
1524
  fee_rate,
1525
+ float(slippage_bps)
 
1526
  )
1527
 
1528
+ # 3. Independent holdout: last 20%.
1529
+ holdout, holdout_trades = independent_holdout(
1530
  dataset,
1531
  raw,
1532
  int(horizon),
1533
+ params,
 
1534
  fee_rate,
1535
+ float(slippage_bps)
 
1536
  )
1537
 
1538
+ # 4. Overfit diagnostic.
1539
+ overfit_score, overfit_label, overfit_note = (
1540
+ overfit_diagnostic(
1541
+ wf,
1542
+ holdout
1543
+ )
1544
  )
1545
 
1546
+ # 5. Live model.
1547
  live_result = live_prediction(
1548
  dataset,
1549
+ live,
1550
  int(horizon),
1551
+ params
1552
  )
1553
 
1554
+ last_timestamp = pd.to_datetime(
1555
  int(raw.iloc[-1]["timestamp"]),
1556
  unit="ms",
1557
  utc=True
1558
  )
1559
 
1560
+ macro = live_result["macro"]
1561
 
1562
+ micro = live_result["micro"]
1563
 
1564
+ state_probs = live_result[
1565
+ "state_probabilities"
1566
+ ]
1567
 
1568
+ joint = live_result["joint"]
1569
 
1570
+ def pct(x):
1571
+ return f"%{100*x:.2f}"
1572
 
1573
+ def price_text(x):
1574
+ if x is None:
1575
+ return "Yok"
1576
+ return f"{x:,.2f} USDT"
1577
+
1578
+ report = f"""
1579
+ # OKX BTC/USDT — HIERARCHICAL QUANT
 
1580
 
1581
+ **Model:** Hierarchical HMM + Bayesian Regime Model
1582
+
1583
+ ## Canlı tahmin
1584
 
1585
  # {live_result['signal']}
1586
 
1587
  | Metrik | Sonuç |
1588
  |---|---:|
1589
+ | BTC fiyatı | {price_text(live_result['price'])} |
1590
+ | LONG olasılığı | {pct(live_result['long_probability'])} |
1591
+ | SHORT olasılığı | {pct(live_result['short_probability'])} |
1592
+ | Stop-loss | {price_text(live_result['stop'])} |
1593
+ | 2R hedef | {price_text(live_result['target'])} |
1594
+ | Risk mesafesi | {pct(live_result['risk_pct'])} |
1595
+ | Son mum | {last_timestamp} |
1596
+
1597
+ ## Üst katman — Macro HMM
1598
+
1599
+ | Gizli durum | Olasılık |
 
 
1600
  |---|---:|
1601
+ | Macro 1 | {pct(macro[0])} |
1602
+ | Macro 2 | {pct(macro[1])} |
1603
+ | Macro 3 | {pct(macro[2])} |
1604
 
1605
+ Bu durumların sırası her eğitimde değişebilir. Model bunları önceden bullish/bearish diye etiketlemez.
1606
 
1607
+ ## Alt katman — Micro HMM
1608
+
1609
+ | Gizli durum | Olasılık |
1610
  |---|---:|
1611
+ | Micro 1 | {pct(micro[0])} |
1612
+ | Micro 2 | {pct(micro[1])} |
1613
+
1614
+ ## Birleşik altı rejim
1615
+
1616
+ | Rejim | Olasılık | Bayesian LONG |
1617
+ |---|---:|---:|
1618
+ | M1-U1 | {pct(joint[0])} | {pct(state_probs[0])} |
1619
+ | M1-U2 | {pct(joint[1])} | {pct(state_probs[1])} |
1620
+ | M2-U1 | {pct(joint[2])} | {pct(state_probs[2])} |
1621
+ | M2-U2 | {pct(joint[3])} | {pct(state_probs[3])} |
1622
+ | M3-U1 | {pct(joint[4])} | {pct(state_probs[4])} |
1623
+ | M3-U2 | {pct(joint[5])} | {pct(state_probs[5])} |
1624
 
1625
+ ## Walk-forward — %60 ile %80 arası
1626
 
1627
  | Metrik | Sonuç |
1628
  |---|---:|
1629
  | İşlem sayısı | {wf['trades']} |
1630
+ | Win rate | {pct(wf['win_rate'])} |
1631
  | Profit Factor | {wf['profit_factor']:.2f} |
1632
+ | Net getiri | {pct(wf['net_return'])} |
1633
+ | Max drawdown | {pct(wf['max_drawdown'])} |
1634
 
1635
+ ## Bağımsız holdout — Son %20
1636
 
1637
  | Metrik | Sonuç |
1638
  |---|---:|
1639
  | İşlem sayısı | {holdout['trades']} |
1640
+ | Win rate | {pct(holdout['win_rate'])} |
1641
  | Profit Factor | {holdout['profit_factor']:.2f} |
1642
+ | Net getiri | {pct(holdout['net_return'])} |
1643
+ | Max drawdown | {pct(holdout['max_drawdown'])} |
1644
 
1645
+ ## Overfit fark göstergesi
1646
 
1647
+ **{overfit_label}**
1648
 
1649
+ {(
1650
+ f"{overfit_score:.1f}/100"
1651
+ if overfit_score is not None
1652
+ else "Hesaplanamadı"
1653
+ )}
1654
+
1655
+ {overfit_note}
1656
 
1657
  ## Optuna parametreleri
1658
 
1659
  | Parametre | Değer |
1660
  |---|---:|
1661
+ | LONG/SHORT eşiği | {params['threshold']:.4f} |
1662
+ | Stop multiplier | {params['stop_multiplier']:.4f} |
1663
+ | Optuna skoru | {best_score:.4f} |
 
1664
 
1665
  ## Walk-forward fold detayları
1666
 
1667
+ | Fold | İşlem | Win rate | PF | Net getiri |
1668
  |---|---:|---:|---:|---:|
1669
  """
1670
 
1671
+ for fold in fold_reports:
1672
 
1673
  report += (
1674
+ f"| {fold['fold']} "
1675
+ f"| {fold['trades']} "
1676
+ f"| {pct(fold['win_rate'])} "
1677
+ f"| {fold['profit_factor']:.2f} "
1678
+ f"| {pct(fold['net_return'])} |\n"
1679
  )
1680
 
1681
  report += """
1682
  ---
1683
 
1684
+ ### Model sınırlamaları
1685
+
1686
+ - Rejimler gizlidir; Macro 1'in daima bullish olması beklenmemelidir.
1687
+ - Forward filtering kullanılır; gelecekteki mumlar canlı rejim olasılığına girmez.
1688
+ - HMM parametreleri her eğitim döneminde yalnızca o dönemin eğitim verisiyle öğrenilir.
1689
+ - Holdout verisi Optuna optimizasyonunda kullanılmaz.
1690
+ - 30'dan az işlem varsa performans istatistikleri güvenilir kabul edilmemelidir.
1691
+ - Backtest funding, likidasyon, gerçek spread ve emir defteri derinliğini tam modellemez.
1692
+ - 2R hedefi ve sonuçlar garanti edilmiş değildir.
1693
  """
1694
 
1695
  return report
 
1697
  except Exception as e:
1698
 
1699
  return (
1700
+ "# UYGULAMA HATASI\n\n"
1701
  f"`{type(e).__name__}: {str(e)}`"
1702
  )
1703
 
1704
 
1705
  # ============================================================
1706
+ # GRADIO UI
1707
  # ============================================================
1708
 
1709
  with gr.Blocks(
1710
+ title="OKX Hierarchical Bayesian Quant",
1711
  theme=gr.themes.Soft()
1712
  ) as demo:
1713
 
1714
  gr.Markdown(
1715
  """
1716
+ # BTC/USDT Hierarchical Bayesian Quant
1717
+
1718
+ **Macro HMM → Micro HMM → 6 latent regimes → Bayesian LONG/SHORT**
1719
 
1720
+ OKX perpetual swap · 1H · Long + Short
1721
 
1722
+ Klasik RSI, MACD, EMA veya Supertrend kullanılmaz.
 
1723
  """
1724
  )
1725
 
1726
  with gr.Row():
1727
 
1728
  candle_count = gr.Slider(
1729
+ minimum=1000,
1730
+ maximum=5000,
1731
  value=3000,
1732
  step=500,
1733
  label="OKX mum sayısı"
1734
  )
1735
 
1736
  horizon = gr.Slider(
1737
+ minimum=2,
1738
+ maximum=24,
1739
  value=6,
1740
  step=1,
1741
  label="Tahmin ufku (saat)"
 
1744
  with gr.Row():
1745
 
1746
  trials = gr.Slider(
1747
+ minimum=5,
1748
+ maximum=40,
1749
+ value=10,
1750
  step=5,
1751
  label="Optuna denemesi"
1752
  )
1753
 
1754
  fee_percent = gr.Slider(
1755
+ minimum=0,
1756
+ maximum=0.20,
1757
  value=0.05,
1758
  step=0.01,
1759
  label="Komisyon % / taraf"
1760
  )
1761
 
1762
  slippage_bps = gr.Slider(
1763
+ minimum=0,
1764
+ maximum=20,
1765
  value=2,
1766
  step=1,
1767
  label="Kayma baz puan / taraf"
1768
  )
1769
 
1770
  run_button = gr.Button(
1771
+ "HIERARCHICAL QUANT ANALİZİ",
1772
  variant="primary"
1773
  )
1774