GoshawkVortexAI commited on
Commit
0b20ce2
·
verified ·
1 Parent(s): f4d6c9d

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +575 -0
app.py ADDED
@@ -0,0 +1,575 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import warnings
2
+ warnings.filterwarnings("ignore")
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ import yfinance as yf
7
+ import gradio as gr
8
+ import plotly.graph_objects as go
9
+
10
+ from sklearn.impute import SimpleImputer
11
+ from sklearn.preprocessing import StandardScaler
12
+ from sklearn.pipeline import Pipeline
13
+ from sklearn.neighbors import KNeighborsClassifier, NearestNeighbors
14
+ from sklearn.isotonic import IsotonicRegression
15
+ from sklearn.metrics import balanced_accuracy_score, accuracy_score
16
+
17
+ APP_TITLE = "Goshawk BTCUSD Adaptive Risk Engine v3"
18
+ HORIZON = 5
19
+ K_GRID = [5, 7, 9, 11, 15, 21]
20
+ SL_GRID = [0.8, 1.0, 1.2, 1.5, 1.8, 2.0, 2.5, 3.0]
21
+ RR_GRID = [1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0]
22
+ CLASSES = [-1, 0, 1]
23
+
24
+
25
+ def get_btc(period):
26
+ df = yf.download("BTC-USD", period=period, interval="1d",
27
+ auto_adjust=True, progress=False)
28
+ if df is None or df.empty:
29
+ raise ValueError("Yahoo Finance BTC verisi döndürmedi.")
30
+ if isinstance(df.columns, pd.MultiIndex):
31
+ df.columns = df.columns.get_level_values(0)
32
+ df = df[["Open", "High", "Low", "Close", "Volume"]].dropna().copy()
33
+ df.index = pd.to_datetime(df.index)
34
+ return df
35
+
36
+
37
+ def calc_atr(df, n=14):
38
+ pc = df.Close.shift(1)
39
+ tr = pd.concat([(df.High-df.Low), (df.High-pc).abs(),
40
+ (df.Low-pc).abs()], axis=1).max(axis=1)
41
+ return tr.ewm(alpha=1/n, adjust=False).mean()
42
+
43
+
44
+ def calc_rsi(s, n=14):
45
+ d = s.diff()
46
+ up = d.clip(lower=0).ewm(alpha=1/n, adjust=False).mean()
47
+ dn = (-d.clip(upper=0)).ewm(alpha=1/n, adjust=False).mean()
48
+ rs = up / dn.replace(0, np.nan)
49
+ return 100 - 100/(1+rs)
50
+
51
+
52
+ def make_features(df):
53
+ c, v = df.Close, df.Volume
54
+ x = pd.DataFrame(index=df.index)
55
+ for n in [1, 2, 3, 5, 10, 20, 50]:
56
+ x[f"ret_{n}"] = c.pct_change(n)
57
+ for n in [5, 10, 20, 50]:
58
+ x[f"ema_gap_{n}"] = c/c.ewm(span=n, adjust=False).mean()-1
59
+ x["rsi_14"] = calc_rsi(c)/100
60
+ x["atr_pct"] = calc_atr(df)/c
61
+ x["range_pct"] = (df.High-df.Low)/c
62
+ x["body_pct"] = (df.Close-df.Open)/df.Open
63
+ daily_ret = c.pct_change()
64
+ x["vol_10"] = daily_ret.rolling(10).std()
65
+ x["vol_20"] = daily_ret.rolling(20).std()
66
+ x["volume_z20"] = (v-v.rolling(20).mean())/v.rolling(20).std().replace(0, np.nan)
67
+ x["volume_change"] = v.pct_change()
68
+ macd = c.ewm(span=12, adjust=False).mean()-c.ewm(span=26, adjust=False).mean()
69
+ x["macd_pct"] = macd/c
70
+ return x.replace([np.inf, -np.inf], np.nan), calc_atr(df)
71
+
72
+
73
+ def regime_labels(features):
74
+ vol = features["vol_20"]
75
+ trend = features["ema_gap_20"]
76
+ vol_q1, vol_q2 = vol.quantile([0.33, 0.67])
77
+ out = []
78
+ for v, t in zip(vol, trend):
79
+ vname = "LOW_VOL" if v <= vol_q1 else ("HIGH_VOL" if v >= vol_q2 else "MID_VOL")
80
+ tname = "UP" if t > 0.02 else ("DOWN" if t < -0.02 else "RANGE")
81
+ out.append(f"{tname}_{vname}")
82
+ return pd.Series(out, index=features.index)
83
+
84
+
85
+ def make_target(df, atr, horizon=HORIZON, threshold=0.20):
86
+ fwd = df.Close.shift(-horizon)/df.Close-1
87
+ band = threshold*(atr/df.Close)*np.sqrt(horizon)
88
+ y = np.where(fwd > band, 1, np.where(fwd < -band, -1, 0))
89
+ return pd.Series(y, index=df.index), fwd
90
+
91
+
92
+ def pipe(k):
93
+ return Pipeline([
94
+ ("imputer", SimpleImputer(strategy="median")),
95
+ ("scale", StandardScaler()),
96
+ ("knn", KNeighborsClassifier(n_neighbors=int(k), weights="distance"))
97
+ ])
98
+
99
+
100
+ def tune_k(X, y):
101
+ # Inner chronological validation, with horizon purge to avoid overlapping labels.
102
+ cut = int(len(X)*0.80)
103
+ tr_end = cut-HORIZON
104
+ if tr_end <= max(K_GRID)+20 or len(X)-cut < 25:
105
+ return 9, []
106
+ scores = []
107
+ for k in K_GRID:
108
+ model = pipe(k)
109
+ model.fit(X.iloc[:tr_end], y.iloc[:tr_end])
110
+ pred = model.predict(X.iloc[cut:])
111
+ score = balanced_accuracy_score(y.iloc[cut:], pred)
112
+ scores.append((float(score), int(k)))
113
+ scores.sort(reverse=True)
114
+ return scores[0][1], scores
115
+
116
+
117
+ def calibrate_probabilities(raw_probs, raw_classes, y_cal):
118
+ # One-vs-rest isotonic calibration on a later, held-out calibration slice.
119
+ # Small calibration sets fall back to raw probabilities.
120
+ calibrated = np.zeros((len(raw_probs), len(CLASSES)), dtype=float)
121
+ for j, cls in enumerate(CLASSES):
122
+ if cls not in raw_classes:
123
+ continue
124
+ raw_col = raw_probs[:, list(raw_classes).index(cls)]
125
+ truth = (np.asarray(y_cal) == cls).astype(int)
126
+ if len(truth) >= 35 and len(np.unique(truth)) == 2:
127
+ iso = IsotonicRegression(out_of_bounds="clip")
128
+ iso.fit(raw_col, truth)
129
+ calibrated[:, j] = iso.predict(raw_col)
130
+ else:
131
+ calibrated[:, j] = raw_col
132
+ sums = calibrated.sum(axis=1, keepdims=True)
133
+ fallback = np.zeros_like(calibrated)
134
+ for j, cls in enumerate(CLASSES):
135
+ if cls in raw_classes:
136
+ fallback[:, j] = raw_probs[:, list(raw_classes).index(cls)]
137
+ fallback_sum = fallback.sum(axis=1, keepdims=True)
138
+ calibrated = np.where(sums > 0, calibrated/np.maximum(sums, 1e-12),
139
+ fallback/np.maximum(fallback_sum, 1e-12))
140
+ return calibrated
141
+
142
+
143
+ def barrier_result(df, atr_s, i, side, sl_atr, rr, horizon=HORIZON):
144
+ entry = float(df.Close.iloc[i])
145
+ av = float(atr_s.iloc[i])
146
+ if not np.isfinite(av) or av <= 0:
147
+ return None
148
+ stop_d = sl_atr*av
149
+ target_d = stop_d*rr
150
+ end = min(i+horizon, len(df)-1)
151
+ for j in range(i+1, end+1):
152
+ hi, lo = float(df.High.iloc[j]), float(df.Low.iloc[j])
153
+ if side == 1:
154
+ sl_hit, tp_hit = lo <= entry-stop_d, hi >= entry+target_d
155
+ else:
156
+ sl_hit, tp_hit = hi >= entry+stop_d, lo <= entry-target_d
157
+ # Daily candles do not reveal intraday order: resolve ambiguous bars against us.
158
+ if sl_hit and tp_hit:
159
+ return -stop_d/entry
160
+ if sl_hit:
161
+ return -stop_d/entry
162
+ if tp_hit:
163
+ return target_d/entry
164
+ return side*(float(df.Close.iloc[end])-entry)/entry
165
+
166
+
167
+ def trade_r_multiple(raw_return, entry, atr_value, sl_atr, fee_pct, slip_pct):
168
+ risk_fraction = sl_atr*atr_value/entry
169
+ round_trip_cost = 2*(fee_pct+slip_pct)/100
170
+ net_return = raw_return-round_trip_cost
171
+ return net_return/max(risk_fraction, 1e-12)
172
+
173
+
174
+ def optimize_barriers(df, atr_s, regimes, end_i, side, fee, slip, current_regime):
175
+ # Prefer observations from the current trend/volatility regime.
176
+ # If the regime has too few matured examples, fall back to all regimes.
177
+ results = []
178
+ start = max(80, end_i-650)
179
+ regime_indices = [
180
+ i for i in range(start, end_i-HORIZON)
181
+ if str(regimes.iloc[i]) == str(current_regime)
182
+ ]
183
+ if len(regime_indices) < 35:
184
+ regime_indices = list(range(start, end_i-HORIZON))
185
+ regime_used = "GLOBAL_FALLBACK"
186
+ else:
187
+ regime_used = str(current_regime)
188
+
189
+ for sl in SL_GRID:
190
+ for rr in RR_GRID:
191
+ vals = []
192
+ for i in regime_indices:
193
+ r = barrier_result(df, atr_s, i, side, sl, rr)
194
+ if r is not None:
195
+ vals.append(r-2*(fee+slip)/100)
196
+ if len(vals) >= 35:
197
+ a = np.asarray(vals)
198
+ downside = np.sqrt(np.mean(np.minimum(a, 0)**2))
199
+ score = a.mean()-0.35*downside
200
+ results.append((score, sl, rr, len(a), a.mean(), (a > 0).mean()))
201
+ if not results:
202
+ return 1.5, 2.0, None, [], regime_used
203
+ results.sort(reverse=True)
204
+ return float(results[0][1]), float(results[0][2]), results[0], results[:10], regime_used
205
+
206
+
207
+ def evaluate_oos(df, atr_s, X, y, regime_s, train_end, k, sl, rr, fee, slip):
208
+ # Model fit only on pre-OOS data; all test observations are later in time.
209
+ model = pipe(k)
210
+ model.fit(X.iloc[:train_end], y.iloc[:train_end])
211
+ test_start = train_end+HORIZON
212
+ test_end = len(X)-HORIZON
213
+ if test_end <= test_start+20:
214
+ return [], None, None, []
215
+ pred = model.predict(X.iloc[test_start:test_end])
216
+ actual = y.iloc[test_start:test_end].to_numpy()
217
+ bacc = balanced_accuracy_score(actual, pred)
218
+ acc = accuracy_score(actual, pred)
219
+ trades, details = [], []
220
+ for off, p in enumerate(pred):
221
+ if int(p) not in (-1, 1):
222
+ continue
223
+ i = test_start+off
224
+ raw = barrier_result(df, atr_s, i, int(p), sl, rr)
225
+ if raw is None:
226
+ continue
227
+ r = trade_r_multiple(raw, float(df.Close.iloc[i]), float(atr_s.iloc[i]),
228
+ sl, fee, slip)
229
+ trades.append(float(r))
230
+ details.append((df.index[i], int(p), float(r), regime_s.iloc[i]))
231
+ return trades, float(bacc), float(acc), details
232
+
233
+
234
+ def risk_path_mc(r_multiples, base_risk_pct, simulations=1000, n_trades=100):
235
+ # Bootstrap in R units. Equity sizing compounds and risk cuts after drawdown.
236
+ if len(r_multiples) < 10:
237
+ return None
238
+ rng = np.random.default_rng(20261103)
239
+ samples = rng.choice(np.asarray(r_multiples), size=(simulations, n_trades), replace=True)
240
+ finals, maxdds, ruin = [], [], []
241
+ for seq in samples:
242
+ eq, peak, mdd = 1.0, 1.0, 0.0
243
+ for r in seq:
244
+ dd = eq/peak-1
245
+ risk = base_risk_pct/100
246
+ if dd <= -0.20:
247
+ risk *= 0.20
248
+ elif dd <= -0.15:
249
+ risk *= 0.35
250
+ elif dd <= -0.10:
251
+ risk *= 0.50
252
+ elif dd <= -0.06:
253
+ risk *= 0.75
254
+ # Cap one-trade loss at 1R; profit remains proportional to R.
255
+ r_limited = max(float(r), -1.0)
256
+ eq *= max(0.01, 1 + risk*r_limited)
257
+ peak = max(peak, eq)
258
+ mdd = min(mdd, eq/peak-1)
259
+ finals.append(eq-1)
260
+ maxdds.append(mdd)
261
+ ruin.append(eq < 0.5)
262
+ return {
263
+ "median": float(np.median(finals)),
264
+ "p05": float(np.percentile(finals, 5)),
265
+ "p95": float(np.percentile(finals, 95)),
266
+ "dd95": float(np.percentile(maxdds, 5)),
267
+ "ruin": float(np.mean(ruin))
268
+ }
269
+
270
+
271
+ def adaptive_risk(min_risk, max_risk, bacc, agreement, expectancy_r, n_trades, drawdown):
272
+ # Explicit deterministic policy. Not represented as reinforcement learning.
273
+ quality = np.clip((bacc-0.33)/0.20, 0, 1) if bacc is not None else 0
274
+ agree = np.clip((agreement-0.33)/0.50, 0, 1)
275
+ edge = np.clip(expectancy_r/0.30, 0, 1)
276
+ sample = np.clip(n_trades/40, 0, 1)
277
+ score = 0.30*quality+0.25*agree+0.30*edge+0.15*sample
278
+ risk = min_risk+(max_risk-min_risk)*score
279
+ if drawdown <= -0.20:
280
+ risk = min_risk
281
+ elif drawdown <= -0.15:
282
+ risk = min_risk+(risk-min_risk)*0.20
283
+ elif drawdown <= -0.10:
284
+ risk = min_risk+(risk-min_risk)*0.40
285
+ elif drawdown <= -0.06:
286
+ risk = min_risk+(risk-min_risk)*0.70
287
+ return float(np.clip(risk, min_risk, max_risk)), float(score)
288
+
289
+
290
+ def run_engine(period, equity, min_risk, max_risk, max_leverage, fee, slip, train_window):
291
+ try:
292
+ df = get_btc(period)
293
+ if len(df) < 450:
294
+ raise ValueError("En az 450 günlük mum gerekli.")
295
+ # Exclude current possibly incomplete daily candle.
296
+ df = df.iloc[:-1].copy()
297
+ X_all, atr_all = make_features(df)
298
+ y_all, _ = make_target(df, atr_all)
299
+ regimes_all = regime_labels(X_all)
300
+
301
+ # Align rows and purge final horizon whose labels are not mature.
302
+ valid = X_all.notna().all(axis=1) & y_all.notna()
303
+ X = X_all.loc[valid].iloc[:-HORIZON].copy()
304
+ y = y_all.loc[X.index].astype(int).copy()
305
+ dfv = df.loc[X.index].copy()
306
+ atrv = atr_all.loc[X.index].copy()
307
+ regv = regimes_all.loc[X.index].copy()
308
+ if len(X) < 300:
309
+ raise ValueError("Temiz özelliklerden sonra yeterli veri kalmadı.")
310
+
311
+ # Reserve untouched final OOS block.
312
+ oos_n = min(180, max(75, int(len(X)*0.22)))
313
+ dev_end = len(X)-oos_n-HORIZON
314
+ if dev_end < 200:
315
+ raise ValueError("OOS ayrımı için yeterli geliştirme verisi yok.")
316
+
317
+ dev_X, dev_y = X.iloc[:dev_end], y.iloc[:dev_end]
318
+ if len(dev_X) > int(train_window):
319
+ dev_X, dev_y = dev_X.iloc[-int(train_window):], dev_y.iloc[-int(train_window):]
320
+ # Keep aligned development frame and indices.
321
+ dev_start_idx = X.index.get_loc(dev_X.index[0])
322
+ else:
323
+ dev_start_idx = 0
324
+
325
+ best_k, k_scores = tune_k(dev_X, dev_y)
326
+ # Calibration split is chronological and separate from fit subset.
327
+ cal_n = min(120, max(45, int(len(dev_X)*0.20)))
328
+ fit_end = len(dev_X)-cal_n-HORIZON
329
+ if fit_end <= 100:
330
+ raise ValueError("Kalibrasyon için eğitim verisi yetersiz.")
331
+ fit_X, fit_y = dev_X.iloc[:fit_end], dev_y.iloc[:fit_end]
332
+ cal_X, cal_y = dev_X.iloc[fit_end+HORIZON:], dev_y.iloc[fit_end+HORIZON:]
333
+ cal_model = pipe(best_k)
334
+ cal_model.fit(fit_X, fit_y)
335
+ raw_cal = cal_model.predict_proba(cal_X)
336
+ calibrated_cal = calibrate_probabilities(
337
+ raw_cal, cal_model.named_steps["knn"].classes_, cal_y.to_numpy()
338
+ )
339
+ # Current prediction from the same pre-calibration fit model.
340
+ current_x = X_all.iloc[[-1]]
341
+ raw_current = cal_model.predict_proba(current_x)[0:1]
342
+ # Fit isotonic calibrators on held-out calibration predictions.
343
+ calibrated_current = np.zeros((1, len(CLASSES)))
344
+ for j, cls in enumerate(CLASSES):
345
+ classes_now = list(cal_model.named_steps["knn"].classes_)
346
+ if cls not in classes_now:
347
+ continue
348
+ col = raw_cal[:, classes_now.index(cls)]
349
+ truth = (cal_y.to_numpy() == cls).astype(int)
350
+ raw_val = raw_current[:, classes_now.index(cls)]
351
+ if len(truth) >= 35 and len(np.unique(truth)) == 2:
352
+ iso = IsotonicRegression(out_of_bounds="clip")
353
+ iso.fit(col, truth)
354
+ calibrated_current[0, j] = iso.predict(raw_val)[0]
355
+ else:
356
+ calibrated_current[0, j] = raw_val[0]
357
+ if calibrated_current.sum() > 0:
358
+ calibrated_current /= calibrated_current.sum()
359
+ else:
360
+ calibrated_current[0] = [1/3, 1/3, 1/3]
361
+ pmap = {cls: float(calibrated_current[0, j]) for j, cls in enumerate(CLASSES)}
362
+ p_down, p_flat, p_up = pmap[-1], pmap[0], pmap[1]
363
+ side = 1 if p_up >= p_down else -1
364
+
365
+ # Neighbor agreement based on fit examples only.
366
+ imp, scaler = cal_model.named_steps["imputer"], cal_model.named_steps["scale"]
367
+ zfit = scaler.transform(imp.transform(fit_X))
368
+ zcur = scaler.transform(imp.transform(current_x))
369
+ nn = NearestNeighbors(n_neighbors=best_k).fit(zfit)
370
+ _, idx = nn.kneighbors(zcur)
371
+ agreement = float((fit_y.iloc[idx[0]].to_numpy() == side).mean())
372
+
373
+ # Optimize barriers on development data only, filtered to the current regime.
374
+ current_regime = str(regimes_all.iloc[-1])
375
+ dev_idx = dev_X.index
376
+ dev_df = df.loc[dev_idx]
377
+ dev_atr = atr_all.loc[dev_idx]
378
+ dev_reg = regimes_all.loc[dev_idx]
379
+ sl, rr, opt, top, regime_used = optimize_barriers(
380
+ dev_df, dev_atr, dev_reg, len(dev_df), side,
381
+ float(fee), float(slip), current_regime
382
+ )
383
+ regime_note = (
384
+ f"TP/SL optimizasyon rejimi: {regime_used}. "
385
+ "Yeterli rejim örneği yoksa tüm geliştirme dönemine geri dönülür."
386
+ )
387
+
388
+ # OOS evaluation on untouched tail. Purge gap separates train and test.
389
+ oos_X = X.iloc[dev_end+HORIZON:]
390
+ oos_y = y.iloc[dev_end+HORIZON:]
391
+ oos_df = df.loc[oos_X.index]
392
+ oos_atr = atr_all.loc[oos_X.index]
393
+ oos_reg = regimes_all.loc[oos_X.index]
394
+ oos_model = pipe(best_k)
395
+ oos_model.fit(dev_X, dev_y)
396
+ oos_pred = oos_model.predict(oos_X)
397
+ oos_bacc = balanced_accuracy_score(oos_y, oos_pred)
398
+ oos_acc = accuracy_score(oos_y, oos_pred)
399
+ r_values, oos_details = [], []
400
+ for i, pred in enumerate(oos_pred):
401
+ if int(pred) not in (-1, 1):
402
+ continue
403
+ raw_ret = barrier_result(oos_df, oos_atr, i, int(pred), sl, rr)
404
+ if raw_ret is None:
405
+ continue
406
+ r = trade_r_multiple(raw_ret, float(oos_df.Close.iloc[i]),
407
+ float(oos_atr.iloc[i]), sl, float(fee), float(slip))
408
+ r_values.append(r)
409
+ oos_details.append((oos_df.index[i], int(pred), r, str(oos_reg.iloc[i])))
410
+
411
+ expectancy_r = float(np.mean(r_values)) if r_values else 0.0
412
+ recent = df.Close.tail(30)
413
+ drawdown = float(recent.iloc[-1]/recent.cummax().iloc[-1]-1)
414
+ risk_pct, risk_score = adaptive_risk(
415
+ float(min_risk), float(max_risk), float(oos_bacc),
416
+ agreement, expectancy_r, len(r_values), drawdown
417
+ )
418
+ mc = risk_path_mc(r_values, risk_pct) if r_values else None
419
+
420
+ entry = float(df.Close.iloc[-1])
421
+ current_atr = float(atr_all.iloc[-1])
422
+ stop_d, target_d = sl*current_atr, sl*current_atr*rr
423
+ stop = entry-stop_d if side == 1 else entry+stop_d
424
+ target = entry+target_d if side == 1 else entry-target_d
425
+
426
+ # Position size: include estimated round-trip costs in risk per BTC.
427
+ risk_cash = float(equity)*risk_pct/100
428
+ cost_per_btc = stop_d+entry*2*(float(fee)+float(slip))/100
429
+ btc_qty = risk_cash/cost_per_btc if cost_per_btc > 0 else 0
430
+ notional = btc_qty*entry
431
+ max_notional = float(equity)*float(max_leverage)
432
+ capped = notional > max_notional
433
+ if capped:
434
+ btc_qty = max_notional/entry
435
+ notional = btc_qty*entry
436
+ actual_risk_cash = btc_qty*cost_per_btc
437
+ margin = notional/max(float(max_leverage), 1)
438
+
439
+ # Strong no-trade gate based on untouched OOS results.
440
+ trade_ok = (
441
+ max(p_up, p_down) >= 0.43
442
+ and abs(p_up-p_down) >= 0.12
443
+ and agreement >= 0.55
444
+ and oos_bacc >= 0.36
445
+ and len(r_values) >= 15
446
+ and expectancy_r > 0
447
+ and p_flat < 0.60
448
+ )
449
+ if not trade_ok:
450
+ signal = "WAIT — İŞLEM AÇMA"
451
+ btc_qty = notional = margin = actual_risk_cash = 0.0
452
+ else:
453
+ signal = "LONG" if side == 1 else "SHORT"
454
+
455
+ mc_text = (
456
+ f"100 işlem · 1.000 bootstrap senaryosu · medyan: {mc['median']:.2%} · "
457
+ f"%5 kötü sonuç: {mc['p05']:.2%} · %5 kuyruk max DD: {mc['dd95']:.2%} · "
458
+ f"%50 sermaye altına inme frekansı: {mc['ruin']:.1%}"
459
+ if mc else "Monte Carlo için yeterli OOS işlem yok."
460
+ )
461
+ k_text = " | ".join([f"K={k}: {s:.3f}" for s, k in sorted(k_scores, reverse=True)])
462
+ report = f"""
463
+ # {signal}
464
+
465
+ **Son kapanmış mum:** {df.index[-1].date()} · **Referans fiyat:** ${entry:,.2f}
466
+
467
+ ## Kalibre edilmiş KNN
468
+ | Ölçüm | Değer |
469
+ |---|---:|
470
+ | P(UP) | {p_up:.1%} |
471
+ | P(FLAT) | {p_flat:.1%} |
472
+ | P(DOWN) | {p_down:.1%} |
473
+ | Seçilen K | {best_k} |
474
+ | Komşu yön uyumu | {agreement:.1%} |
475
+ | OOS balanced accuracy | {oos_bacc:.3f} |
476
+ | OOS accuracy | {oos_acc:.3f} |
477
+ | OOS işlem sayısı | {len(r_values)} |
478
+ | OOS expectancy | {expectancy_r:.3f}R / işlem |
479
+ | Güncel rejim | {current_regime} |
480
+
481
+ **K seçim skorları:** {k_text}
482
+
483
+ ## TP / SL
484
+ | Parametre | Değer |
485
+ |---|---:|
486
+ | SL | {sl:.2f} ATR |
487
+ | TP | {rr:.2f}R |
488
+ | Stop-loss | ${stop:,.2f} |
489
+ | Take-profit | ${target:,.2f} |
490
+ | ATR(14) | ${current_atr:,.2f} |
491
+
492
+ {regime_note}
493
+
494
+ ## Gerçek risk-bütçeli pozisyon
495
+ | Parametre | Değer |
496
+ |---|---:|
497
+ | Hesap büyüklüğü | ${equity:,.2f} |
498
+ | Adaptif risk | {risk_pct:.3f}% |
499
+ | Risk bütçesi | ${risk_cash:,.2f} |
500
+ | BTC miktarı | {btc_qty:.8f} |
501
+ | Nominal pozisyon | ${notional:,.2f} |
502
+ | Tahmini teminat | ${margin:,.2f} |
503
+ | Tahmini stop kaybı + maliyet | ${actual_risk_cash:,.2f} |
504
+ | Kaldıraç sınırı | {"Uygulandı" if capped else "Uygulanmadı"} |
505
+ | Son 30 günlük kapanı�� DD | {drawdown:.2%} |
506
+
507
+ ## Monte Carlo — R-multiple + gerçek risk yüzdesi
508
+ {mc_text}
509
+ """
510
+ fig = go.Figure([go.Candlestick(
511
+ x=df.index[-150:], open=df.Open.tail(150), high=df.High.tail(150),
512
+ low=df.Low.tail(150), close=df.Close.tail(150), name="BTCUSD"
513
+ )])
514
+ if trade_ok:
515
+ fig.add_hline(y=stop, line_dash="dash", annotation_text="SL")
516
+ fig.add_hline(y=target, line_dash="dash", annotation_text="TP")
517
+ fig.update_layout(title="BTCUSD Daily — last 150 candles", height=520,
518
+ xaxis_rangeslider_visible=False)
519
+
520
+ top_df = pd.DataFrame(top, columns=[
521
+ "score", "SL_ATR", "TP_R", "samples", "mean_net_return", "win_rate"
522
+ ])
523
+ if not top_df.empty:
524
+ top_df["score"] = top_df.score.map(lambda x: f"{x:.5f}")
525
+ top_df["mean_net_return"] = top_df.mean_net_return.map(lambda x: f"{x:.3%}")
526
+ top_df["win_rate"] = top_df.win_rate.map(lambda x: f"{x:.1%}")
527
+
528
+ disclaimer = (
529
+ "**Araştırma prototipi; otomatik emir göndermez.** KNN olasılık kalibrasyonu "
530
+ "tek bir tarihsel kalibrasyon dilimine dayanır ve küçük örneklerde ham olasılığa "
531
+ "geri döner. TP/SL optimizasyonu geliştirme döneminde yapılır; son dönem OOS "
532
+ "olarak ayrılır. Rejim etiketleri geliştirme verisinin volatilite kantillerine "
533
+ "dayanır; bu nedenle canlı dağılım değişebilir. Monte Carlo, OOS R-multiple "
534
+ "serisini bootstrap eder ve işlem başına gerçek risk yüzdesini bileşik uygular; "
535
+ "bağımsızlık varsayımı seri korelasyonunu ve rejim değişimini tam modellemez. "
536
+ "Pozisyon hesabı BTC spot miktarı varsayar; CFD lot sözleşmesi, spread, funding, "
537
+ "swap, komisyon, minimum lot, marjin ve broker fiyatı farklı olabilir. Gap ve "
538
+ "kayma kaybı planlanan riskin üstüne çıkarabilir. Canlı hesap öncesi paper trading "
539
+ "ve broker bazlı doğrulama zorunludur."
540
+ )
541
+ return report, fig, top_df, disclaimer
542
+
543
+ except Exception as exc:
544
+ return f"# Hata\n{type(exc).__name__}: {exc}", None, pd.DataFrame(), \
545
+ "Veri bağlantısını ve parametreleri kontrol edin."
546
+
547
+
548
+ with gr.Blocks(title=APP_TITLE, theme=gr.themes.Soft()) as demo:
549
+ gr.Markdown(
550
+ "# GOSHAWK — Adaptive BTCUSD Risk Engine v3\n"
551
+ "Kalibre edilmiş KNN · adaptif K · TP/SL optimizasyonu · bağımsız OOS · "
552
+ "R-multiple Monte Carlo · drawdown bazlı risk."
553
+ )
554
+ with gr.Row():
555
+ with gr.Column():
556
+ period = gr.Dropdown(["2y", "5y", "max"], value="5y", label="Veri geçmişi")
557
+ train_window = gr.Slider(300, 1500, value=900, step=50, label="Maksimum eğitim penceresi")
558
+ equity = gr.Number(value=10000, label="Hesap büyüklüğü (USD)")
559
+ max_leverage = gr.Slider(1, 10, value=3, step=1, label="Azami kaldıraç")
560
+ with gr.Column():
561
+ min_risk = gr.Slider(0.05, 0.5, value=0.10, step=0.05, label="Minimum risk (%)")
562
+ max_risk = gr.Slider(0.25, 1.5, value=0.75, step=0.05, label="Maksimum risk (%)")
563
+ fee = gr.Number(value=0.05, label="Tek yön komisyon (%)")
564
+ slip = gr.Number(value=0.03, label="Tek yön slippage (%)")
565
+ run = gr.Button("Öğren, optimize et ve test et", variant="primary")
566
+ output = gr.Markdown()
567
+ chart = gr.Plot()
568
+ optimization = gr.Dataframe(label="En iyi TP/SL kombinasyonları", interactive=False)
569
+ disclaimer = gr.Markdown()
570
+ run.click(run_engine,
571
+ inputs=[period, equity, min_risk, max_risk, max_leverage, fee, slip, train_window],
572
+ outputs=[output, chart, optimization, disclaimer])
573
+
574
+ if __name__ == "__main__":
575
+ demo.launch(server_name="0.0.0.0", server_port=7860)