GoshawkVortexAI commited on
Commit
3aa85b1
·
verified ·
1 Parent(s): fc0e195

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +563 -385
app.py CHANGED
@@ -5,8 +5,9 @@ warnings.filterwarnings("ignore")
5
  import time
6
  import numpy as np
7
  import pandas as pd
8
- import ccxt
9
  import gradio as gr
 
 
10
 
11
  from sklearn.ensemble import (
12
  ExtraTreesClassifier,
@@ -27,7 +28,7 @@ from sklearn.metrics import (
27
  # CONFIG
28
  # =========================================================
29
 
30
- SYMBOL = "BTC/USDT"
31
 
32
  FEATURES = [
33
  "rsi",
@@ -49,25 +50,39 @@ FEATURES = [
49
 
50
 
51
  # =========================================================
52
- # BINANCE DATA
53
  # =========================================================
54
 
55
  def fetch_data(timeframe="1h", candles=2000):
56
 
57
- exchange = ccxt.binance({
58
  "enableRateLimit": True,
59
- "timeout": 20000
 
 
 
60
  })
61
 
 
 
 
 
 
 
 
62
  timeframe_ms = exchange.parse_timeframe(timeframe) * 1000
63
 
64
- since = exchange.milliseconds() - candles * timeframe_ms
 
 
 
 
65
  all_rows = []
 
66
 
67
  while len(all_rows) < candles:
68
 
69
- remaining = candles - len(all_rows)
70
- limit = min(1000, remaining)
71
 
72
  batch = exchange.fetch_ohlcv(
73
  SYMBOL,
@@ -79,10 +94,13 @@ def fetch_data(timeframe="1h", candles=2000):
79
  if not batch:
80
  break
81
 
82
- all_rows.extend(batch)
83
 
84
- last_timestamp = batch[-1][0]
85
- new_since = last_timestamp + timeframe_ms
 
 
 
86
 
87
  if new_since <= since:
88
  break
@@ -95,7 +113,7 @@ def fetch_data(timeframe="1h", candles=2000):
95
  time.sleep(exchange.rateLimit / 1000)
96
 
97
  if not all_rows:
98
- raise RuntimeError("Binance veri döndürmedi.")
99
 
100
  df = pd.DataFrame(
101
  all_rows,
@@ -120,30 +138,36 @@ def fetch_data(timeframe="1h", candles=2000):
120
 
121
  df = df.set_index("timestamp")
122
 
123
- for col in ["open", "high", "low", "close", "volume"]:
124
- df[col] = pd.to_numeric(df[col], errors="coerce")
 
 
 
 
 
 
 
 
 
125
 
126
  df = df.dropna()
127
 
128
- # Son mum henüz tamamlanmamış olabilir.
129
- if len(df) > 2:
130
  df = df.iloc[:-1]
131
 
132
  return df.tail(candles)
133
 
134
 
135
  # =========================================================
136
- # INDICATOR CALCULATIONS
137
  # =========================================================
138
 
139
  def calculate_indicators(df):
140
 
141
  df = df.copy()
142
 
143
- # -----------------------------------------------------
144
- # ATR - Wilder RMA
145
- # -----------------------------------------------------
146
-
147
  previous_close = df["close"].shift(1)
148
 
149
  tr = pd.concat([
@@ -158,10 +182,7 @@ def calculate_indicators(df):
158
  min_periods=10
159
  ).mean()
160
 
161
- # -----------------------------------------------------
162
  # RSI 14
163
- # -----------------------------------------------------
164
-
165
  delta = df["close"].diff()
166
 
167
  gain = delta.clip(lower=0)
@@ -181,12 +202,9 @@ def calculate_indicators(df):
181
 
182
  rs = avg_gain / avg_loss.replace(0, np.nan)
183
 
184
- df["rsi"] = 100 - (100 / (1 + rs))
185
 
186
- # -----------------------------------------------------
187
  # MACD 12,26,9
188
- # -----------------------------------------------------
189
-
190
  ema12 = df["close"].ewm(
191
  span=12,
192
  adjust=False
@@ -208,97 +226,87 @@ def calculate_indicators(df):
208
  df["macd"] - df["macd_signal"]
209
  )
210
 
211
- # -----------------------------------------------------
212
  # SUPERTREND 10,3
213
- # -----------------------------------------------------
214
-
215
- multiplier = 3.0
216
-
217
  hl2 = (df["high"] + df["low"]) / 2
218
 
219
- basic_upper = hl2 + multiplier * df["atr"]
220
- basic_lower = hl2 - multiplier * df["atr"]
221
 
222
  final_upper = basic_upper.copy()
223
  final_lower = basic_lower.copy()
224
 
225
- supertrend = pd.Series(
226
- np.nan,
227
  index=df.index
228
  )
229
 
230
- direction = pd.Series(
231
- 1,
232
- index=df.index,
233
- dtype=float
234
  )
235
 
236
  for i in range(1, len(df)):
237
 
238
- previous_upper = final_upper.iloc[i - 1]
239
- previous_lower = final_lower.iloc[i - 1]
240
  previous_close_value = df["close"].iloc[i - 1]
241
 
242
- current_upper = basic_upper.iloc[i]
243
- current_lower = basic_lower.iloc[i]
244
-
245
  if (
246
- current_upper < previous_upper
247
- or previous_close_value > previous_upper
248
  ):
249
- final_upper.iloc[i] = current_upper
250
  else:
251
- final_upper.iloc[i] = previous_upper
252
 
253
  if (
254
- current_lower > previous_lower
255
- or previous_close_value < previous_lower
256
  ):
257
- final_lower.iloc[i] = current_lower
258
  else:
259
- final_lower.iloc[i] = previous_lower
260
 
261
- previous_direction = direction.iloc[i - 1]
262
  current_close = df["close"].iloc[i]
263
 
264
- if previous_direction == -1:
265
 
266
- if current_close > final_upper.iloc[i]:
267
- direction.iloc[i] = 1
268
- else:
269
- direction.iloc[i] = -1
270
 
271
  else:
272
 
273
- if current_close < final_lower.iloc[i]:
274
- direction.iloc[i] = -1
275
- else:
276
- direction.iloc[i] = 1
277
 
278
- if direction.iloc[i] == 1:
279
- supertrend.iloc[i] = final_lower.iloc[i]
280
- else:
281
- supertrend.iloc[i] = final_upper.iloc[i]
 
282
 
283
  df["supertrend"] = supertrend
284
  df["supertrend_direction"] = direction
285
 
286
- # -----------------------------------------------------
287
- # VWAP - UTC DAILY RESET
288
- # -----------------------------------------------------
289
-
290
  typical_price = (
291
  df["high"] + df["low"] + df["close"]
292
  ) / 3
293
 
294
  daily_group = df.index.date
295
 
296
- pv = typical_price * df["volume"]
297
-
298
- cumulative_pv = pv.groupby(daily_group).cumsum()
 
 
299
 
300
  cumulative_volume = (
301
- df["volume"].groupby(daily_group).cumsum()
 
 
302
  )
303
 
304
  df["vwap"] = (
@@ -306,10 +314,7 @@ def calculate_indicators(df):
306
  cumulative_volume.replace(0, np.nan)
307
  )
308
 
309
- # -----------------------------------------------------
310
- # ADDITIONAL FEATURES
311
- # -----------------------------------------------------
312
-
313
  df["macd_pct"] = df["macd"] / df["close"] * 100
314
 
315
  df["macd_signal_pct"] = (
@@ -321,12 +326,14 @@ def calculate_indicators(df):
321
  )
322
 
323
  df["supertrend_distance"] = (
324
- df["close"] - df["supertrend"]
325
- ) / df["close"] * 100
 
326
 
327
  df["vwap_distance"] = (
328
- df["close"] - df["vwap"]
329
- ) / df["close"] * 100
 
330
 
331
  df["atr_pct"] = df["atr"] / df["close"] * 100
332
 
@@ -338,31 +345,33 @@ def calculate_indicators(df):
338
  df["return_1"].rolling(14).std()
339
  )
340
 
341
- volume_average = df["volume"].rolling(20).mean()
342
 
343
  df["volume_ratio"] = (
344
  df["volume"] /
345
- volume_average.replace(0, np.nan)
346
  )
347
 
348
  df["body_pct"] = (
349
- df["close"] - df["open"]
350
- ) / df["open"] * 100
 
351
 
352
  df["high_low_pct"] = (
353
- df["high"] - df["low"]
354
- ) / df["close"] * 100
 
355
 
356
  return df
357
 
358
 
359
  # =========================================================
360
- # TARGET
361
  # =========================================================
362
 
363
- def prepare_dataset(df, horizon):
364
 
365
- data = calculate_indicators(df)
366
 
367
  future_close = data["close"].shift(-horizon)
368
 
@@ -370,7 +379,7 @@ def prepare_dataset(df, horizon):
370
  future_close > data["close"]
371
  ).astype(int)
372
 
373
- # Son horizon mumunda gelecek hedefi bilinmez.
374
  data = data.iloc[:-horizon].copy()
375
 
376
  data = data.replace(
@@ -384,133 +393,71 @@ def prepare_dataset(df, horizon):
384
 
385
  if len(data) < 500:
386
  raise ValueError(
387
- "Yeterli veri yok. Mum sayısını artır."
388
  )
389
 
390
  return data
391
 
392
 
393
  # =========================================================
394
- # MODELS
395
  # =========================================================
396
 
397
- def create_models():
398
 
399
- models = {
400
-
401
- "HistGradientBoosting":
402
- HistGradientBoostingClassifier(
403
- max_iter=180,
404
- learning_rate=0.05,
405
- max_leaf_nodes=15,
406
- l2_regularization=2.0,
407
- random_state=42
408
- ),
409
-
410
- "ExtraTrees":
411
- ExtraTreesClassifier(
412
- n_estimators=300,
413
- max_depth=8,
414
- min_samples_leaf=5,
415
- max_features=0.8,
416
- class_weight="balanced",
417
- random_state=42,
418
- n_jobs=-1
419
- ),
420
 
421
- "LogisticRegression":
422
- Pipeline([
423
- ("scaler", StandardScaler()),
424
- ("model", LogisticRegression(
425
- C=0.2,
426
- max_iter=2000,
427
- class_weight="balanced",
428
- random_state=42
429
- ))
430
- ])
431
- }
432
 
433
- return models
 
 
 
 
 
 
 
 
434
 
 
435
 
436
- def ensemble_predict(X_train, y_train, X_test):
437
 
438
- models = create_models()
 
 
 
 
 
439
 
440
  probabilities = []
441
 
442
- for name, model in models.items():
443
 
444
  model.fit(X_train, y_train)
445
 
446
- probability = model.predict_proba(X_test)[:, 1]
447
-
448
- probabilities.append(probability)
449
-
450
- ensemble_probability = np.mean(
451
- probabilities,
452
- axis=0
453
- )
454
-
455
- return ensemble_probability
456
-
457
-
458
- # =========================================================
459
- # WALK FORWARD VALIDATION
460
- # =========================================================
461
-
462
- def walk_forward_validation(X, y):
463
-
464
- splitter = TimeSeriesSplit(n_splits=4)
465
-
466
- scores = []
467
- fold_results = []
468
-
469
- for fold, (train_idx, test_idx) in enumerate(
470
- splitter.split(X),
471
- start=1
472
- ):
473
-
474
- X_train = X.iloc[train_idx]
475
- X_test = X.iloc[test_idx]
476
-
477
- y_train = y.iloc[train_idx]
478
- y_test = y.iloc[test_idx]
479
-
480
- if y_train.nunique() < 2:
481
- continue
482
 
483
- probabilities = ensemble_predict(
484
- X_train,
485
- y_train,
486
- X_test
487
- )
488
 
489
- predictions = (
490
- probabilities >= 0.5
491
- ).astype(int)
492
-
493
- score = balanced_accuracy_score(
494
- y_test,
495
- predictions
496
- )
497
-
498
- scores.append(score)
499
-
500
- fold_results.append(
501
- f"Fold {fold}: {score * 100:.2f}%"
502
- )
503
-
504
- if not scores:
505
- raise ValueError(
506
- "Walk-forward validation yapılamadı."
507
- )
508
-
509
- return np.mean(scores), fold_results
510
 
511
 
512
  # =========================================================
513
- # BACKTEST
514
  # =========================================================
515
 
516
  def run_backtest(
@@ -519,30 +466,31 @@ def run_backtest(
519
  probabilities,
520
  horizon,
521
  threshold,
 
522
  fee_pct
523
  ):
524
 
525
  trades = []
526
 
527
- next_allowed_bar = -1
528
-
529
  fee_rate = fee_pct / 100
530
 
531
- for timestamp, row in test.iterrows():
 
 
 
 
532
 
533
- raw_position = raw.index.get_loc(timestamp)
534
 
535
- if raw_position < next_allowed_bar:
536
  continue
537
 
538
- probability = float(
539
- probabilities[test.index.get_loc(timestamp)]
540
- )
541
 
542
  if probability >= threshold:
543
  side = "LONG"
544
 
545
- elif probability <= (1 - threshold):
546
  side = "SHORT"
547
 
548
  else:
@@ -554,21 +502,18 @@ def run_backtest(
554
  if not np.isfinite(atr) or atr <= 0:
555
  continue
556
 
557
- risk_distance = atr * 1.5
558
 
559
  if side == "LONG":
560
-
561
- stop = entry - risk_distance
562
- target = entry + risk_distance * 2
563
-
564
  else:
565
-
566
- stop = entry + risk_distance
567
- target = entry - risk_distance * 2
568
 
569
  future = raw.iloc[
570
- raw_position + 1:
571
- raw_position + horizon + 1
572
  ]
573
 
574
  if len(future) < horizon:
@@ -584,8 +529,8 @@ def run_backtest(
584
 
585
  if side == "LONG":
586
 
587
- # Aynı mumda iki seviyeye de dokunursa
588
- # ihtiyatlı olarak stop önce kabul edilir.
589
  if low <= stop:
590
  exit_price = stop
591
  exit_reason = "STOP"
@@ -609,12 +554,12 @@ def run_backtest(
609
  break
610
 
611
  if side == "LONG":
612
- gross_return = (exit_price - entry) / entry
613
-
614
  else:
615
- gross_return = (entry - exit_price) / entry
616
 
617
- net_return = gross_return - (2 * fee_rate)
 
618
 
619
  trades.append({
620
  "time": timestamp,
@@ -622,14 +567,12 @@ def run_backtest(
622
  "entry": entry,
623
  "exit": exit_price,
624
  "probability": probability,
625
- "gross_return": gross_return * 100,
626
- "net_return": net_return * 100,
627
- "result": "WIN" if net_return > 0 else "LOSS",
628
  "exit_reason": exit_reason
629
  })
630
 
631
- # Pozisyonlar çakışmasın.
632
- next_allowed_bar = raw_position + horizon
633
 
634
  if not trades:
635
 
@@ -638,43 +581,263 @@ def run_backtest(
638
  "winrate": 0,
639
  "profit_factor": 0,
640
  "total_return": 0,
 
641
  "trades": []
642
  }
643
 
644
  trade_df = pd.DataFrame(trades)
645
 
646
- wins = trade_df[
647
- trade_df["net_return"] > 0
648
- ]["net_return"]
 
649
 
650
- losses = trade_df[
651
- trade_df["net_return"] < 0
652
- ]["net_return"]
 
653
 
654
  gross_profit = wins.sum()
655
  gross_loss = abs(losses.sum())
656
 
657
- profit_factor = (
658
- gross_profit / gross_loss
659
- if gross_loss > 0
660
- else float("inf") if gross_profit > 0 else 0
661
- )
 
662
 
663
  winrate = (
664
  len(wins) / len(trade_df)
665
  ) * 100
666
 
667
- total_return = trade_df["net_return"].sum()
 
 
 
 
 
 
 
 
 
 
 
 
 
668
 
669
  return {
670
  "count": len(trade_df),
671
  "winrate": winrate,
672
- "profit_factor": profit_factor,
673
  "total_return": total_return,
 
674
  "trades": trades
675
  }
676
 
677
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
  # =========================================================
679
  # MAIN ANALYSIS
680
  # =========================================================
@@ -683,7 +846,7 @@ def analyze(
683
  timeframe,
684
  candles,
685
  horizon,
686
- threshold,
687
  fee_pct
688
  ):
689
 
@@ -691,30 +854,29 @@ def analyze(
691
 
692
  candles = int(candles)
693
  horizon = int(horizon)
694
- threshold = float(threshold)
695
  fee_pct = float(fee_pct)
696
 
697
- if candles < 700:
698
- return "En az 700 mum gerekli."
699
 
700
  if horizon < 1 or horizon > 48:
701
- return "Horizon 1 ile 48 arasında olmalı."
702
 
703
- if not 0.50 < threshold < 0.90:
704
- return "Threshold 0.50 ile 0.90 arasında olmalı."
705
 
706
  if fee_pct < 0 or fee_pct > 1:
707
- return "Komisyon 0 ile 1 arasında olmalı."
708
 
709
- # -------------------------------------------------
710
  # DATA
711
- # -------------------------------------------------
712
-
713
  raw = fetch_data(
714
  timeframe=timeframe,
715
  candles=candles
716
  )
717
 
 
 
718
  data = prepare_dataset(
719
  raw,
720
  horizon
@@ -723,82 +885,100 @@ def analyze(
723
  X = data[FEATURES]
724
  y = data["target"].astype(int)
725
 
726
- # -------------------------------------------------
727
- # CHRONOLOGICAL SPLIT
728
- # -------------------------------------------------
729
 
 
730
  split = int(len(data) * 0.80)
731
 
732
- X_train = X.iloc[:split]
733
- X_test = X.iloc[split:]
734
 
735
- y_train = y.iloc[:split]
736
  y_test = y.iloc[split:]
737
 
738
- if y_train.nunique() < 2:
739
  return "Eğitim verisinde iki sınıf da bulunmalı."
740
 
741
- # -------------------------------------------------
742
- # WALK FORWARD
743
- # -------------------------------------------------
744
-
745
- wf_score, fold_results = walk_forward_validation(
746
- X_train,
747
- y_train
 
 
 
748
  )
749
 
750
- # -------------------------------------------------
751
- # FINAL HOLDOUT
752
- # -------------------------------------------------
 
 
 
753
 
754
- probabilities = ensemble_predict(
755
- X_train,
756
- y_train,
757
- X_test
 
 
758
  )
759
 
760
- predictions = (
761
- probabilities >= 0.5
762
  ).astype(int)
763
 
764
  accuracy = accuracy_score(
765
  y_test,
766
- predictions
767
  )
768
 
769
  balanced = balanced_accuracy_score(
770
  y_test,
771
- predictions
772
  )
773
 
774
  try:
775
  auc = roc_auc_score(
776
  y_test,
777
- probabilities
778
  )
779
  except Exception:
780
  auc = 0.5
781
 
782
- # -------------------------------------------------
783
- # BACKTEST
784
- # -------------------------------------------------
 
 
 
 
 
 
 
 
 
785
 
786
  backtest = run_backtest(
787
- raw=raw,
788
- test=data.iloc[split:],
789
- probabilities=probabilities,
790
  horizon=horizon,
791
  threshold=threshold,
 
792
  fee_pct=fee_pct
793
  )
794
 
795
- # -------------------------------------------------
796
- # CURRENT SIGNAL
797
- # -------------------------------------------------
798
-
799
- latest = calculate_indicators(raw).dropna(
800
  subset=FEATURES + ["atr"]
801
- ).iloc[-1]
 
 
802
 
803
  latest_X = pd.DataFrame(
804
  [latest[FEATURES].values],
@@ -806,9 +986,10 @@ def analyze(
806
  )
807
 
808
  latest_probability = ensemble_predict(
809
- X,
810
- y,
811
- latest_X
 
812
  )[0]
813
 
814
  p_up = float(latest_probability)
@@ -816,107 +997,114 @@ def analyze(
816
 
817
  if p_up >= threshold:
818
  signal = "LONG"
819
-
820
  elif p_down >= threshold:
821
  signal = "SHORT"
822
-
823
  else:
824
  signal = "NÖTR"
825
 
826
- # -------------------------------------------------
827
- # ENTRY / STOP / TARGET
828
- # -------------------------------------------------
829
-
830
  entry = float(latest["close"])
831
  atr = float(latest["atr"])
832
 
833
  if signal == "LONG":
834
-
835
- stop = entry - 1.5 * atr
836
- target = entry + 3.0 * atr
837
 
838
  elif signal == "SHORT":
839
-
840
- stop = entry + 1.5 * atr
841
- target = entry - 3.0 * atr
842
 
843
  else:
844
-
845
  stop = None
846
  target = None
847
 
848
- # -------------------------------------------------
849
- # INDICATOR INTERPRETATION
850
- # -------------------------------------------------
851
-
852
  rsi = float(latest["rsi"])
853
 
854
- macd_value = float(latest["macd"])
855
- macd_signal = float(latest["macd_signal"])
 
 
 
856
 
857
- st_direction = int(
858
- latest["supertrend_direction"]
 
 
859
  )
860
 
861
- vwap_distance = float(
862
- latest["vwap_distance"]
 
 
863
  )
864
 
865
- indicator_text = (
866
- f"RSI: {rsi:.2f}\n"
867
- f"MACD: {'BULLISH' if macd_value > macd_signal else 'BEARISH'}\n"
868
- f"Supertrend: {'BULLISH' if st_direction == 1 else 'BEARISH'}\n"
869
- f"VWAP: {'ÜSTÜNDE' if vwap_distance > 0 else 'ALTINDA'}\n"
870
- f"VWAP uzaklığı: {vwap_distance:.3f}%\n"
871
  )
872
 
873
- # -------------------------------------------------
874
- # RESULTS
875
- # -------------------------------------------------
876
 
877
- pf = backtest["profit_factor"]
878
 
879
- if np.isinf(pf):
880
- pf_text = "∞"
881
- else:
882
- pf_text = f"{pf:.2f}"
883
 
884
- if signal == "LONG":
885
- signal_text = "LONG"
886
- elif signal == "SHORT":
887
- signal_text = "SHORT"
 
 
 
888
  else:
889
- signal_text = "NÖTR"
 
890
 
891
  output = f"""
892
- # BTC/USDT AI ANALİZİ
893
-
894
- <badge> {signal_text} </badge>
895
 
896
- ## Güncel tahmin
897
 
898
  | Metrik | Sonuç |
899
  |---|---:|
900
- | BTC fiyatı | {entry:,.2f} USDT |
901
  | LONG olasılığı | {p_up * 100:.2f}% |
902
  | SHORT olasılığı | {p_down * 100:.2f}% |
903
- | Model sinyali | **{signal}** |
904
  | Zaman dilimi | {timeframe} |
905
  | Tahmin horizon | {horizon} mum |
906
 
907
- ## TradingView indikatörleri
908
 
909
- ```text
910
- {indicator_text}
911
- ```
 
 
 
 
912
 
913
  ## İşlem seviyeleri
914
 
915
  | Seviye | Fiyat |
916
  |---|---:|
917
  | Giriş | {entry:,.2f} |
918
- | Stop Loss | {stop:,.2f if stop is not None else "—"} |
919
- | 2R Hedef | {target:,.2f if target is not None else "—"} |
 
 
 
 
 
 
 
 
 
 
920
 
921
  ## Bağımsız hold-out test
922
 
@@ -927,39 +1115,28 @@ def analyze(
927
  | AUC | {auc:.4f} |
928
  | Walk-forward Balanced Accuracy | {wf_score * 100:.2f}% |
929
 
930
- ## Backtest sonuçları
931
 
932
  | Metrik | Sonuç |
933
  |---|---:|
934
- | İşlem sayısı | {backtest["count"]} |
935
- | Win Rate | {backtest["winrate"]:.2f}% |
936
  | Profit Factor | {pf_text} |
937
- | Net toplam getiri | {backtest["total_return"]:.2f}% |
938
- | İşlem komisyonu | %{fee_pct:.3f} / taraf |
939
-
940
- ## Walk-forward fold sonuçları
941
-
942
- """ + "\n".join(
943
- f"- {x}" for x in fold_results
944
- )
945
-
946
- if backtest["trades"]:
947
-
948
- trade_df = pd.DataFrame(
949
- backtest["trades"]
950
- )
951
 
952
- output += "\n\n## Son backtest işlemleri\n\n"
953
 
954
- output += trade_df.tail(15).to_markdown(
955
- index=False
956
- )
957
 
958
- output += """
959
 
960
- ---
 
 
961
 
962
- **Uyarı:** Bu bir istatistiksel model tahminidir, kesin fiyat tahmini değildir. Hold-out sonuçları geçmiş veriye aittir. Komisyon, kayma ve gerçek emir gerçekleşmesi sonuçları değiştirebilir. NÖTR sinyali işlem açmama anlamına gelir.
963
  """
964
 
965
  return output
@@ -967,9 +1144,9 @@ def analyze(
967
  except Exception as e:
968
 
969
  return (
970
- "# HATA\n\n"
971
  f"`{type(e).__name__}: {str(e)}`\n\n"
972
- "Bağlantıyı, Binance erişimini ve paket kurulumlarını kontrol et."
973
  )
974
 
975
 
@@ -978,15 +1155,16 @@ def analyze(
978
  # =========================================================
979
 
980
  with gr.Blocks(
981
- title="BTC AI Indicator Ensemble"
982
  ) as app:
983
 
984
  gr.Markdown(
985
  """
986
- # BTC AI — Indicator Ensemble
987
- MCP'siz, Binance verisiyle çalışan BTC/USDT tahmin sistemi.
988
 
989
- **Supertrend + RSI + MACD + VWAP + Machine Learning**
 
 
990
  """
991
  )
992
 
@@ -1015,7 +1193,7 @@ MCP'siz, Binance verisiyle çalışan BTC/USDT tahmin sistemi.
1015
  5000
1016
  ],
1017
  value=2000,
1018
- label="Mum sayısı"
1019
  )
1020
 
1021
  with gr.Row():
@@ -1028,24 +1206,24 @@ MCP'siz, Binance verisiyle çalışan BTC/USDT tahmin sistemi.
1028
  label="Tahmin horizon (mum)"
1029
  )
1030
 
1031
- threshold = gr.Slider(
1032
- minimum=0.51,
1033
- maximum=0.80,
1034
- value=0.56,
1035
- step=0.01,
1036
- label="Sinyal güven eşiği"
1037
  )
1038
 
1039
  fee = gr.Slider(
1040
  minimum=0,
1041
  maximum=0.20,
1042
- value=0.10,
1043
  step=0.01,
1044
  label="Komisyon (%) / taraf"
1045
  )
1046
 
1047
  run_button = gr.Button(
1048
- "BTC ANALİZ ET",
1049
  variant="primary"
1050
  )
1051
 
@@ -1057,7 +1235,7 @@ MCP'siz, Binance verisiyle çalışan BTC/USDT tahmin sistemi.
1057
  timeframe,
1058
  candles,
1059
  horizon,
1060
- threshold,
1061
  fee
1062
  ],
1063
  outputs=output
 
5
  import time
6
  import numpy as np
7
  import pandas as pd
 
8
  import gradio as gr
9
+ import ccxt
10
+ import optuna
11
 
12
  from sklearn.ensemble import (
13
  ExtraTreesClassifier,
 
28
  # CONFIG
29
  # =========================================================
30
 
31
+ SYMBOL = "BTC/USDT:USDT"
32
 
33
  FEATURES = [
34
  "rsi",
 
50
 
51
 
52
  # =========================================================
53
+ # OKX DATA
54
  # =========================================================
55
 
56
  def fetch_data(timeframe="1h", candles=2000):
57
 
58
+ exchange = ccxt.okx({
59
  "enableRateLimit": True,
60
+ "timeout": 30000,
61
+ "options": {
62
+ "defaultType": "swap"
63
+ }
64
  })
65
 
66
+ exchange.load_markets()
67
+
68
+ if SYMBOL not in exchange.markets:
69
+ raise ValueError(
70
+ f"OKX sembolü bulunamadı: {SYMBOL}"
71
+ )
72
+
73
  timeframe_ms = exchange.parse_timeframe(timeframe) * 1000
74
 
75
+ since = (
76
+ exchange.milliseconds()
77
+ - candles * timeframe_ms
78
+ )
79
+
80
  all_rows = []
81
+ last_timestamp = None
82
 
83
  while len(all_rows) < candles:
84
 
85
+ limit = min(100, candles - len(all_rows))
 
86
 
87
  batch = exchange.fetch_ohlcv(
88
  SYMBOL,
 
94
  if not batch:
95
  break
96
 
97
+ for row in batch:
98
 
99
+ if last_timestamp is None or row[0] > last_timestamp:
100
+ all_rows.append(row)
101
+ last_timestamp = row[0]
102
+
103
+ new_since = batch[-1][0] + timeframe_ms
104
 
105
  if new_since <= since:
106
  break
 
113
  time.sleep(exchange.rateLimit / 1000)
114
 
115
  if not all_rows:
116
+ raise RuntimeError("OKX veri döndürmedi.")
117
 
118
  df = pd.DataFrame(
119
  all_rows,
 
138
 
139
  df = df.set_index("timestamp")
140
 
141
+ for col in [
142
+ "open",
143
+ "high",
144
+ "low",
145
+ "close",
146
+ "volume"
147
+ ]:
148
+ df[col] = pd.to_numeric(
149
+ df[col],
150
+ errors="coerce"
151
+ )
152
 
153
  df = df.dropna()
154
 
155
+ # Tamamlanmamış son mumu çıkar.
156
+ if len(df) > 1:
157
  df = df.iloc[:-1]
158
 
159
  return df.tail(candles)
160
 
161
 
162
  # =========================================================
163
+ # INDICATORS
164
  # =========================================================
165
 
166
  def calculate_indicators(df):
167
 
168
  df = df.copy()
169
 
170
+ # ATR
 
 
 
171
  previous_close = df["close"].shift(1)
172
 
173
  tr = pd.concat([
 
182
  min_periods=10
183
  ).mean()
184
 
 
185
  # RSI 14
 
 
186
  delta = df["close"].diff()
187
 
188
  gain = delta.clip(lower=0)
 
202
 
203
  rs = avg_gain / avg_loss.replace(0, np.nan)
204
 
205
+ df["rsi"] = 100 - 100 / (1 + rs)
206
 
 
207
  # MACD 12,26,9
 
 
208
  ema12 = df["close"].ewm(
209
  span=12,
210
  adjust=False
 
226
  df["macd"] - df["macd_signal"]
227
  )
228
 
 
229
  # SUPERTREND 10,3
 
 
 
 
230
  hl2 = (df["high"] + df["low"]) / 2
231
 
232
+ basic_upper = hl2 + 3 * df["atr"]
233
+ basic_lower = hl2 - 3 * df["atr"]
234
 
235
  final_upper = basic_upper.copy()
236
  final_lower = basic_lower.copy()
237
 
238
+ direction = pd.Series(
239
+ 1.0,
240
  index=df.index
241
  )
242
 
243
+ supertrend = pd.Series(
244
+ np.nan,
245
+ index=df.index
 
246
  )
247
 
248
  for i in range(1, len(df)):
249
 
 
 
250
  previous_close_value = df["close"].iloc[i - 1]
251
 
 
 
 
252
  if (
253
+ basic_upper.iloc[i] < final_upper.iloc[i - 1]
254
+ or previous_close_value > final_upper.iloc[i - 1]
255
  ):
256
+ final_upper.iloc[i] = basic_upper.iloc[i]
257
  else:
258
+ final_upper.iloc[i] = final_upper.iloc[i - 1]
259
 
260
  if (
261
+ basic_lower.iloc[i] > final_lower.iloc[i - 1]
262
+ or previous_close_value < final_lower.iloc[i - 1]
263
  ):
264
+ final_lower.iloc[i] = basic_lower.iloc[i]
265
  else:
266
+ final_lower.iloc[i] = final_lower.iloc[i - 1]
267
 
 
268
  current_close = df["close"].iloc[i]
269
 
270
+ if direction.iloc[i - 1] == -1:
271
 
272
+ direction.iloc[i] = (
273
+ 1 if current_close > final_upper.iloc[i]
274
+ else -1
275
+ )
276
 
277
  else:
278
 
279
+ direction.iloc[i] = (
280
+ -1 if current_close < final_lower.iloc[i]
281
+ else 1
282
+ )
283
 
284
+ supertrend.iloc[i] = (
285
+ final_lower.iloc[i]
286
+ if direction.iloc[i] == 1
287
+ else final_upper.iloc[i]
288
+ )
289
 
290
  df["supertrend"] = supertrend
291
  df["supertrend_direction"] = direction
292
 
293
+ # UTC DAILY VWAP
 
 
 
294
  typical_price = (
295
  df["high"] + df["low"] + df["close"]
296
  ) / 3
297
 
298
  daily_group = df.index.date
299
 
300
+ cumulative_pv = (
301
+ (typical_price * df["volume"])
302
+ .groupby(daily_group)
303
+ .cumsum()
304
+ )
305
 
306
  cumulative_volume = (
307
+ df["volume"]
308
+ .groupby(daily_group)
309
+ .cumsum()
310
  )
311
 
312
  df["vwap"] = (
 
314
  cumulative_volume.replace(0, np.nan)
315
  )
316
 
317
+ # FEATURES
 
 
 
318
  df["macd_pct"] = df["macd"] / df["close"] * 100
319
 
320
  df["macd_signal_pct"] = (
 
326
  )
327
 
328
  df["supertrend_distance"] = (
329
+ (df["close"] - df["supertrend"])
330
+ / df["close"] * 100
331
+ )
332
 
333
  df["vwap_distance"] = (
334
+ (df["close"] - df["vwap"])
335
+ / df["close"] * 100
336
+ )
337
 
338
  df["atr_pct"] = df["atr"] / df["close"] * 100
339
 
 
345
  df["return_1"].rolling(14).std()
346
  )
347
 
348
+ volume_avg = df["volume"].rolling(20).mean()
349
 
350
  df["volume_ratio"] = (
351
  df["volume"] /
352
+ volume_avg.replace(0, np.nan)
353
  )
354
 
355
  df["body_pct"] = (
356
+ (df["close"] - df["open"])
357
+ / df["open"] * 100
358
+ )
359
 
360
  df["high_low_pct"] = (
361
+ (df["high"] - df["low"])
362
+ / df["close"] * 100
363
+ )
364
 
365
  return df
366
 
367
 
368
  # =========================================================
369
+ # DATASET
370
  # =========================================================
371
 
372
+ def prepare_dataset(raw, horizon):
373
 
374
+ data = calculate_indicators(raw)
375
 
376
  future_close = data["close"].shift(-horizon)
377
 
 
379
  future_close > data["close"]
380
  ).astype(int)
381
 
382
+ # Bilinmeyen gelecek hedeflerini çıkar.
383
  data = data.iloc[:-horizon].copy()
384
 
385
  data = data.replace(
 
393
 
394
  if len(data) < 500:
395
  raise ValueError(
396
+ "Model için yeterli temiz veri bulunamadı."
397
  )
398
 
399
  return data
400
 
401
 
402
  # =========================================================
403
+ # MODEL FACTORY
404
  # =========================================================
405
 
406
+ def create_models(params):
407
 
408
+ extra = ExtraTreesClassifier(
409
+ n_estimators=params["extra_trees"],
410
+ max_depth=params["extra_depth"],
411
+ min_samples_leaf=params["extra_leaf"],
412
+ max_features=0.8,
413
+ class_weight="balanced",
414
+ random_state=42,
415
+ n_jobs=-1
416
+ )
 
 
 
 
 
 
 
 
 
 
 
 
417
 
418
+ hist = HistGradientBoostingClassifier(
419
+ max_iter=params["hgb_iter"],
420
+ learning_rate=params["hgb_lr"],
421
+ max_leaf_nodes=params["hgb_leaves"],
422
+ l2_regularization=params["hgb_l2"],
423
+ random_state=42
424
+ )
 
 
 
 
425
 
426
+ logistic = Pipeline([
427
+ ("scaler", StandardScaler()),
428
+ ("model", LogisticRegression(
429
+ C=params["logistic_c"],
430
+ max_iter=1500,
431
+ class_weight="balanced",
432
+ random_state=42
433
+ ))
434
+ ])
435
 
436
+ return [extra, hist, logistic]
437
 
 
438
 
439
+ def ensemble_predict(
440
+ X_train,
441
+ y_train,
442
+ X_test,
443
+ params
444
+ ):
445
 
446
  probabilities = []
447
 
448
+ for model in create_models(params):
449
 
450
  model.fit(X_train, y_train)
451
 
452
+ p = model.predict_proba(X_test)[:, 1]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
 
454
+ probabilities.append(p)
 
 
 
 
455
 
456
+ return np.mean(probabilities, axis=0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
 
458
 
459
  # =========================================================
460
+ # BACKTEST ENGINE
461
  # =========================================================
462
 
463
  def run_backtest(
 
466
  probabilities,
467
  horizon,
468
  threshold,
469
+ atr_multiplier,
470
  fee_pct
471
  ):
472
 
473
  trades = []
474
 
 
 
475
  fee_rate = fee_pct / 100
476
 
477
+ next_allowed = -1
478
+
479
+ raw_positions = raw.index.get_indexer(test.index)
480
+
481
+ for i, (timestamp, row) in enumerate(test.iterrows()):
482
 
483
+ position = raw_positions[i]
484
 
485
+ if position < 0 or position < next_allowed:
486
  continue
487
 
488
+ probability = float(probabilities[i])
 
 
489
 
490
  if probability >= threshold:
491
  side = "LONG"
492
 
493
+ elif probability <= 1 - threshold:
494
  side = "SHORT"
495
 
496
  else:
 
502
  if not np.isfinite(atr) or atr <= 0:
503
  continue
504
 
505
+ risk = atr * atr_multiplier
506
 
507
  if side == "LONG":
508
+ stop = entry - risk
509
+ target = entry + 2 * risk
 
 
510
  else:
511
+ stop = entry + risk
512
+ target = entry - 2 * risk
 
513
 
514
  future = raw.iloc[
515
+ position + 1:
516
+ position + horizon + 1
517
  ]
518
 
519
  if len(future) < horizon:
 
529
 
530
  if side == "LONG":
531
 
532
+ # Aynı mumda stop ve hedef görülürse
533
+ # ihtiyatlı biçimde stop önce kabul edilir.
534
  if low <= stop:
535
  exit_price = stop
536
  exit_reason = "STOP"
 
554
  break
555
 
556
  if side == "LONG":
557
+ gross = (exit_price - entry) / entry
 
558
  else:
559
+ gross = (entry - exit_price) / entry
560
 
561
+ # Giriş ve çıkış komisyonu
562
+ net = gross - 2 * fee_rate
563
 
564
  trades.append({
565
  "time": timestamp,
 
567
  "entry": entry,
568
  "exit": exit_price,
569
  "probability": probability,
570
+ "net_pct": net * 100,
571
+ "result": "WIN" if net > 0 else "LOSS",
 
572
  "exit_reason": exit_reason
573
  })
574
 
575
+ next_allowed = position + horizon
 
576
 
577
  if not trades:
578
 
 
581
  "winrate": 0,
582
  "profit_factor": 0,
583
  "total_return": 0,
584
+ "max_drawdown": 0,
585
  "trades": []
586
  }
587
 
588
  trade_df = pd.DataFrame(trades)
589
 
590
+ wins = trade_df.loc[
591
+ trade_df["net_pct"] > 0,
592
+ "net_pct"
593
+ ]
594
 
595
+ losses = trade_df.loc[
596
+ trade_df["net_pct"] < 0,
597
+ "net_pct"
598
+ ]
599
 
600
  gross_profit = wins.sum()
601
  gross_loss = abs(losses.sum())
602
 
603
+ if gross_loss > 0:
604
+ pf = gross_profit / gross_loss
605
+ elif gross_profit > 0:
606
+ pf = float("inf")
607
+ else:
608
+ pf = 0
609
 
610
  winrate = (
611
  len(wins) / len(trade_df)
612
  ) * 100
613
 
614
+ # İşlemler sırasıyla bileşik getiri
615
+ returns = trade_df["net_pct"].values / 100
616
+
617
+ equity = np.cumprod(1 + returns)
618
+
619
+ peak = np.maximum.accumulate(equity)
620
+
621
+ drawdown = (
622
+ (equity - peak) / peak
623
+ ) * 100
624
+
625
+ total_return = (
626
+ equity[-1] - 1
627
+ ) * 100
628
 
629
  return {
630
  "count": len(trade_df),
631
  "winrate": winrate,
632
+ "profit_factor": pf,
633
  "total_return": total_return,
634
+ "max_drawdown": abs(float(drawdown.min())),
635
  "trades": trades
636
  }
637
 
638
 
639
+ # =========================================================
640
+ # OPTUNA OPTIMIZATION
641
+ # =========================================================
642
+
643
+ def optimize_model(
644
+ X,
645
+ y,
646
+ raw,
647
+ horizon,
648
+ trials,
649
+ fee_pct
650
+ ):
651
+
652
+ splitter = TimeSeriesSplit(n_splits=3)
653
+
654
+ splits = list(splitter.split(X))
655
+
656
+ def objective(trial):
657
+
658
+ params = {
659
+ "extra_trees": trial.suggest_int(
660
+ "extra_trees", 100, 350
661
+ ),
662
+ "extra_depth": trial.suggest_int(
663
+ "extra_depth", 3, 14
664
+ ),
665
+ "extra_leaf": trial.suggest_int(
666
+ "extra_leaf", 2, 12
667
+ ),
668
+ "hgb_iter": trial.suggest_int(
669
+ "hgb_iter", 80, 250
670
+ ),
671
+ "hgb_lr": trial.suggest_float(
672
+ "hgb_lr", 0.01, 0.15, log=True
673
+ ),
674
+ "hgb_leaves": trial.suggest_int(
675
+ "hgb_leaves", 7, 25
676
+ ),
677
+ "hgb_l2": trial.suggest_float(
678
+ "hgb_l2", 0.1, 10, log=True
679
+ ),
680
+ "logistic_c": trial.suggest_float(
681
+ "logistic_c", 0.01, 3, log=True
682
+ )
683
+ }
684
+
685
+ threshold = trial.suggest_float(
686
+ "threshold", 0.53, 0.75
687
+ )
688
+
689
+ atr_multiplier = trial.suggest_float(
690
+ "atr_multiplier", 1.0, 2.5
691
+ )
692
+
693
+ fold_scores = []
694
+
695
+ for train_idx, val_idx in splits:
696
+
697
+ X_train = X.iloc[train_idx]
698
+ X_val = X.iloc[val_idx]
699
+
700
+ y_train = y.iloc[train_idx]
701
+
702
+ if y_train.nunique() < 2:
703
+ continue
704
+
705
+ probs = ensemble_predict(
706
+ X_train,
707
+ y_train,
708
+ X_val,
709
+ params
710
+ )
711
+
712
+ val_data = pd.DataFrame(
713
+ index=X_val.index
714
+ )
715
+
716
+ val_data["close"] = raw.loc[
717
+ X_val.index, "close"
718
+ ]
719
+
720
+ val_data["atr"] = raw.loc[
721
+ X_val.index, "atr"
722
+ ]
723
+
724
+ result = run_backtest(
725
+ raw=raw,
726
+ test=val_data,
727
+ probabilities=probs,
728
+ horizon=horizon,
729
+ threshold=threshold,
730
+ atr_multiplier=atr_multiplier,
731
+ fee_pct=fee_pct
732
+ )
733
+
734
+ if result["count"] < 5:
735
+ fold_scores.append(-5.0)
736
+ continue
737
+
738
+ pf = result["profit_factor"]
739
+
740
+ if np.isinf(pf):
741
+ pf = 3.0
742
+
743
+ # Getiri + PF - aşırı düşüş cezası
744
+ score = (
745
+ result["total_return"]
746
+ + min(pf, 3.0) * 3
747
+ - result["max_drawdown"] * 0.5
748
+ )
749
+
750
+ fold_scores.append(score)
751
+
752
+ if not fold_scores:
753
+ return -100.0
754
+
755
+ return float(np.mean(fold_scores))
756
+
757
+ study = optuna.create_study(
758
+ direction="maximize",
759
+ sampler=optuna.samplers.TPESampler(
760
+ seed=42
761
+ )
762
+ )
763
+
764
+ optuna.logging.set_verbosity(
765
+ optuna.logging.WARNING
766
+ )
767
+
768
+ study.optimize(
769
+ objective,
770
+ n_trials=trials,
771
+ show_progress_bar=False
772
+ )
773
+
774
+ best = study.best_params
775
+
776
+ params = {
777
+ "extra_trees": best["extra_trees"],
778
+ "extra_depth": best["extra_depth"],
779
+ "extra_leaf": best["extra_leaf"],
780
+ "hgb_iter": best["hgb_iter"],
781
+ "hgb_lr": best["hgb_lr"],
782
+ "hgb_leaves": best["hgb_leaves"],
783
+ "hgb_l2": best["hgb_l2"],
784
+ "logistic_c": best["logistic_c"]
785
+ }
786
+
787
+ return (
788
+ params,
789
+ best["threshold"],
790
+ best["atr_multiplier"],
791
+ study.best_value,
792
+ len(study.trials)
793
+ )
794
+
795
+
796
+ # =========================================================
797
+ # WALK FORWARD VALIDATION
798
+ # =========================================================
799
+
800
+ def walk_forward_validation(X, y, params):
801
+
802
+ splitter = TimeSeriesSplit(n_splits=4)
803
+
804
+ scores = []
805
+
806
+ for train_idx, test_idx in splitter.split(X):
807
+
808
+ X_train = X.iloc[train_idx]
809
+ X_test = X.iloc[test_idx]
810
+
811
+ y_train = y.iloc[train_idx]
812
+ y_test = y.iloc[test_idx]
813
+
814
+ if y_train.nunique() < 2:
815
+ continue
816
+
817
+ probabilities = ensemble_predict(
818
+ X_train,
819
+ y_train,
820
+ X_test,
821
+ params
822
+ )
823
+
824
+ predictions = (
825
+ probabilities >= 0.5
826
+ ).astype(int)
827
+
828
+ scores.append(
829
+ balanced_accuracy_score(
830
+ y_test,
831
+ predictions
832
+ )
833
+ )
834
+
835
+ if not scores:
836
+ return 0
837
+
838
+ return float(np.mean(scores))
839
+
840
+
841
  # =========================================================
842
  # MAIN ANALYSIS
843
  # =========================================================
 
846
  timeframe,
847
  candles,
848
  horizon,
849
+ trials,
850
  fee_pct
851
  ):
852
 
 
854
 
855
  candles = int(candles)
856
  horizon = int(horizon)
857
+ trials = int(trials)
858
  fee_pct = float(fee_pct)
859
 
860
+ if candles < 800:
861
+ return "En az 800 mum seç."
862
 
863
  if horizon < 1 or horizon > 48:
864
+ return "Horizon 1-48 arasında olmalı."
865
 
866
+ if trials < 5 or trials > 100:
867
+ return "Optuna trial sayısı 5-100 arasında olmalı."
868
 
869
  if fee_pct < 0 or fee_pct > 1:
870
+ return "Komisyon değeri geçersiz."
871
 
 
872
  # DATA
 
 
873
  raw = fetch_data(
874
  timeframe=timeframe,
875
  candles=candles
876
  )
877
 
878
+ indicators = calculate_indicators(raw)
879
+
880
  data = prepare_dataset(
881
  raw,
882
  horizon
 
885
  X = data[FEATURES]
886
  y = data["target"].astype(int)
887
 
888
+ if y.nunique() < 2:
889
+ return "Veri setinde yalnızca tek hedef sınıfı var."
 
890
 
891
+ # CHRONOLOGICAL HOLDOUT
892
  split = int(len(data) * 0.80)
893
 
894
+ X_dev = X.iloc[:split]
895
+ y_dev = y.iloc[:split]
896
 
897
+ X_test = X.iloc[split:]
898
  y_test = y.iloc[split:]
899
 
900
+ if y_dev.nunique() < 2:
901
  return "Eğitim verisinde iki sınıf da bulunmalı."
902
 
903
+ # OPTUNA
904
+ params, threshold, atr_mult, best_score, n_trials = (
905
+ optimize_model(
906
+ X=X_dev,
907
+ y=y_dev,
908
+ raw=indicators,
909
+ horizon=horizon,
910
+ trials=trials,
911
+ fee_pct=fee_pct
912
+ )
913
  )
914
 
915
+ # WALK FORWARD
916
+ wf_score = walk_forward_validation(
917
+ X_dev,
918
+ y_dev,
919
+ params
920
+ )
921
 
922
+ # HOLDOUT PREDICTION
923
+ test_probs = ensemble_predict(
924
+ X_dev,
925
+ y_dev,
926
+ X_test,
927
+ params
928
  )
929
 
930
+ test_predictions = (
931
+ test_probs >= 0.5
932
  ).astype(int)
933
 
934
  accuracy = accuracy_score(
935
  y_test,
936
+ test_predictions
937
  )
938
 
939
  balanced = balanced_accuracy_score(
940
  y_test,
941
+ test_predictions
942
  )
943
 
944
  try:
945
  auc = roc_auc_score(
946
  y_test,
947
+ test_probs
948
  )
949
  except Exception:
950
  auc = 0.5
951
 
952
+ # HOLDOUT BACKTEST
953
+ test_data = pd.DataFrame(
954
+ index=X_test.index
955
+ )
956
+
957
+ test_data["close"] = indicators.loc[
958
+ X_test.index, "close"
959
+ ]
960
+
961
+ test_data["atr"] = indicators.loc[
962
+ X_test.index, "atr"
963
+ ]
964
 
965
  backtest = run_backtest(
966
+ raw=indicators,
967
+ test=test_data,
968
+ probabilities=test_probs,
969
  horizon=horizon,
970
  threshold=threshold,
971
+ atr_multiplier=atr_mult,
972
  fee_pct=fee_pct
973
  )
974
 
975
+ # LIVE MODEL: development data only
976
+ # Holdout is not used for final model fitting.
977
+ latest_indicators = indicators.dropna(
 
 
978
  subset=FEATURES + ["atr"]
979
+ )
980
+
981
+ latest = latest_indicators.iloc[-1]
982
 
983
  latest_X = pd.DataFrame(
984
  [latest[FEATURES].values],
 
986
  )
987
 
988
  latest_probability = ensemble_predict(
989
+ X_dev,
990
+ y_dev,
991
+ latest_X,
992
+ params
993
  )[0]
994
 
995
  p_up = float(latest_probability)
 
997
 
998
  if p_up >= threshold:
999
  signal = "LONG"
 
1000
  elif p_down >= threshold:
1001
  signal = "SHORT"
 
1002
  else:
1003
  signal = "NÖTR"
1004
 
 
 
 
 
1005
  entry = float(latest["close"])
1006
  atr = float(latest["atr"])
1007
 
1008
  if signal == "LONG":
1009
+ stop = entry - atr * atr_mult
1010
+ target = entry + atr * atr_mult * 2
 
1011
 
1012
  elif signal == "SHORT":
1013
+ stop = entry + atr * atr_mult
1014
+ target = entry - atr * atr_mult * 2
 
1015
 
1016
  else:
 
1017
  stop = None
1018
  target = None
1019
 
1020
+ # INDICATORS
 
 
 
1021
  rsi = float(latest["rsi"])
1022
 
1023
+ macd_state = (
1024
+ "BULLISH"
1025
+ if latest["macd"] > latest["macd_signal"]
1026
+ else "BEARISH"
1027
+ )
1028
 
1029
+ supertrend_state = (
1030
+ "BULLISH"
1031
+ if latest["supertrend_direction"] == 1
1032
+ else "BEARISH"
1033
  )
1034
 
1035
+ vwap_state = (
1036
+ "ÜSTÜNDE"
1037
+ if latest["vwap_distance"] > 0
1038
+ else "ALTINDA"
1039
  )
1040
 
1041
+ pf = backtest["profit_factor"]
1042
+
1043
+ pf_text = (
1044
+ "∞" if np.isinf(pf)
1045
+ else f"{pf:.2f}"
 
1046
  )
1047
 
1048
+ trade_table = ""
 
 
1049
 
1050
+ if backtest["trades"]:
1051
 
1052
+ trade_df = pd.DataFrame(
1053
+ backtest["trades"]
1054
+ )
 
1055
 
1056
+ trade_table = trade_df.tail(15).to_markdown(
1057
+ index=False
1058
+ )
1059
+
1060
+ if stop is None:
1061
+ stop_text = "—"
1062
+ target_text = "—"
1063
  else:
1064
+ stop_text = f"{stop:,.2f}"
1065
+ target_text = f"{target:,.2f}"
1066
 
1067
  output = f"""
1068
+ # OKX BTC/USDT AI ANALİZİ
 
 
1069
 
1070
+ ## Güncel sinyal: {signal}
1071
 
1072
  | Metrik | Sonuç |
1073
  |---|---:|
1074
+ | OKX BTC fiyatı | {entry:,.2f} USDT |
1075
  | LONG olasılığı | {p_up * 100:.2f}% |
1076
  | SHORT olasılığı | {p_down * 100:.2f}% |
1077
+ | Sinyal eşiği | {threshold:.3f} |
1078
  | Zaman dilimi | {timeframe} |
1079
  | Tahmin horizon | {horizon} mum |
1080
 
1081
+ ## TradingView uyumlu indikatörler
1082
 
1083
+ | İndikatör | Değer |
1084
+ |---|---:|
1085
+ | RSI 14 | {rsi:.2f} |
1086
+ | MACD | {macd_state} |
1087
+ | Supertrend 10,3 | {supertrend_state} |
1088
+ | VWAP | {vwap_state} |
1089
+ | VWAP uzaklığı | {latest['vwap_distance']:.3f}% |
1090
 
1091
  ## İşlem seviyeleri
1092
 
1093
  | Seviye | Fiyat |
1094
  |---|---:|
1095
  | Giriş | {entry:,.2f} |
1096
+ | Stop Loss | {stop_text} |
1097
+ | 2R hedef | {target_text} |
1098
+ | ATR çarpanı | {atr_mult:.2f} |
1099
+
1100
+ ## Optuna optimizasyonu
1101
+
1102
+ | Metrik | Sonuç |
1103
+ |---|---:|
1104
+ | Tamamlanan trial | {n_trials} |
1105
+ | En iyi objective | {best_score:.4f} |
1106
+ | Optimize edilen threshold | {threshold:.3f} |
1107
+ | Optimize edilen ATR | {atr_mult:.2f} |
1108
 
1109
  ## Bağımsız hold-out test
1110
 
 
1115
  | AUC | {auc:.4f} |
1116
  | Walk-forward Balanced Accuracy | {wf_score * 100:.2f}% |
1117
 
1118
+ ## Hold-out backtest
1119
 
1120
  | Metrik | Sonuç |
1121
  |---|---:|
1122
+ | İşlem sayısı | {backtest['count']} |
1123
+ | Win Rate | {backtest['winrate']:.2f}% |
1124
  | Profit Factor | {pf_text} |
1125
+ | Bileşik net getiri | {backtest['total_return']:.2f}% |
1126
+ | Maksimum drawdown | {backtest['max_drawdown']:.2f}% |
1127
+ | Komisyon | %{fee_pct:.3f} / taraf |
 
 
 
 
 
 
 
 
 
 
 
1128
 
1129
+ ## Son işlemler
1130
 
1131
+ {trade_table if trade_table else "Backtest döneminde işlem oluşmadı."}
 
 
1132
 
1133
+ ## Optuna'nın seçtiği model parametreleri
1134
 
1135
+ ```text
1136
+ {params}
1137
+ ```
1138
 
1139
+ **Not:** Optuna yalnızca ilk %80'lik geliştirme verisinde optimize edildi. Son %20 hold-out test optimizasyona dahil edilmedi. Hold-out sonucu geçmiş performanstır; gelecekteki performans garantisi değildir.
1140
  """
1141
 
1142
  return output
 
1144
  except Exception as e:
1145
 
1146
  return (
1147
+ "# ANALİZ HATASI\n\n"
1148
  f"`{type(e).__name__}: {str(e)}`\n\n"
1149
+ "OKX bağlantısını, sembolü ve paketleri kontrol et."
1150
  )
1151
 
1152
 
 
1155
  # =========================================================
1156
 
1157
  with gr.Blocks(
1158
+ title="OKX BTC AI Optuna Backtest"
1159
  ) as app:
1160
 
1161
  gr.Markdown(
1162
  """
1163
+ # OKX BTC AI — OPTUNA + BACKTEST
 
1164
 
1165
+ OKX perpetual swap verisiyle çalışan makine öğrenmesi sistemi.
1166
+
1167
+ **Supertrend + RSI + MACD + VWAP + Ensemble ML + Optuna**
1168
  """
1169
  )
1170
 
 
1193
  5000
1194
  ],
1195
  value=2000,
1196
+ label="OKX mum sayısı"
1197
  )
1198
 
1199
  with gr.Row():
 
1206
  label="Tahmin horizon (mum)"
1207
  )
1208
 
1209
+ trials = gr.Slider(
1210
+ minimum=5,
1211
+ maximum=100,
1212
+ value=20,
1213
+ step=5,
1214
+ label="Optuna trial sayısı"
1215
  )
1216
 
1217
  fee = gr.Slider(
1218
  minimum=0,
1219
  maximum=0.20,
1220
+ value=0.05,
1221
  step=0.01,
1222
  label="Komisyon (%) / taraf"
1223
  )
1224
 
1225
  run_button = gr.Button(
1226
+ "OKX AI ANALİZ + OPTUNA BACKTEST",
1227
  variant="primary"
1228
  )
1229
 
 
1235
  timeframe,
1236
  candles,
1237
  horizon,
1238
+ trials,
1239
  fee
1240
  ],
1241
  outputs=output