GoshawkVortexAI commited on
Commit
a88bc98
·
verified ·
1 Parent(s): 6bcb7d9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +477 -529
app.py CHANGED
@@ -1,6 +1,6 @@
1
  # app.py
2
- # GOSHAWK ALBION MARKET AI
3
- # Kalite ayrımı + uzun vadeli trend (7/14 gün) + alış-satış spread feature'ları
4
 
5
  import os
6
  import time
@@ -15,7 +15,7 @@ import plotly.graph_objects as go
15
  import plotly.express as px
16
 
17
  import lightgbm as lgb
18
- from sklearn.metrics import accuracy_score
19
 
20
  import gradio as gr
21
 
@@ -38,10 +38,6 @@ CITIES = [
38
  "Martlock", "Thetford", "Caerleon",
39
  ]
40
 
41
- # Kalite seviyeleri (1=Normal, 2=Good, 3=Outstanding, 4=Excellent, 5=Masterpiece)
42
- QUALITIES = ["1", "2", "3", "4", "5"]
43
-
44
- # Kalite kodları → insan-okunur
45
  QUALITY_NAMES = {1: "Normal", 2: "Good", 3: "Outstanding", 4: "Excellent", 5: "Masterpiece"}
46
 
47
  _ITEM_POOL = [
@@ -108,32 +104,36 @@ DATA_DIR = "gos_hawk_data"
108
  os.makedirs(DATA_DIR, exist_ok=True)
109
  MODEL_DIR = os.path.join(DATA_DIR, "models")
110
  os.makedirs(MODEL_DIR, exist_ok=True)
111
- MODEL_PATH = os.path.join(MODEL_DIR, "gos_hawk_model.pkl")
 
112
 
113
  MIN_TRAIN_SAMPLES = 150
114
  REQUEST_TIMEOUT = 60
115
  MAX_RETRIES = 3
116
  RETRY_BACKOFF = 2
117
 
118
- HISTORY_TIME_SCALE = 1
119
 
120
- CLASS_DOWN = -0.03
121
- CLASS_UP = 0.03
 
 
122
 
123
- TARGET_TOLERANCE_HOURS = 12
 
 
 
 
124
 
125
- # Dip alım
126
  DIP_RSI_MAX = 40.0
127
  DIP_PERCENTILE_MAX = 0.25
128
  DIP_MIN_VOLUME_RATIO = 1.0
129
  DIP_MIN_PREDICTED_PCT = -1.0
130
  DIP_MAX_SPREAD_PCT = 0.15
131
 
132
- # Uzun vadeli lag/chg pencereleri
133
- LONG_TERM_WINDOWS = [24, 168, 336] # 1g, 7g, 14g
134
-
135
- # Feature kolonları (spread + uzun vadeli dahil)
136
- FEATURE_COLS = [
137
  "hour", "dayofweek",
138
  "lag_24h_log", "lag_168h_log", "lag_336h_log",
139
  "chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h",
@@ -145,13 +145,27 @@ FEATURE_COLS = [
145
  "item_code", "city_code",
146
  ]
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  # ---------------------------------------------------------------------------
149
  # PUBLIC API
150
  # ---------------------------------------------------------------------------
151
 
152
 
153
  def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
154
- headers = {"User-Agent": "GOSHAWK-ALBION-MARKET-AI/2.0", "Accept": "application/json"}
155
  for attempt in range(MAX_RETRIES):
156
  try:
157
  resp = requests.get(url, params=params, headers=headers, timeout=REQUEST_TIMEOUT)
@@ -164,18 +178,18 @@ def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
164
  time.sleep(RETRY_BACKOFF * (attempt + 1))
165
  continue
166
  else:
167
- print(f"API hatası: {resp.status_code} - {url}")
168
  return None
169
  except requests.exceptions.Timeout:
170
- print(f"Zaman aşımı: {url}")
171
  except requests.exceptions.RequestException as e:
172
- print(f"İstek hatası: {e}")
173
  time.sleep(RETRY_BACKOFF * (attempt + 1))
174
  return None
175
 
176
 
177
- def fetch_current_prices(item_ids: List[str], cities: List[str], qualities: str = "1,2,3,4,5") -> pd.DataFrame:
178
- """Anlık fiyatlar — tüm kaliteler."""
179
  all_data = []
180
  for i in range(0, len(item_ids), 40):
181
  chunk = item_ids[i:i+40]
@@ -197,13 +211,15 @@ def fetch_current_prices(item_ids: List[str], cities: List[str], qualities: str
197
  return df
198
 
199
 
200
- def fetch_history(item_ids: List[str], cities: List[str], time_scale: int = 1, qualities: str = "1,2,3,4,5") -> pd.DataFrame:
201
- """Tarihsel fiyatlar — tüm kaliteler."""
 
202
  all_records = []
203
  for i in range(0, len(item_ids), 15):
204
  chunk = item_ids[i:i+15]
205
  url = f"{API_BASE}/stats/history/{','.join(chunk)}.json"
206
- params = {"locations": ",".join(cities), "qualities": qualities, "time-scale": time_scale}
 
207
  data = safe_request(url, params)
208
  if data:
209
  for entry in data:
@@ -254,7 +270,6 @@ def clean_market_data(df: pd.DataFrame) -> pd.DataFrame:
254
  df = df[df["avg_price"] > 0].copy()
255
  df = df.dropna(subset=["timestamp"]).copy()
256
 
257
- # Kalite bazlı temizlik (item+city+quality bazında)
258
  for (item, qual) in df[["item_id", "quality"]].drop_duplicates().itertuples(index=False):
259
  mask = (df["item_id"] == item) & (df["quality"] == qual)
260
  if mask.sum() < 10:
@@ -274,7 +289,7 @@ def save_to_csv(df: pd.DataFrame, name: str) -> str:
274
  try:
275
  df.to_csv(path, index=False)
276
  except Exception as e:
277
- print(f"CSV yazma hatası: {e}")
278
  return ""
279
  return path
280
 
@@ -292,11 +307,19 @@ def compute_rsi(series: pd.Series, period: int = 14) -> pd.Series:
292
  return (100 - (100 / (1 + rs))).fillna(50)
293
 
294
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  def build_features(df: pd.DataFrame) -> pd.DataFrame:
296
- """
297
- Kalite ayrımı + uzun vadeli trend + spread feature'ları.
298
- group_cols = [item_id, city, quality] → kalite farkı artık karışmıyor.
299
- """
300
  if df.empty:
301
  return df
302
 
@@ -309,24 +332,21 @@ def build_features(df: pd.DataFrame) -> pd.DataFrame:
309
 
310
  df["hour"] = df["timestamp"].dt.hour
311
  df["dayofweek"] = df["timestamp"].dt.dayofweek
 
312
  df["is_weekend"] = (df["dayofweek"] >= 5).astype(int)
313
  df["qual_code"] = df["quality"].astype(int)
314
 
315
- # KRİTİK: kalite artık grup içinde
316
  group_cols = ["item_id", "city", "quality"]
317
-
318
  df["log_price"] = np.log(df["avg_price"].replace(0, np.nan))
319
 
320
- # Uzun vadeli lag'ler
321
  df["lag_24h_log"] = df.groupby(group_cols)["log_price"].shift(24)
322
  df["lag_168h_log"] = df.groupby(group_cols)["log_price"].shift(168)
323
  df["lag_336h_log"] = df.groupby(group_cols)["log_price"].shift(336)
324
 
325
- # Değişimler
326
  for chg in [1, 6, 24, 168, 336]:
327
  df[f"chg_{chg}h"] = df.groupby(group_cols)["log_price"].shift(chg)
328
 
329
- # MA oranları
330
  df["ma_24h_raw"] = df.groupby(group_cols)["avg_price"].transform(
331
  lambda x: x.rolling(24, min_periods=6).mean()
332
  )
@@ -337,82 +357,61 @@ def build_features(df: pd.DataFrame) -> pd.DataFrame:
337
  )
338
  df["ma_168h_ratio"] = df["ma_168h_raw"] / df["avg_price"].replace(0, np.nan) - 1.0
339
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  df["rsi_14"] = df.groupby(group_cols)["avg_price"].transform(lambda x: compute_rsi(x, 14))
341
  df["volatility_24h"] = df.groupby(group_cols)["log_price"].transform(
342
  lambda x: x.rolling(24, min_periods=6).std()
343
  )
 
 
 
344
  df["volume_ma_24h"] = df.groupby(group_cols)["item_count"].transform(
345
  lambda x: x.rolling(24, min_periods=6).mean()
346
  )
347
  df["volume_ratio"] = df["item_count"] / df["volume_ma_24h"].replace(0, np.nan)
348
 
349
- # Şehir fiyat farkı (aynı item+quality+timestamp içinde)
 
 
 
 
350
  city_avg = df.groupby(["item_id", "quality", "timestamp"])["avg_price"].transform("mean")
351
  df["city_price_deviation"] = (df["avg_price"] - city_avg) / city_avg.replace(0, np.nan)
352
 
353
- # 7g yüzdelik konum
354
  df["price_pct_7d"] = df.groupby(group_cols)["avg_price"].transform(
355
  lambda x: x.rolling(168, min_periods=24).apply(
356
  lambda arr: (arr[-1] - np.min(arr)) / (np.max(arr) - np.min(arr) + 1e-9),
357
  raw=True
358
  )
359
  )
360
-
361
  df["is_downtrend"] = ((df["chg_6h"] < 0) & (df["chg_24h"] < 0)).astype(int)
362
 
363
- # SPREAD feature'ları — prices_df'ten merge edilecek
364
- # Şimdilik NaN, sonra enrich_spread_features ile doldurulacak
365
-
366
- df = _compute_forward_target(df)
367
  return df
368
 
369
 
370
- def enrich_spread_features(featured_df: pd.DataFrame, prices_df: pd.DataFrame) -> pd.DataFrame:
371
- """
372
- Anlık alış-satış spread'i feature olarak ekle.
373
- prices_df'te kalite ayrımı varsa ona göre merge.
374
- """
375
- if featured_df.empty or prices_df.empty:
376
- featured_df["spread_pct"] = 0.0
377
- featured_df["buy_sell_ratio"] = 1.0
378
- return featured_df
379
-
380
- prices = prices_df.copy()
381
- if "quality" not in prices.columns:
382
- prices["quality"] = 1
383
- prices["quality"] = pd.to_numeric(prices["quality"], errors="coerce").fillna(1).astype(int)
384
-
385
- prices["sell_price_min"] = pd.to_numeric(prices["sell_price_min"], errors="coerce").astype("float64")
386
- prices["buy_price_max"] = pd.to_numeric(prices["buy_price_max"], errors="coerce").astype("float64")
387
-
388
- prices["spread_pct"] = np.where(
389
- prices["buy_price_max"] > 0,
390
- (prices["sell_price_min"] - prices["buy_price_max"]) / prices["buy_price_max"],
391
- np.nan
392
- )
393
- prices["buy_sell_ratio"] = np.where(
394
- prices["sell_price_min"] > 0,
395
- prices["buy_price_max"] / prices["sell_price_min"],
396
- np.nan
397
- )
398
-
399
- price_features = prices[["item_id", "city", "quality", "spread_pct", "buy_sell_ratio"]].drop_duplicates(
400
- subset=["item_id", "city", "quality"]
401
- )
402
-
403
- result = featured_df.merge(price_features, on=["item_id", "city", "quality"], how="left")
404
- result["spread_pct"] = result["spread_pct"].fillna(0.0)
405
- result["buy_sell_ratio"] = result["buy_sell_ratio"].fillna(1.0)
406
- return result
407
-
408
-
409
- def _compute_forward_target(df: pd.DataFrame) -> pd.DataFrame:
410
  if df.empty or "log_price" not in df.columns:
411
  df["target_log_return_24h"] = np.nan
412
- df["target_class"] = np.nan
 
 
413
  return df
414
 
415
  df["target_log_return_24h"] = np.nan
 
 
416
  group_cols = ["item_id", "city", "quality"]
417
 
418
  for keys, g in df.groupby(group_cols):
@@ -430,153 +429,220 @@ def _compute_forward_target(df: pd.DataFrame) -> pd.DataFrame:
430
  "log_price_future": g["log_price"].values,
431
  }).sort_values("timestamp")
432
 
433
- left["target_timestamp"] = left["timestamp"] + pd.Timedelta(hours=24)
 
 
 
 
 
 
 
 
 
 
 
 
 
434
 
435
- merged = pd.merge_asof(
436
- left.sort_values("target_timestamp"),
 
 
437
  right.sort_values("timestamp"),
438
- left_on="target_timestamp",
439
  right_on="timestamp",
440
  direction="nearest",
441
- tolerance=pd.Timedelta(hours=TARGET_TOLERANCE_HOURS),
442
- suffixes=("", "_future"),
443
  )
 
 
 
 
 
 
 
444
 
445
- target_log = merged["log_price_future"].values - merged["log_price_now"].values
446
- df.loc[merged["idx"].values, "target_log_return_24h"] = target_log
 
447
 
448
- t = df["target_log_return_24h"]
449
- df["target_class"] = np.where(t < CLASS_DOWN, 0, np.where(t > CLASS_UP, 2, 1))
450
- df.loc[t.isna(), "target_class"] = np.nan
451
  return df
452
 
453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  # ---------------------------------------------------------------------------
455
- # MODEL
456
  # ---------------------------------------------------------------------------
457
 
458
 
459
- def prepare_model_data(featured_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.Series, pd.DataFrame]:
 
460
  if featured_df.empty:
461
  return pd.DataFrame(), pd.Series(dtype=int), pd.DataFrame()
462
 
 
 
 
 
463
  df = featured_df.copy()
464
  df["item_code"] = df["item_id"].astype("category").cat.codes
465
  df["city_code"] = df["city"].astype("category").cat.codes
466
  if "qual_code" not in df.columns:
467
  df["qual_code"] = df.get("quality", 1)
468
 
469
- df = df.dropna(subset=["target_class", "lag_24h_log"]).copy()
470
- df["target_class"] = df["target_class"].astype(int)
471
 
472
- # Uzun pencereli feature'ları doldur (kısa pencereden)
473
- for col in ["chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h"]:
474
- if col in df.columns:
475
- df[col] = df[col].fillna(0.0)
 
476
 
477
- df["lag_168h_log"] = df["lag_168h_log"].fillna(df["lag_24h_log"])
478
- df["lag_336h_log"] = df["lag_336h_log"].fillna(df["lag_168h_log"]).fillna(df["lag_24h_log"])
 
479
 
480
- df["rsi_14"] = df["rsi_14"].fillna(50.0)
481
- df["volatility_24h"] = df["volatility_24h"].fillna(0.0)
482
- df["volume_ratio"] = df["volume_ratio"].fillna(1.0)
483
- df["ma_24h_ratio"] = df["ma_24h_ratio"].fillna(0.0)
484
- df["ma_168h_ratio"] = df["ma_168h_ratio"].fillna(df["ma_24h_ratio"]).fillna(0.0)
485
- df["city_price_deviation"] = df["city_price_deviation"].fillna(0.0)
486
- df["spread_pct"] = df.get("spread_pct", pd.Series(0.0, index=df.index)).fillna(0.0)
487
- df["buy_sell_ratio"] = df.get("buy_sell_ratio", pd.Series(1.0, index=df.index)).fillna(1.0)
488
 
489
- model_df = df.dropna(subset=FEATURE_COLS + ["target_class"]).copy()
 
490
 
491
- X = model_df[FEATURE_COLS].astype("float64")
492
- y = model_df["target_class"]
493
- meta = model_df[["item_id", "city", "quality", "timestamp", "avg_price", "target_log_return_24h"]].copy()
 
 
 
 
494
  return X, y, meta
495
 
496
 
497
- def train_global_model(X: pd.DataFrame, y: pd.Series) -> Optional[lgb.LGBMClassifier]:
498
  if len(X) < MIN_TRAIN_SAMPLES:
499
  return None
500
-
501
- class_counts = y.value_counts().to_dict()
502
- print(f"[TANI] Sınıf: {class_counts}")
503
- if len(class_counts) < 3 or min(class_counts.values()) < 10:
504
  return None
505
 
506
- split_idx = int(len(X) * 0.8)
507
- X_train, X_val = X.iloc[:split_idx], X.iloc[split_idx:]
508
- y_train, y_val = y.iloc[:split_idx], y.iloc[split_idx:]
509
 
 
510
  model = lgb.LGBMClassifier(
511
  objective="multiclass", num_class=3, metric="multi_logloss",
512
  boosting_type="gbdt",
513
  num_leaves=15, max_depth=5, min_child_samples=50,
514
- learning_rate=0.03, n_estimators=300,
515
  feature_fraction=0.8, bagging_fraction=0.8, bagging_freq=5,
516
  reg_alpha=0.5, reg_lambda=1.0,
517
  verbose=-1, random_state=42, n_jobs=-1,
518
  )
519
-
520
  model.fit(
521
- X_train, y_train,
522
  eval_set=[(X_val, y_val)],
523
  callbacks=[lgb.early_stopping(30, verbose=False), lgb.log_evaluation(0)],
524
  )
525
-
526
- val_preds = model.predict(X_val)
527
- val_acc = accuracy_score(y_val, val_preds)
528
- print(f"[TANI] val_accuracy={val_acc:.4f}")
529
  return model
530
 
531
 
532
- def predict_24h_returns(model, latest_features: pd.DataFrame) -> pd.DataFrame:
533
  if latest_features.empty or model is None:
534
  return pd.DataFrame()
535
 
536
- missing = [c for c in FEATURE_COLS if c not in latest_features.columns]
537
- if missing:
538
- return pd.DataFrame()
 
 
539
 
540
- X_pred = latest_features.copy()
541
- for col in ["chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h"]:
542
- if col in X_pred.columns:
543
- X_pred[col] = X_pred[col].fillna(0.0)
544
- X_pred["lag_168h_log"] = X_pred["lag_168h_log"].fillna(X_pred["lag_24h_log"])
545
- X_pred["lag_336h_log"] = X_pred["lag_336h_log"].fillna(X_pred["lag_168h_log"]).fillna(X_pred["lag_24h_log"])
546
- X_pred["rsi_14"] = X_pred["rsi_14"].fillna(50.0)
547
- X_pred["volatility_24h"] = X_pred["volatility_24h"].fillna(0.0)
548
- X_pred["volume_ratio"] = X_pred["volume_ratio"].fillna(1.0)
549
- X_pred["ma_24h_ratio"] = X_pred["ma_24h_ratio"].fillna(0.0)
550
- X_pred["ma_168h_ratio"] = X_pred["ma_168h_ratio"].fillna(X_pred["ma_24h_ratio"]).fillna(0.0)
551
- X_pred["city_price_deviation"] = X_pred["city_price_deviation"].fillna(0.0)
552
- X_pred["spread_pct"] = X_pred.get("spread_pct", pd.Series(0.0, index=X_pred.index)).fillna(0.0)
553
- X_pred["buy_sell_ratio"] = X_pred.get("buy_sell_ratio", pd.Series(1.0, index=X_pred.index)).fillna(1.0)
554
-
555
- X_ready = X_pred[FEATURE_COLS].astype("float64").dropna()
 
 
556
  if X_ready.empty:
557
  return pd.DataFrame()
558
 
559
  probs = model.predict_proba(X_ready)
560
  pred_class = np.argmax(probs, axis=1)
561
- expected_log = probs[:, 0] * CLASS_DOWN + probs[:, 1] * 0.0 + probs[:, 2] * CLASS_UP
562
- clipped_log = np.clip(expected_log, CLASS_DOWN, CLASS_UP)
563
- pred_return_pct = (np.exp(clipped_log) - 1) * 100
564
-
565
- keep_cols = ["item_id", "city", "quality", "avg_price", "item_count",
566
- "volatility_24h", "rsi_14", "volume_ratio", "price_pct_7d",
567
- "is_downtrend", "chg_6h", "chg_24h", "chg_168h",
568
- "spread_pct", "buy_sell_ratio"]
569
- keep_cols = [c for c in keep_cols if c in X_pred.columns]
570
- result = X_pred.loc[X_ready.index, keep_cols].copy()
571
- result["pred_class"] = pred_class
572
- result["prob_down"] = probs[:, 0]
573
- result["prob_flat"] = probs[:, 1]
574
- result["prob_up"] = probs[:, 2]
575
- result["predicted_log_return_24h"] = clipped_log
576
- result["predicted_return_pct"] = pred_return_pct
577
- result["predicted_price_24h"] = result["avg_price"] * np.exp(clipped_log)
578
- result["confidence"] = probs.max(axis=1)
579
- return result
 
 
 
 
580
 
581
 
582
  # ---------------------------------------------------------------------------
@@ -586,34 +652,28 @@ def predict_24h_returns(model, latest_features: pd.DataFrame) -> pd.DataFrame:
586
 
587
  def compute_dip_signal(row: pd.Series) -> Tuple[str, str]:
588
  reasons, passed = [], 0
589
-
590
  rsi = row.get("rsi_14", 50)
591
  if pd.notna(rsi) and rsi < DIP_RSI_MAX:
592
  passed += 1
593
  reasons.append(f"RSI={rsi:.0f}")
594
-
595
  pct = row.get("price_pct_7d", 0.5)
596
  if pd.notna(pct) and pct < DIP_PERCENTILE_MAX:
597
  passed += 1
598
  reasons.append(f"7g%={pct*100:.0f}")
599
-
600
  vr = row.get("volume_ratio", 1.0)
601
  if pd.notna(vr) and vr >= DIP_MIN_VOLUME_RATIO:
602
  passed += 1
603
  reasons.append(f"H×{vr:.1f}")
604
-
605
- pred_pct = row.get("predicted_return_pct", 0)
606
- if pd.notna(pred_pct) and pred_pct >= DIP_MIN_PREDICTED_PCT:
607
  passed += 1
608
- reasons.append(f"M={pred_pct:+.1f}%")
609
-
610
- spread_pct = row.get("spread_pct", np.nan)
611
- if pd.notna(spread_pct) and abs(spread_pct) < DIP_MAX_SPREAD_PCT:
612
  passed += 1
613
- reasons.append(f"Sp={spread_pct*100:.1f}%")
614
-
615
- is_down = row.get("is_downtrend", 0)
616
- if not is_down or (pd.notna(pred_pct) and pred_pct > 2):
617
  passed += 1
618
  reasons.append("Trend✓")
619
 
@@ -623,117 +683,13 @@ def compute_dip_signal(row: pd.Series) -> Tuple[str, str]:
623
  sig = "🟡 ZAYIF AL"
624
  elif passed == 3 and pd.notna(rsi) and rsi < 45:
625
  sig = "🔵 İZLE"
626
- elif is_down and pd.notna(pred_pct) and pred_pct < -2:
627
  sig = "🔴 ALMA"
628
  else:
629
  sig = "⚪ BEKLE"
630
-
631
  return sig, " · ".join(reasons) if reasons else "—"
632
 
633
 
634
- # ---------------------------------------------------------------------------
635
- # BACKTEST
636
- # ---------------------------------------------------------------------------
637
-
638
-
639
- def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
640
- if featured_df.empty:
641
- return {"error": "Veri yok."}
642
-
643
- df = featured_df.sort_values("timestamp").copy()
644
- unique_times = df["timestamp"].sort_values().unique()
645
-
646
- if len(unique_times) < 40:
647
- return {"error": f"Yeterli zaman noktası yok ({len(unique_times)} < 40)."}
648
-
649
- test_size = len(unique_times) // (n_splits + 1)
650
- if test_size < 5:
651
- return {"error": "Test boyutu çok küçük."}
652
-
653
- results, all_predictions = [], []
654
-
655
- for i in range(n_splits):
656
- test_start_idx = test_size * (i + 1) + 24
657
- test_end_idx = test_start_idx + test_size
658
- if test_end_idx > len(unique_times):
659
- break
660
- train_end_idx = test_start_idx - 24
661
- if train_end_idx <= 0:
662
- continue
663
-
664
- train_times = unique_times[:train_end_idx]
665
- test_times = unique_times[test_start_idx:test_end_idx]
666
-
667
- train_df = df[df["timestamp"].isin(train_times)]
668
- test_df = df[df["timestamp"].isin(test_times)]
669
-
670
- if len(train_df) < 50 or len(test_df) < 5:
671
- continue
672
-
673
- X_train, y_train, _ = prepare_model_data(train_df)
674
- X_test, y_test, meta_test = prepare_model_data(test_df)
675
-
676
- if len(X_train) < 30 or len(X_test) < 3 or y_train.nunique() < 3:
677
- continue
678
-
679
- model = lgb.LGBMClassifier(
680
- objective="multiclass", num_class=3,
681
- num_leaves=15, max_depth=5, min_child_samples=50,
682
- learning_rate=0.03, n_estimators=200,
683
- reg_alpha=0.5, reg_lambda=1.0,
684
- verbose=-1, random_state=42, n_jobs=-1,
685
- )
686
- model.fit(X_train, y_train)
687
-
688
- preds_class = model.predict(X_test)
689
- probs = model.predict_proba(X_test)
690
-
691
- accuracy = accuracy_score(y_test, preds_class)
692
-
693
- y_dir = np.where(y_test == 2, 1, np.where(y_test == 0, 0, -1))
694
- p_dir = np.where(preds_class == 2, 1, np.where(preds_class == 0, 0, -1))
695
- mask = (y_dir >= 0) & (p_dir >= 0)
696
- direction_acc = float(np.mean(y_dir[mask] == p_dir[mask])) if mask.sum() > 0 else 0.5
697
-
698
- baseline_acc = float((y_test == 1).mean())
699
-
700
- results.append({
701
- "split": i + 1,
702
- "train_samples": int(len(X_train)),
703
- "test_samples": int(len(X_test)),
704
- "accuracy": round(float(accuracy), 4),
705
- "direction_accuracy": round(float(direction_acc), 4),
706
- "baseline_accuracy": round(float(baseline_acc), 4),
707
- "accuracy_lift_pct": round(float((accuracy - baseline_acc) * 100), 2),
708
- })
709
-
710
- test_meta = meta_test.copy()
711
- test_meta["predicted_class"] = preds_class
712
- test_meta["prob_up"] = probs[:, 2]
713
- all_predictions.append(test_meta)
714
-
715
- if not results:
716
- return {"error": "Backtest tamamlanamadı."}
717
-
718
- simulation = {"total_trades": 0, "win_rate_pct": 0.0, "avg_net_return_pct": 0.0,
719
- "profitable_trades": 0, "avg_confidence_pct": 0.0}
720
- if all_predictions:
721
- sim_df = pd.concat(all_predictions).dropna(subset=["target_log_return_24h"])
722
- # Sadece YUKARI tahmini + yüksek güven
723
- buys = sim_df[(sim_df["predicted_class"] == 2) & (sim_df["prob_up"] > 0.6)].copy()
724
- if len(buys) > 0:
725
- buys["net_return"] = buys["target_log_return_24h"] - TOTAL_TRADE_COST
726
- simulation = {
727
- "total_trades": int(len(buys)),
728
- "win_rate_pct": round(float((buys["net_return"] > 0).mean() * 100), 2),
729
- "avg_net_return_pct": round(float(buys["net_return"].mean() * 100), 4),
730
- "profitable_trades": int((buys["net_return"] > 0).sum()),
731
- "avg_confidence_pct": round(float(buys["prob_up"].mean() * 100), 1),
732
- }
733
-
734
- return {"results": results, "simulation": simulation, "n_splits_completed": len(results)}
735
-
736
-
737
  # ---------------------------------------------------------------------------
738
  # PIPELINE
739
  # ---------------------------------------------------------------------------
@@ -744,16 +700,20 @@ class DataPipeline:
744
  self.history_df = pd.DataFrame()
745
  self.prices_df = pd.DataFrame()
746
  self.featured_df = pd.DataFrame()
747
- self.model = None
 
748
  self.last_update = None
749
  self.status_message = "Veri yüklenmedi."
750
- self._backtest_cache = None
751
  self._diagnostics = {}
752
 
753
- if _HAS_JOBLIB and os.path.exists(MODEL_PATH):
754
  try:
755
- self.model = joblib.load(MODEL_PATH)
756
- self.status_message = "Kayıtlı model yüklendi."
 
 
 
 
757
  except Exception:
758
  pass
759
 
@@ -761,7 +721,7 @@ class DataPipeline:
761
  item_ids = item_ids or ITEM_IDS
762
  cities = cities or CITIES
763
 
764
- self.status_message = "Public API'den veri çekiliyor (5 kalite)..."
765
  ok, msg = check_api_health()
766
  if not ok:
767
  self.status_message = f"API erişilemiyor: {msg}"
@@ -780,15 +740,14 @@ class DataPipeline:
780
 
781
  self.history_df = clean_market_data(hist)
782
  self.prices_df = prices
783
- n_qualities = self.history_df["quality"].nunique() if "quality" in self.history_df.columns else 1
784
- self.status_message = f"✓ {len(self.history_df)} satır tarihsel ({n_qualities} kalite), {len(self.prices_df)} anlık."
785
 
786
  save_to_csv(self.history_df, "history")
787
  save_to_csv(self.prices_df, "prices")
788
 
789
  self.featured_df = build_features(self.history_df)
790
  self.featured_df = enrich_spread_features(self.featured_df, self.prices_df)
791
- self._backtest_cache = None
792
  self._update_diagnostics()
793
 
794
  def _update_diagnostics(self):
@@ -796,94 +755,95 @@ class DataPipeline:
796
  if df.empty:
797
  self._diagnostics = {}
798
  return
799
- n_with_target = int(df["target_class"].notna().sum()) if "target_class" in df.columns else 0
800
- class_dist = df["target_class"].value_counts().to_dict() if n_with_target > 0 else {}
 
 
801
  self._diagnostics = {
802
  "n_total": len(df),
803
- "n_with_target": n_with_target,
804
- "class_dist": class_dist,
805
- "n_qualities": int(df["quality"].nunique()) if "quality" in df.columns else 1,
 
806
  }
807
- print(f"[TANI] total={len(df)}, hedefli={n_with_target}, sınıf={class_dist}")
808
 
809
- def train_model(self):
810
  if self.featured_df.empty:
811
  self.status_message = "Önce veri çekin."
812
  return False
813
 
814
- diag = self._diagnostics or {}
815
- if diag.get("n_with_target", 0) < MIN_TRAIN_SAMPLES:
816
- self.status_message = f"Yetersiz örnek: {diag.get('n_with_target', 0)}"
817
- return False
818
-
819
- class_dist = diag.get("class_dist", {})
820
- if len(class_dist) < 3 or min(class_dist.values()) < 10:
821
- self.status_message = f"Sınıf dengesiz: {class_dist}"
822
- return False
823
-
824
- X, y, _ = prepare_model_data(self.featured_df)
825
- if len(X) < MIN_TRAIN_SAMPLES:
826
- self.status_message = f"Model için yetersiz: {len(X)}"
827
- return False
828
-
829
- self.model = train_global_model(X, y)
830
- if self.model is None:
831
- self.status_message = "Model eğitimi başarısız."
832
- return False
833
-
834
- self.status_message = f"✓ Model eğitildi ({len(X)} örnek)."
835
 
836
- if _HAS_JOBLIB:
837
- try:
838
- joblib.dump(self.model, MODEL_PATH)
839
- except Exception:
840
- pass
 
 
 
 
841
 
 
842
  self.last_update = dt.datetime.now()
843
- return True
844
 
845
- def get_latest_predictions(self) -> pd.DataFrame:
846
- if self.featured_df.empty or self.model is None:
847
  return pd.DataFrame()
848
-
849
- latest = self.featured_df.sort_values("timestamp").groupby(
850
  ["item_id", "city", "quality"]
851
  ).tail(1).copy()
852
 
853
- latest["item_code"] = latest["item_id"].astype("category").cat.codes
854
- latest["city_code"] = latest["city"].astype("category").cat.codes
855
- if "qual_code" not in latest.columns:
856
- latest["qual_code"] = latest.get("quality", 1)
857
-
858
- for col in FEATURE_COLS:
859
- if col not in latest.columns:
860
- latest[col] = 0.0
861
-
862
- preds = predict_24h_returns(self.model, latest)
863
  if preds.empty:
864
  return preds
865
 
866
  if not self.prices_df.empty:
867
- price_info = self.prices_df[["item_id", "city", "quality",
868
- "sell_price_min", "buy_price_max"]].copy()
869
- for col in ["sell_price_min", "buy_price_max"]:
870
- price_info[col] = pd.to_numeric(price_info[col], errors="coerce").astype("float64")
871
- preds = preds.merge(price_info, on=["item_id", "city", "quality"], how="left")
872
  preds["spread"] = preds["sell_price_min"] - preds["buy_price_max"]
873
  else:
874
  preds["spread"] = np.nan
875
 
876
  preds["net_return_pct"] = preds["predicted_return_pct"] - (TOTAL_TRADE_COST * 100)
877
-
878
- dip_signals = preds.apply(compute_dip_signal, axis=1, result_type="expand")
879
- preds["Dip Sinyali"] = dip_signals[0]
880
- preds["Sinyal Detayı"] = dip_signals[1]
881
  return preds
882
 
883
- def get_backtest(self):
884
- if self._backtest_cache is None:
885
- self._backtest_cache = walk_forward_backtest(self.featured_df)
886
- return self._backtest_cache
 
887
 
888
 
889
  pipeline = DataPipeline()
@@ -897,43 +857,81 @@ pipeline = DataPipeline()
897
  def get_dashboard_metrics():
898
  diag = pipeline._diagnostics
899
  if pipeline.featured_df.empty:
900
- return pd.DataFrame({
901
- "Metrik": ["Durum", "Veri Kaynağı", "Model"],
902
- "Değer": ["Veri yok.", "Public AODP API", "Yok" if pipeline.model is None else "Yüklü"],
903
- })
904
 
905
  df = pipeline.featured_df
906
- cd = diag.get("class_dist", {})
907
- metrics = {
908
- "Veri Kaynağı": "Public AODP API (europe)",
909
- "Görev": "3-sınıf sınıflandırma",
910
- "Kalite Sayısı": diag.get("n_qualities", 1),
911
  "Toplam Gözlem": len(df),
912
- "Hedefli Satır": diag.get("n_with_target", 0),
913
- "Sınıf AŞAĞI": cd.get(0, 0),
914
- "Sınıf SABİT": cd.get(1, 0),
915
- "Sınıf YUKARI": cd.get(2, 0),
 
 
916
  "Tarih Aralığı": f"{df['timestamp'].min().date()} - {df['timestamp'].max().date()}",
917
- "Model": "Eğitildi" if pipeline.model else "Eğitilmedi",
918
  "Son Güncelleme": pipeline.last_update.strftime("%Y-%m-%d %H:%M") if pipeline.last_update else "—",
919
  }
920
- return pd.DataFrame(list(metrics.items()), columns=["Metrik", "Değer"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
921
 
922
 
923
  def get_analysis_table():
924
  if pipeline.featured_df.empty:
925
  return pd.DataFrame()
926
 
927
- latest = pipeline.featured_df.sort_values("timestamp").groupby(
928
- ["item_id", "city", "quality"]
929
- ).tail(1).copy()
930
-
931
  if not pipeline.prices_df.empty:
932
- price_info = pipeline.prices_df[["item_id", "city", "quality",
933
- "sell_price_min", "buy_price_max"]].copy()
934
- for col in ["sell_price_min", "buy_price_max"]:
935
- price_info[col] = pd.to_numeric(price_info[col], errors="coerce").astype("float64")
936
- latest = latest.merge(price_info, on=["item_id", "city", "quality"], how="left")
937
  else:
938
  latest["sell_price_min"] = np.nan
939
  latest["buy_price_max"] = np.nan
@@ -941,65 +939,40 @@ def get_analysis_table():
941
  latest["Ürün"] = latest["item_id"].apply(item_display_name)
942
  latest["Kalite"] = latest["quality"].apply(quality_name)
943
  latest["Şehir"] = latest["city"]
944
- latest["Mevcut Fiyat"] = latest["avg_price"].round(0)
945
  latest["Hacim"] = latest["item_count"].fillna(0).astype(int)
946
  latest["Spread"] = (latest["sell_price_min"] - latest["buy_price_max"]).round(0)
947
  latest["RSI"] = latest["rsi_14"].round(0)
948
  latest["7g%"] = (latest["price_pct_7d"] * 100).round(0)
949
- latest["Trend"] = np.where(latest["is_downtrend"] == 1, "↓", "→")
950
- latest["Likidite"] = pd.cut(latest["item_count"], bins=[-1, 10, 50, 200, np.inf],
951
- labels=["Düşük", "Orta", "İyi", "Yüksek"]).astype(str)
952
 
953
- cols = ["Ürün", "Kalite", "Şehir", "Mevcut Fiyat", "Hacim", "Spread",
954
- "RSI", "7g%", "Trend", "Likidite"]
955
  return latest[cols].sort_values("Hacim", ascending=False).head(100)
956
 
957
 
958
- def get_prediction_table():
959
- preds = pipeline.get_latest_predictions()
960
- if preds.empty:
961
- return pd.DataFrame({"Bilgi": ["Tahmin üretilemedi."]})
962
-
963
- preds = preds.copy()
964
- preds["Ürün"] = preds["item_id"].apply(item_display_name)
965
- preds["Kalite"] = preds["quality"].apply(quality_name)
966
- preds["Şehir"] = preds["city"]
967
- preds["Mevcut Fiyat"] = preds["avg_price"].round(0)
968
- preds["Tahmin"] = preds["pred_class"].map({0: "AŞAĞI", 1: "SABİT", 2: "YUKARI"})
969
- preds["P(↑)"] = (preds["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
970
- preds["Güvenilirlik"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
971
- preds["Tahmini Değişim %"] = preds["predicted_return_pct"].round(2)
972
- preds["Net Getiri %"] = preds["net_return_pct"].round(2)
973
-
974
- cols = ["Ürün", "Kalite", "Şehir", "Mevcut Fiyat", "Tahmin",
975
- "P(↑)", "Güvenilirlik", "Tahmini Değişim %", "Net Getiri %", "Dip Sinyali"]
976
- return preds[cols].sort_values("Güvenilirlik", ascending=False)
977
-
978
-
979
- def get_dip_alim_table():
980
- preds = pipeline.get_latest_predictions()
981
  if preds.empty:
982
  return pd.DataFrame({"Bilgi": ["Tahmin yok."]})
983
-
984
  dip = preds[preds["Dip Sinyali"].isin(["🟢 GÜÇLÜ AL", "🟡 ZAYIF AL", "🔵 İZLE"])].copy()
985
  if dip.empty:
986
- return pd.DataFrame({"Bilgi": ["Şu anda dip alım sinyali yok."]})
987
 
988
  order = {"🟢 GÜÇLÜ AL": 0, "🟡 ZAYIF AL": 1, "🔵 İZLE": 2}
989
  dip["_r"] = dip["Dip Sinyali"].map(order)
990
-
991
  dip["Ürün"] = dip["item_id"].apply(item_display_name)
992
  dip["Kalite"] = dip["quality"].apply(quality_name)
993
  dip["Şehir"] = dip["city"]
994
- dip["Mevcut Fiyat"] = dip["avg_price"].round(0)
995
  dip["RSI"] = dip["rsi_14"].round(0)
996
  dip["7g%"] = (dip["price_pct_7d"] * 100).round(0)
997
  dip["P(↑)"] = (dip["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
998
- dip["Net Getiri %"] = dip["net_return_pct"].round(2)
999
-
1000
  dip = dip.sort_values(["_r", "confidence"], ascending=[True, False])
1001
- cols = ["Dip Sinyali", "Ürün", "Kalite", "Şehir", "Mevcut Fiyat",
1002
- "RSI", "7g%", "P(↑)", "Net Getiri %", "Sinyal Detayı"]
 
1003
  return dip[cols].head(50)
1004
 
1005
 
@@ -1013,12 +986,10 @@ def get_arbitrage_table():
1013
  prices["quality"] = pd.to_numeric(prices["quality"], errors="coerce").fillna(1).astype(int)
1014
  for c in ["sell_price_min", "buy_price_max"]:
1015
  prices[c] = pd.to_numeric(prices[c], errors="coerce").astype("float64")
1016
-
1017
  prices = prices[prices["sell_price_min"] > 0]
1018
  if prices.empty:
1019
- return pd.DataFrame({"Bilgi": ["Geçerli fiyat yok."]})
1020
 
1021
- # Kalite bazlı arbitraj — T4.0 ile T4.2 karıştırılmıyor
1022
  buy = prices.loc[prices.groupby(["item_id", "quality"])["sell_price_min"].idxmin()][
1023
  ["item_id", "quality", "city", "sell_price_min"]
1024
  ].rename(columns={"city": "buy_city", "sell_price_min": "buy_price"})
@@ -1034,108 +1005,86 @@ def get_arbitrage_table():
1034
 
1035
  arb["brüt"] = arb["sell_price"] - arb["buy_price"]
1036
  arb["maliyet"] = arb["buy_price"] * SETUP_FEE_RATE + arb["sell_price"] * (SETUP_FEE_RATE + TRANSACTION_TAX_RATE)
1037
- arb["net_kâr"] = arb["brüt"] - arb["maliyet"]
1038
- arb["marj_%"] = (arb["net_kâr"] / arb["buy_price"]) * 100
1039
- arb = arb[arb["net_kâr"] > 0].sort_values("marj_%", ascending=False)
1040
 
1041
  if arb.empty:
1042
  return pd.DataFrame({"Bilgi": ["Kârlı arbitraj yok."]})
1043
 
1044
  arb["Ürün"] = arb["item_id"].apply(item_display_name)
1045
  arb["Kalite"] = arb["quality"].apply(quality_name)
1046
- arb["Alış Şehri"] = arb["buy_city"]
1047
- arb["Satış Şehri"] = arb["sell_city"]
1048
- arb["Alış"] = arb["buy_price"].round(0)
1049
- arb["Satış"] = arb["sell_price"].round(0)
1050
- arb["Net Kâr"] = arb["net_kâr"].round(0)
1051
- arb["Marj %"] = arb["marj_%"].round(1)
1052
-
1053
- cols = ["Ürün", "Kalite", "Alış Şehri", "Satış Şehri", "Alış", "Satış", "Net Kâr", "Marj %"]
1054
  return arb[cols].head(50)
1055
 
1056
 
1057
- def get_model_performance_plot():
1058
- if pipeline.model is None:
1059
- return go.Figure().update_layout(title="Model eğitilmedi", template="plotly_dark")
 
1060
  try:
1061
- df_imp = pd.DataFrame({
1062
- "Özellik": pipeline.model.feature_name_,
1063
- "Önem": pipeline.model.feature_importances_,
1064
  }).sort_values("Önem", ascending=True).tail(20)
1065
- fig = px.bar(df_imp, x="Önem", y="Özellik", orientation="h",
1066
- title="Feature Importance", template="plotly_dark")
1067
  fig.update_layout(height=600)
1068
  return fig
1069
  except Exception as e:
1070
  return go.Figure().update_layout(title=f"Hata: {e}", template="plotly_dark")
1071
 
1072
 
1073
- def get_backtest_outputs():
1074
- bt = pipeline.get_backtest()
1075
- if "error" in bt:
1076
- return (pd.DataFrame({"Bilgi": [bt["error"]]}),
1077
- go.Figure().update_layout(title="Hata", template="plotly_dark"),
1078
- pd.DataFrame())
1079
-
1080
- rdf = pd.DataFrame(bt["results"])
1081
- fig = go.Figure()
1082
- fig.add_trace(go.Bar(name="Model Acc", x=rdf["split"], y=rdf["accuracy"], marker_color="cyan"))
1083
- fig.add_trace(go.Bar(name="Baseline", x=rdf["split"], y=rdf["baseline_accuracy"], marker_color="gray"))
1084
- fig.update_layout(title="Accuracy vs Baseline", xaxis_title="Split",
1085
- yaxis_title="Accuracy", barmode="group", template="plotly_dark")
1086
-
1087
- sim = pd.DataFrame([bt["simulation"]]) if bt.get("simulation") else pd.DataFrame()
1088
- return rdf, fig, sim
1089
-
1090
-
1091
  def list_csv_files() -> pd.DataFrame:
1092
  rows = []
1093
  if os.path.isdir(DATA_DIR):
1094
- for fname in sorted(os.listdir(DATA_DIR)):
1095
- if fname.endswith(".csv"):
1096
- fpath = os.path.join(DATA_DIR, fname)
1097
  try:
1098
  rows.append({
1099
- "Dosya": fname,
1100
- "Boyut (KB)": round(os.path.getsize(fpath) / 1024, 2),
1101
- "Son Değişiklik": dt.datetime.fromtimestamp(os.path.getmtime(fpath)).strftime("%Y-%m-%d %H:%M:%S"),
1102
  })
1103
  except OSError:
1104
  continue
1105
  return pd.DataFrame(rows) if rows else pd.DataFrame(columns=["Dosya", "Boyut (KB)", "Son Değişiklik"])
1106
 
1107
 
1108
- def refresh_data_ui():
1109
  try:
1110
  pipeline.fetch_data()
1111
  if not pipeline.featured_df.empty:
1112
- pipeline.train_model()
1113
  except Exception as e:
1114
  pipeline.status_message = f"Hata: {e}"
1115
  import traceback
1116
  traceback.print_exc()
1117
 
1118
- try:
1119
- bt_table, bt_fig, sim_table = get_backtest_outputs()
1120
- except Exception:
1121
- bt_table, sim_table = pd.DataFrame(), pd.DataFrame()
1122
- bt_fig = go.Figure().update_layout(title="Hata", template="plotly_dark")
1123
-
1124
  def _safe(fn, default):
1125
  try:
1126
  return fn()
1127
- except Exception as e:
1128
- return pd.DataFrame({"Bilgi": [f"Hata: {e}"]}) if default is None else default
1129
 
1130
  return (
1131
  pipeline.status_message,
1132
  _safe(get_dashboard_metrics, pd.DataFrame()),
1133
  _safe(get_analysis_table, pd.DataFrame()),
1134
- _safe(get_prediction_table, pd.DataFrame()),
1135
- _safe(get_dip_alim_table, pd.DataFrame()),
 
1136
  _safe(get_arbitrage_table, pd.DataFrame()),
1137
- _safe(get_model_performance_plot, go.Figure().update_layout(title="—", template="plotly_dark")),
1138
- bt_table, bt_fig, sim_table,
1139
  _safe(list_csv_files, pd.DataFrame()),
1140
  )
1141
 
@@ -1150,10 +1099,9 @@ def initial_load():
1150
  return (
1151
  _safe(get_dashboard_metrics, pd.DataFrame({"Metrik": ["Durum"], "Değer": ["Hazır"]})),
1152
  pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame(),
 
 
1153
  go.Figure().update_layout(title="Yenile", template="plotly_dark"),
1154
- pd.DataFrame(),
1155
- go.Figure().update_layout(title="Veri gerekli", template="plotly_dark"),
1156
- pd.DataFrame(),
1157
  _safe(list_csv_files, pd.DataFrame()),
1158
  )
1159
 
@@ -1167,85 +1115,85 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
1167
 
1168
  gr.Markdown(
1169
  """
1170
- # 🦅 GOSHAWK ALBION MARKET AI — v2.0
1171
- **Kalite ayrımı + uzun vadeli trend + spread feature'ları** | Avrupa Sunucusu
1172
- *LightGBM 3-sınıf sınıflandırma: AŞAĞI / SABİT / YUKARI*
1173
 
1174
- **⚠️ Risk Uyarısı:** Yatırım tavsiyesi değildir.
 
1175
  """
1176
  )
1177
 
1178
  with gr.Row():
1179
- refresh_btn = gr.Button("🔄 Verileri Yenile ve Modeli Eğit", variant="primary", scale=1)
1180
  status_text = gr.Textbox(label="Durum", interactive=False, scale=2, value=pipeline.status_message)
1181
 
1182
  with gr.Tabs():
1183
  with gr.TabItem("📊 Dashboard"):
1184
  dashboard_table = gr.Dataframe(label="Özet", interactive=False, wrap=True)
1185
  gr.Markdown(
1186
- "### v2.0 Yenilikleri\n"
1187
- "- **Kalite ayrımı:** T4.0, T4.1, T4.2 ayrı ürün olarak işlenir\n"
1188
- "- **7/14 günlük trend:** `lag_168h`, `lag_336h`, `chg_168h`, `chg_336h`\n"
1189
- "- **Spread feature:** Alış-satış farkı modele girdi\n"
1190
- "- **Arbitraj:** Kalite bazlı, T4.0 ile T4.2 karışmaz\n"
1191
- "- **7g yüzdelik konum:** Dip noktası daha net\n\n"
1192
- "### Sınıflar\n"
1193
- f"- AŞAĞI: log < {CLASS_DOWN:.2f}\n"
1194
- f"- SABİT: arada\n"
1195
- f"- YUKARI: log > {CLASS_UP:.2f}\n\n"
1196
  "### Riskler\n"
1197
- "- Kalite/enchant hâlâ sınırlı\n"
1198
- "- Overfit kontrolü yapın"
1199
  )
1200
 
1201
- with gr.TabItem("🎯 DİP ALIM"):
1202
- dip_alim_table = gr.Dataframe(label="Dip Alım Sinyalleri", interactive=False)
1203
 
1204
- with gr.TabItem("🔮 Tahminler"):
1205
- prediction_table = gr.Dataframe(label="Tüm Tahminler", interactive=False)
 
 
 
 
 
 
 
 
1206
 
1207
- with gr.TabItem("📦 100 Ürün"):
1208
  analysis_table = gr.Dataframe(label="Ürün Analizi", interactive=False)
1209
 
1210
  with gr.TabItem("🚚 Arbitraj"):
1211
  arbitrage_table = gr.Dataframe(label="Arbitraj (Kalite Bazlı)", interactive=False)
1212
- gr.Markdown(
1213
- "*T4.0 Normal ile T4.2 Outstanding ayrı hesaplanır. "
1214
- "Thetford'daki ucuz T4.0 Cape, başka şehirde pahalıysa burada görünür.*"
1215
- )
1216
 
1217
- with gr.TabItem("📈 Model"):
1218
- performance_plot = gr.Plot(label="Feature Importance")
1219
 
1220
- with gr.TabItem("🧪 Backtest"):
1221
- backtest_table = gr.Dataframe(label="Split Sonuçları", interactive=False)
1222
- backtest_plot = gr.Plot(label="Accuracy vs Baseline")
1223
- simulation_table = gr.Dataframe(label="Simülasyon", interactive=False)
1224
 
1225
  with gr.TabItem("🗄️ Veri"):
1226
  gr.Markdown(
1227
  "**API:** europe.albion-online-data.com\n"
1228
- "**Kaliteler:** 1-5 (Normal→Masterpiece)\n"
1229
- f"**Maliyet:** %{TOTAL_TRADE_COST*100:.1f}\n"
1230
- f"**Sınıf sınırı:** ±%{CLASS_UP*100:.0f}"
 
1231
  )
1232
- csv_files = gr.Dataframe(label="CSV Dosyaları", interactive=False,
1233
  headers=["Dosya", "Boyut (KB)", "Son Değişiklik"])
1234
 
1235
  refresh_btn.click(
1236
- fn=refresh_data_ui,
1237
  inputs=[],
1238
- outputs=[status_text, dashboard_table, analysis_table, prediction_table,
1239
- dip_alim_table, arbitrage_table, performance_plot, backtest_table,
1240
- backtest_plot, simulation_table, csv_files],
1241
  )
1242
 
1243
  demo.load(
1244
  fn=initial_load,
1245
  inputs=[],
1246
- outputs=[dashboard_table, analysis_table, prediction_table, dip_alim_table,
1247
- arbitrage_table, performance_plot, backtest_table, backtest_plot,
1248
- simulation_table, csv_files],
1249
  )
1250
 
1251
 
 
1
  # app.py
2
+ # GOSHAWK ALBION MARKET AI v3.0
3
+ # 24h + 3 aylık (90 gün) çok ufuklu tahmin
4
 
5
  import os
6
  import time
 
15
  import plotly.express as px
16
 
17
  import lightgbm as lgb
18
+ from sklearn.metrics import accuracy_score, mean_absolute_error
19
 
20
  import gradio as gr
21
 
 
38
  "Martlock", "Thetford", "Caerleon",
39
  ]
40
 
 
 
 
 
41
  QUALITY_NAMES = {1: "Normal", 2: "Good", 3: "Outstanding", 4: "Excellent", 5: "Masterpiece"}
42
 
43
  _ITEM_POOL = [
 
104
  os.makedirs(DATA_DIR, exist_ok=True)
105
  MODEL_DIR = os.path.join(DATA_DIR, "models")
106
  os.makedirs(MODEL_DIR, exist_ok=True)
107
+ MODEL_PATH_24H = os.path.join(MODEL_DIR, "gos_hawk_24h.pkl")
108
+ MODEL_PATH_90D = os.path.join(MODEL_DIR, "gos_hawk_90d.pkl")
109
 
110
  MIN_TRAIN_SAMPLES = 150
111
  REQUEST_TIMEOUT = 60
112
  MAX_RETRIES = 3
113
  RETRY_BACKOFF = 2
114
 
115
+ HISTORY_TIME_SCALE = 6 # 6h — 3 aylık veri için saatlik çok ağır
116
 
117
+ # 24h sınıflandırma
118
+ CLASS_DOWN_24H = -0.03
119
+ CLASS_UP_24H = 0.03
120
+ TARGET_TOLERANCE_HOURS_24H = 12
121
 
122
+ # 90 gün (3 ay) sınıflandırma — daha geniş sınırlar
123
+ HORIZON_DAYS_90D = 90
124
+ CLASS_DOWN_90D = -0.10
125
+ CLASS_UP_90D = 0.10
126
+ TARGET_TOLERANCE_HOURS_90D = 72 # 3 gün tolerans
127
 
128
+ # Dip sinyali
129
  DIP_RSI_MAX = 40.0
130
  DIP_PERCENTILE_MAX = 0.25
131
  DIP_MIN_VOLUME_RATIO = 1.0
132
  DIP_MIN_PREDICTED_PCT = -1.0
133
  DIP_MAX_SPREAD_PCT = 0.15
134
 
135
+ # 24h feature'lar
136
+ FEATURE_COLS_24H = [
 
 
 
137
  "hour", "dayofweek",
138
  "lag_24h_log", "lag_168h_log", "lag_336h_log",
139
  "chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h",
 
145
  "item_code", "city_code",
146
  ]
147
 
148
+ # 90d feature'lar — daha uzun vadeli
149
+ FEATURE_COLS_90D = [
150
+ "month", "dayofweek",
151
+ "lag_720h_log", "lag_1440h_log", # 30g, 60g
152
+ "chg_336h", "chg_720h", "chg_1440h", # 14g, 30g, 60g değişim
153
+ "rsi_14", "volatility_24h", "volatility_168h",
154
+ "volume_ratio",
155
+ "ma_168h_ratio", "ma_720h_ratio", # 7g, 30g MA oranı
156
+ "trend_slope_30d", # 30 günlük lineer trend eğimi
157
+ "city_price_deviation",
158
+ "spread_pct", "buy_sell_ratio", "qual_code",
159
+ "item_code", "city_code",
160
+ ]
161
+
162
  # ---------------------------------------------------------------------------
163
  # PUBLIC API
164
  # ---------------------------------------------------------------------------
165
 
166
 
167
  def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
168
+ headers = {"User-Agent": "GOSHAWK-ALBION-MARKET-AI/3.0", "Accept": "application/json"}
169
  for attempt in range(MAX_RETRIES):
170
  try:
171
  resp = requests.get(url, params=params, headers=headers, timeout=REQUEST_TIMEOUT)
 
178
  time.sleep(RETRY_BACKOFF * (attempt + 1))
179
  continue
180
  else:
181
+ print(f"API: {resp.status_code} - {url}")
182
  return None
183
  except requests.exceptions.Timeout:
184
+ print(f"Timeout: {url}")
185
  except requests.exceptions.RequestException as e:
186
+ print(f"Hata: {e}")
187
  time.sleep(RETRY_BACKOFF * (attempt + 1))
188
  return None
189
 
190
 
191
+ def fetch_current_prices(item_ids: List[str], cities: List[str], qualities: str = "1,2,3") -> pd.DataFrame:
192
+ """Anlık fiyatlar — Q1, Q2, Q3 yeterli (Q4/Q5 nadiren işlem görür)."""
193
  all_data = []
194
  for i in range(0, len(item_ids), 40):
195
  chunk = item_ids[i:i+40]
 
211
  return df
212
 
213
 
214
+ def fetch_history(item_ids: List[str], cities: List[str], time_scale: int = 6,
215
+ qualities: str = "1,2,3") -> pd.DataFrame:
216
+ """Tarihsel fiyatlar — 3 aylık tahmin için en az 3 ay veri gerekir."""
217
  all_records = []
218
  for i in range(0, len(item_ids), 15):
219
  chunk = item_ids[i:i+15]
220
  url = f"{API_BASE}/stats/history/{','.join(chunk)}.json"
221
+ params = {"locations": ",".join(cities), "qualities": qualities,
222
+ "time-scale": time_scale}
223
  data = safe_request(url, params)
224
  if data:
225
  for entry in data:
 
270
  df = df[df["avg_price"] > 0].copy()
271
  df = df.dropna(subset=["timestamp"]).copy()
272
 
 
273
  for (item, qual) in df[["item_id", "quality"]].drop_duplicates().itertuples(index=False):
274
  mask = (df["item_id"] == item) & (df["quality"] == qual)
275
  if mask.sum() < 10:
 
289
  try:
290
  df.to_csv(path, index=False)
291
  except Exception as e:
292
+ print(f"CSV hatası: {e}")
293
  return ""
294
  return path
295
 
 
307
  return (100 - (100 / (1 + rs))).fillna(50)
308
 
309
 
310
+ def _linear_trend_slope(y: np.ndarray) -> float:
311
+ """Basit lineer regresyon eğimi (log ölçek)."""
312
+ if len(y) < 5:
313
+ return 0.0
314
+ x = np.arange(len(y), dtype=float)
315
+ try:
316
+ slope = np.polyfit(x, y, 1)[0]
317
+ return float(slope)
318
+ except Exception:
319
+ return 0.0
320
+
321
+
322
  def build_features(df: pd.DataFrame) -> pd.DataFrame:
 
 
 
 
323
  if df.empty:
324
  return df
325
 
 
332
 
333
  df["hour"] = df["timestamp"].dt.hour
334
  df["dayofweek"] = df["timestamp"].dt.dayofweek
335
+ df["month"] = df["timestamp"].dt.month
336
  df["is_weekend"] = (df["dayofweek"] >= 5).astype(int)
337
  df["qual_code"] = df["quality"].astype(int)
338
 
 
339
  group_cols = ["item_id", "city", "quality"]
 
340
  df["log_price"] = np.log(df["avg_price"].replace(0, np.nan))
341
 
342
+ # Kısa vadeli (24h model)
343
  df["lag_24h_log"] = df.groupby(group_cols)["log_price"].shift(24)
344
  df["lag_168h_log"] = df.groupby(group_cols)["log_price"].shift(168)
345
  df["lag_336h_log"] = df.groupby(group_cols)["log_price"].shift(336)
346
 
 
347
  for chg in [1, 6, 24, 168, 336]:
348
  df[f"chg_{chg}h"] = df.groupby(group_cols)["log_price"].shift(chg)
349
 
 
350
  df["ma_24h_raw"] = df.groupby(group_cols)["avg_price"].transform(
351
  lambda x: x.rolling(24, min_periods=6).mean()
352
  )
 
357
  )
358
  df["ma_168h_ratio"] = df["ma_168h_raw"] / df["avg_price"].replace(0, np.nan) - 1.0
359
 
360
+ # Uzun vadeli (90d model)
361
+ df["lag_720h_log"] = df.groupby(group_cols)["log_price"].shift(720) # 30g
362
+ df["lag_1440h_log"] = df.groupby(group_cols)["log_price"].shift(1440) # 60g
363
+
364
+ for chg in [720, 1440]:
365
+ df[f"chg_{chg}h"] = df.groupby(group_cols)["log_price"].shift(chg)
366
+
367
+ df["ma_720h_raw"] = df.groupby(group_cols)["avg_price"].transform(
368
+ lambda x: x.rolling(720, min_periods=72).mean()
369
+ )
370
+ df["ma_720h_ratio"] = df["ma_720h_raw"] / df["avg_price"].replace(0, np.nan) - 1.0
371
+
372
  df["rsi_14"] = df.groupby(group_cols)["avg_price"].transform(lambda x: compute_rsi(x, 14))
373
  df["volatility_24h"] = df.groupby(group_cols)["log_price"].transform(
374
  lambda x: x.rolling(24, min_periods=6).std()
375
  )
376
+ df["volatility_168h"] = df.groupby(group_cols)["log_price"].transform(
377
+ lambda x: x.rolling(168, min_periods=24).std()
378
+ )
379
  df["volume_ma_24h"] = df.groupby(group_cols)["item_count"].transform(
380
  lambda x: x.rolling(24, min_periods=6).mean()
381
  )
382
  df["volume_ratio"] = df["item_count"] / df["volume_ma_24h"].replace(0, np.nan)
383
 
384
+ # 30 günlük lineer trend eğimi
385
+ df["trend_slope_30d"] = df.groupby(group_cols)["log_price"].transform(
386
+ lambda x: x.rolling(720, min_periods=72).apply(_linear_trend_slope, raw=True)
387
+ )
388
+
389
  city_avg = df.groupby(["item_id", "quality", "timestamp"])["avg_price"].transform("mean")
390
  df["city_price_deviation"] = (df["avg_price"] - city_avg) / city_avg.replace(0, np.nan)
391
 
 
392
  df["price_pct_7d"] = df.groupby(group_cols)["avg_price"].transform(
393
  lambda x: x.rolling(168, min_periods=24).apply(
394
  lambda arr: (arr[-1] - np.min(arr)) / (np.max(arr) - np.min(arr) + 1e-9),
395
  raw=True
396
  )
397
  )
 
398
  df["is_downtrend"] = ((df["chg_6h"] < 0) & (df["chg_24h"] < 0)).astype(int)
399
 
400
+ df = _compute_forward_targets(df)
 
 
 
401
  return df
402
 
403
 
404
+ def _compute_forward_targets(df: pd.DataFrame) -> pd.DataFrame:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  if df.empty or "log_price" not in df.columns:
406
  df["target_log_return_24h"] = np.nan
407
+ df["target_class_24h"] = np.nan
408
+ df["target_log_return_90d"] = np.nan
409
+ df["target_class_90d"] = np.nan
410
  return df
411
 
412
  df["target_log_return_24h"] = np.nan
413
+ df["target_log_return_90d"] = np.nan
414
+
415
  group_cols = ["item_id", "city", "quality"]
416
 
417
  for keys, g in df.groupby(group_cols):
 
429
  "log_price_future": g["log_price"].values,
430
  }).sort_values("timestamp")
431
 
432
+ # 24 saat
433
+ left["target_t_24h"] = left["timestamp"] + pd.Timedelta(hours=24)
434
+ m24 = pd.merge_asof(
435
+ left.sort_values("target_t_24h"),
436
+ right.sort_values("timestamp"),
437
+ left_on="target_t_24h",
438
+ right_on="timestamp",
439
+ direction="nearest",
440
+ tolerance=pd.Timedelta(hours=TARGET_TOLERANCE_HOURS_24H),
441
+ suffixes=("", "_f"),
442
+ )
443
+ df.loc[m24["idx"].values, "target_log_return_24h"] = (
444
+ m24["log_price_future"].values - m24["log_price_now"].values
445
+ )
446
 
447
+ # 90 gün
448
+ left["target_t_90d"] = left["timestamp"] + pd.Timedelta(days=HORIZON_DAYS_90D)
449
+ m90 = pd.merge_asof(
450
+ left.sort_values("target_t_90d"),
451
  right.sort_values("timestamp"),
452
+ left_on="target_t_90d",
453
  right_on="timestamp",
454
  direction="nearest",
455
+ tolerance=pd.Timedelta(hours=TARGET_TOLERANCE_HOURS_90D),
456
+ suffixes=("", "_f"),
457
  )
458
+ df.loc[m90["idx"].values, "target_log_return_90d"] = (
459
+ m90["log_price_future"].values - m90["log_price_now"].values
460
+ )
461
+
462
+ t24 = df["target_log_return_24h"]
463
+ df["target_class_24h"] = np.where(t24 < CLASS_DOWN_24H, 0, np.where(t24 > CLASS_UP_24H, 2, 1))
464
+ df.loc[t24.isna(), "target_class_24h"] = np.nan
465
 
466
+ t90 = df["target_log_return_90d"]
467
+ df["target_class_90d"] = np.where(t90 < CLASS_DOWN_90D, 0, np.where(t90 > CLASS_UP_90D, 2, 1))
468
+ df.loc[t90.isna(), "target_class_90d"] = np.nan
469
 
 
 
 
470
  return df
471
 
472
 
473
+ def enrich_spread_features(featured_df: pd.DataFrame, prices_df: pd.DataFrame) -> pd.DataFrame:
474
+ if featured_df.empty or prices_df.empty:
475
+ featured_df["spread_pct"] = 0.0
476
+ featured_df["buy_sell_ratio"] = 1.0
477
+ return featured_df
478
+
479
+ prices = prices_df.copy()
480
+ if "quality" not in prices.columns:
481
+ prices["quality"] = 1
482
+ prices["quality"] = pd.to_numeric(prices["quality"], errors="coerce").fillna(1).astype(int)
483
+ for c in ["sell_price_min", "buy_price_max"]:
484
+ prices[c] = pd.to_numeric(prices[c], errors="coerce").astype("float64")
485
+
486
+ prices["spread_pct"] = np.where(
487
+ prices["buy_price_max"] > 0,
488
+ (prices["sell_price_min"] - prices["buy_price_max"]) / prices["buy_price_max"], np.nan
489
+ )
490
+ prices["buy_sell_ratio"] = np.where(
491
+ prices["sell_price_min"] > 0,
492
+ prices["buy_price_max"] / prices["sell_price_min"], np.nan
493
+ )
494
+
495
+ pf = prices[["item_id", "city", "quality", "spread_pct", "buy_sell_ratio"]].drop_duplicates(
496
+ subset=["item_id", "city", "quality"]
497
+ )
498
+ out = featured_df.merge(pf, on=["item_id", "city", "quality"], how="left")
499
+ out["spread_pct"] = out["spread_pct"].fillna(0.0)
500
+ out["buy_sell_ratio"] = out["buy_sell_ratio"].fillna(1.0)
501
+ return out
502
+
503
+
504
  # ---------------------------------------------------------------------------
505
+ # MODEL EĞİTİMİ (24h + 90d)
506
  # ---------------------------------------------------------------------------
507
 
508
 
509
+ def _prepare_model_df(featured_df: pd.DataFrame, horizon: str) -> Tuple[pd.DataFrame, pd.Series, pd.DataFrame]:
510
+ """horizon: '24h' veya '90d'"""
511
  if featured_df.empty:
512
  return pd.DataFrame(), pd.Series(dtype=int), pd.DataFrame()
513
 
514
+ target_col = f"target_class_{horizon}"
515
+ if target_col not in featured_df.columns:
516
+ return pd.DataFrame(), pd.Series(dtype=int), pd.DataFrame()
517
+
518
  df = featured_df.copy()
519
  df["item_code"] = df["item_id"].astype("category").cat.codes
520
  df["city_code"] = df["city"].astype("category").cat.codes
521
  if "qual_code" not in df.columns:
522
  df["qual_code"] = df.get("quality", 1)
523
 
524
+ df = df.dropna(subset=[target_col]).copy()
525
+ df[target_col] = df[target_col].astype(int)
526
 
527
+ # Doldur
528
+ fill_cols = ["chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h", "chg_720h", "chg_1440h"]
529
+ for c in fill_cols:
530
+ if c in df.columns:
531
+ df[c] = df[c].fillna(0.0)
532
 
533
+ for c in ["lag_24h_log", "lag_168h_log", "lag_336h_log", "lag_720h_log", "lag_1440h_log"]:
534
+ if c in df.columns:
535
+ df[c] = df[c].fillna(df["log_price"])
536
 
537
+ for c in ["rsi_14"]:
538
+ df[c] = df[c].fillna(50.0)
539
+ for c in ["volatility_24h", "volatility_168h", "volume_ratio", "ma_24h_ratio",
540
+ "ma_168h_ratio", "ma_720h_ratio", "trend_slope_30d", "city_price_deviation",
541
+ "spread_pct", "buy_sell_ratio"]:
542
+ if c in df.columns:
543
+ df[c] = df[c].fillna(0.0)
 
544
 
545
+ feature_cols = FEATURE_COLS_24H if horizon == "24h" else FEATURE_COLS_90D
546
+ feature_cols = [c for c in feature_cols if c in df.columns]
547
 
548
+ model_df = df.dropna(subset=feature_cols + [target_col]).copy()
549
+ if model_df.empty:
550
+ return pd.DataFrame(), pd.Series(dtype=int), pd.DataFrame()
551
+
552
+ X = model_df[feature_cols].astype("float64")
553
+ y = model_df[target_col]
554
+ meta = model_df[["item_id", "city", "quality", "timestamp", "avg_price"]].copy()
555
  return X, y, meta
556
 
557
 
558
+ def train_model(X: pd.DataFrame, y: pd.Series, horizon: str) -> Optional[lgb.LGBMClassifier]:
559
  if len(X) < MIN_TRAIN_SAMPLES:
560
  return None
561
+ cc = y.value_counts().to_dict()
562
+ print(f"[{horizon}] sınıf: {cc}")
563
+ if len(cc) < 3 or min(cc.values()) < 10:
 
564
  return None
565
 
566
+ split = int(len(X) * 0.8)
567
+ X_tr, X_val = X.iloc[:split], X.iloc[split:]
568
+ y_tr, y_val = y.iloc[:split], y.iloc[split:]
569
 
570
+ n_est = 300 if horizon == "24h" else 250
571
  model = lgb.LGBMClassifier(
572
  objective="multiclass", num_class=3, metric="multi_logloss",
573
  boosting_type="gbdt",
574
  num_leaves=15, max_depth=5, min_child_samples=50,
575
+ learning_rate=0.03, n_estimators=n_est,
576
  feature_fraction=0.8, bagging_fraction=0.8, bagging_freq=5,
577
  reg_alpha=0.5, reg_lambda=1.0,
578
  verbose=-1, random_state=42, n_jobs=-1,
579
  )
 
580
  model.fit(
581
+ X_tr, y_tr,
582
  eval_set=[(X_val, y_val)],
583
  callbacks=[lgb.early_stopping(30, verbose=False), lgb.log_evaluation(0)],
584
  )
585
+ acc = accuracy_score(y_val, model.predict(X_val))
586
+ print(f"[{horizon}] val_accuracy={acc:.4f}")
 
 
587
  return model
588
 
589
 
590
+ def predict(model, latest_features: pd.DataFrame, horizon: str) -> pd.DataFrame:
591
  if latest_features.empty or model is None:
592
  return pd.DataFrame()
593
 
594
+ df = latest_features.copy()
595
+ df["item_code"] = df["item_id"].astype("category").cat.codes
596
+ df["city_code"] = df["city"].astype("category").cat.codes
597
+ if "qual_code" not in df.columns:
598
+ df["qual_code"] = df.get("quality", 1)
599
 
600
+ fill_cols = ["chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h", "chg_720h", "chg_1440h"]
601
+ for c in fill_cols:
602
+ if c in df.columns:
603
+ df[c] = df[c].fillna(0.0)
604
+ for c in ["lag_24h_log", "lag_168h_log", "lag_336h_log", "lag_720h_log", "lag_1440h_log"]:
605
+ if c in df.columns:
606
+ df[c] = df[c].fillna(df["log_price"])
607
+ for c in ["rsi_14"]:
608
+ df[c] = df[c].fillna(50.0)
609
+ for c in ["volatility_24h", "volatility_168h", "volume_ratio", "ma_24h_ratio",
610
+ "ma_168h_ratio", "ma_720h_ratio", "trend_slope_30d", "city_price_deviation",
611
+ "spread_pct", "buy_sell_ratio"]:
612
+ if c in df.columns:
613
+ df[c] = df[c].fillna(0.0)
614
+
615
+ feature_cols = FEATURE_COLS_24H if horizon == "24h" else FEATURE_COLS_90D
616
+ feature_cols = [c for c in feature_cols if c in df.columns]
617
+ X_ready = df[feature_cols].astype("float64").dropna()
618
  if X_ready.empty:
619
  return pd.DataFrame()
620
 
621
  probs = model.predict_proba(X_ready)
622
  pred_class = np.argmax(probs, axis=1)
623
+
624
+ if horizon == "24h":
625
+ exp_log = probs[:, 0] * CLASS_DOWN_24H + probs[:, 2] * CLASS_UP_24H
626
+ clipped = np.clip(exp_log, CLASS_DOWN_24H, CLASS_UP_24H)
627
+ else:
628
+ exp_log = probs[:, 0] * CLASS_DOWN_90D + probs[:, 2] * CLASS_UP_90D
629
+ clipped = np.clip(exp_log, CLASS_DOWN_90D, CLASS_UP_90D)
630
+
631
+ keep = ["item_id", "city", "quality", "avg_price", "item_count",
632
+ "volatility_24h", "rsi_14", "volume_ratio", "price_pct_7d",
633
+ "is_downtrend", "chg_6h", "chg_24h", "chg_168h",
634
+ "spread_pct", "buy_sell_ratio"]
635
+ keep = [c for c in keep if c in df.columns]
636
+ out = df.loc[X_ready.index, keep].copy()
637
+ out["pred_class"] = pred_class
638
+ out["prob_down"] = probs[:, 0]
639
+ out["prob_flat"] = probs[:, 1]
640
+ out["prob_up"] = probs[:, 2]
641
+ out["predicted_log_return"] = clipped
642
+ out["predicted_return_pct"] = (np.exp(clipped) - 1) * 100
643
+ out["predicted_price"] = out["avg_price"] * np.exp(clipped)
644
+ out["confidence"] = probs.max(axis=1)
645
+ return out
646
 
647
 
648
  # ---------------------------------------------------------------------------
 
652
 
653
  def compute_dip_signal(row: pd.Series) -> Tuple[str, str]:
654
  reasons, passed = [], 0
 
655
  rsi = row.get("rsi_14", 50)
656
  if pd.notna(rsi) and rsi < DIP_RSI_MAX:
657
  passed += 1
658
  reasons.append(f"RSI={rsi:.0f}")
 
659
  pct = row.get("price_pct_7d", 0.5)
660
  if pd.notna(pct) and pct < DIP_PERCENTILE_MAX:
661
  passed += 1
662
  reasons.append(f"7g%={pct*100:.0f}")
 
663
  vr = row.get("volume_ratio", 1.0)
664
  if pd.notna(vr) and vr >= DIP_MIN_VOLUME_RATIO:
665
  passed += 1
666
  reasons.append(f"H×{vr:.1f}")
667
+ pp = row.get("predicted_return_pct", 0)
668
+ if pd.notna(pp) and pp >= DIP_MIN_PREDICTED_PCT:
 
669
  passed += 1
670
+ reasons.append(f"M={pp:+.1f}%")
671
+ sp = row.get("spread_pct", np.nan)
672
+ if pd.notna(sp) and abs(sp) < DIP_MAX_SPREAD_PCT:
 
673
  passed += 1
674
+ reasons.append(f"Sp={sp*100:.1f}%")
675
+ down = row.get("is_downtrend", 0)
676
+ if not down or (pd.notna(pp) and pp > 2):
 
677
  passed += 1
678
  reasons.append("Trend✓")
679
 
 
683
  sig = "🟡 ZAYIF AL"
684
  elif passed == 3 and pd.notna(rsi) and rsi < 45:
685
  sig = "🔵 İZLE"
686
+ elif down and pd.notna(pp) and pp < -2:
687
  sig = "🔴 ALMA"
688
  else:
689
  sig = "⚪ BEKLE"
 
690
  return sig, " · ".join(reasons) if reasons else "—"
691
 
692
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  # ---------------------------------------------------------------------------
694
  # PIPELINE
695
  # ---------------------------------------------------------------------------
 
700
  self.history_df = pd.DataFrame()
701
  self.prices_df = pd.DataFrame()
702
  self.featured_df = pd.DataFrame()
703
+ self.model_24h = None
704
+ self.model_90d = None
705
  self.last_update = None
706
  self.status_message = "Veri yüklenmedi."
 
707
  self._diagnostics = {}
708
 
709
+ if _HAS_JOBLIB:
710
  try:
711
+ if os.path.exists(MODEL_PATH_24H):
712
+ self.model_24h = joblib.load(MODEL_PATH_24H)
713
+ if os.path.exists(MODEL_PATH_90D):
714
+ self.model_90d = joblib.load(MODEL_PATH_90D)
715
+ if self.model_24h or self.model_90d:
716
+ self.status_message = "Kayıtlı model(ler) yüklendi."
717
  except Exception:
718
  pass
719
 
 
721
  item_ids = item_ids or ITEM_IDS
722
  cities = cities or CITIES
723
 
724
+ self.status_message = "Public API'den veri çekiliyor (3 kalite)..."
725
  ok, msg = check_api_health()
726
  if not ok:
727
  self.status_message = f"API erişilemiyor: {msg}"
 
740
 
741
  self.history_df = clean_market_data(hist)
742
  self.prices_df = prices
743
+ n_q = self.history_df["quality"].nunique() if "quality" in self.history_df.columns else 1
744
+ self.status_message = f"✓ {len(self.history_df)} satır ({n_q} kalite), {len(self.prices_df)} anlık."
745
 
746
  save_to_csv(self.history_df, "history")
747
  save_to_csv(self.prices_df, "prices")
748
 
749
  self.featured_df = build_features(self.history_df)
750
  self.featured_df = enrich_spread_features(self.featured_df, self.prices_df)
 
751
  self._update_diagnostics()
752
 
753
  def _update_diagnostics(self):
 
755
  if df.empty:
756
  self._diagnostics = {}
757
  return
758
+ n24 = int(df["target_class_24h"].notna().sum()) if "target_class_24h" in df.columns else 0
759
+ n90 = int(df["target_class_90d"].notna().sum()) if "target_class_90d" in df.columns else 0
760
+ cd24 = df["target_class_24h"].value_counts().to_dict() if n24 > 0 else {}
761
+ cd90 = df["target_class_90d"].value_counts().to_dict() if n90 > 0 else {}
762
  self._diagnostics = {
763
  "n_total": len(df),
764
+ "n_24h": n24,
765
+ "n_90d": n90,
766
+ "class_24h": cd24,
767
+ "class_90d": cd90,
768
  }
769
+ print(f"[TANI] total={len(df)}, 24h={n24}, 90d={n90}")
770
 
771
+ def train_all(self):
772
  if self.featured_df.empty:
773
  self.status_message = "Önce veri çekin."
774
  return False
775
 
776
+ # 24h
777
+ X24, y24, _ = _prepare_model_df(self.featured_df, "24h")
778
+ if len(X24) >= MIN_TRAIN_SAMPLES:
779
+ self.model_24h = train_model(X24, y24, "24h")
780
+ if _HAS_JOBLIB and self.model_24h:
781
+ try:
782
+ joblib.dump(self.model_24h, MODEL_PATH_24H)
783
+ except Exception:
784
+ pass
785
+
786
+ # 90d
787
+ X90, y90, _ = _prepare_model_df(self.featured_df, "90d")
788
+ n90 = len(X90)
789
+ if n90 >= MIN_TRAIN_SAMPLES:
790
+ self.model_90d = train_model(X90, y90, "90d")
791
+ if _HAS_JOBLIB and self.model_90d:
792
+ try:
793
+ joblib.dump(self.model_90d, MODEL_PATH_90D)
794
+ except Exception:
795
+ pass
 
796
 
797
+ msgs = []
798
+ if self.model_24h:
799
+ msgs.append("24h✓")
800
+ else:
801
+ msgs.append("24h✗")
802
+ if self.model_90d:
803
+ msgs.append(f"90d✓ ({n90} örnek)")
804
+ else:
805
+ msgs.append(f"90d✗ (yetersiz: {n90})")
806
 
807
+ self.status_message = "Model: " + " · ".join(msgs)
808
  self.last_update = dt.datetime.now()
809
+ return self.model_24h is not None or self.model_90d is not None
810
 
811
+ def get_latest(self) -> pd.DataFrame:
812
+ if self.featured_df.empty:
813
  return pd.DataFrame()
814
+ return self.featured_df.sort_values("timestamp").groupby(
 
815
  ["item_id", "city", "quality"]
816
  ).tail(1).copy()
817
 
818
+ def predict_24h(self) -> pd.DataFrame:
819
+ latest = self.get_latest()
820
+ if latest.empty or self.model_24h is None:
821
+ return pd.DataFrame()
822
+ preds = predict(self.model_24h, latest, "24h")
 
 
 
 
 
823
  if preds.empty:
824
  return preds
825
 
826
  if not self.prices_df.empty:
827
+ pi = self.prices_df[["item_id", "city", "quality",
828
+ "sell_price_min", "buy_price_max"]].copy()
829
+ for c in ["sell_price_min", "buy_price_max"]:
830
+ pi[c] = pd.to_numeric(pi[c], errors="coerce").astype("float64")
831
+ preds = preds.merge(pi, on=["item_id", "city", "quality"], how="left")
832
  preds["spread"] = preds["sell_price_min"] - preds["buy_price_max"]
833
  else:
834
  preds["spread"] = np.nan
835
 
836
  preds["net_return_pct"] = preds["predicted_return_pct"] - (TOTAL_TRADE_COST * 100)
837
+ dip = preds.apply(compute_dip_signal, axis=1, result_type="expand")
838
+ preds["Dip Sinyali"] = dip[0]
839
+ preds["Sinyal Detayı"] = dip[1]
 
840
  return preds
841
 
842
+ def predict_90d(self) -> pd.DataFrame:
843
+ latest = self.get_latest()
844
+ if latest.empty or self.model_90d is None:
845
+ return pd.DataFrame()
846
+ return predict(self.model_90d, latest, "90d")
847
 
848
 
849
  pipeline = DataPipeline()
 
857
  def get_dashboard_metrics():
858
  diag = pipeline._diagnostics
859
  if pipeline.featured_df.empty:
860
+ return pd.DataFrame({"Metrik": ["Durum"], "Değer": ["Veri yok."]})
 
 
 
861
 
862
  df = pipeline.featured_df
863
+ m = {
864
+ "Veri Kaynağı": "Public AODP API",
 
 
 
865
  "Toplam Gözlem": len(df),
866
+ "Hedefli 24h": diag.get("n_24h", 0),
867
+ "Hedefli 90d": diag.get("n_90d", 0),
868
+ "Sınıf 24h (A/S/Y)": str(diag.get("class_24h", {})),
869
+ "Sınıf 90d (A/S/Y)": str(diag.get("class_90d", {})),
870
+ "Model 24h": "Eğitildi" if pipeline.model_24h else "Yok",
871
+ "Model 90d": "Eğitildi" if pipeline.model_90d else "Yok",
872
  "Tarih Aralığı": f"{df['timestamp'].min().date()} - {df['timestamp'].max().date()}",
 
873
  "Son Güncelleme": pipeline.last_update.strftime("%Y-%m-%d %H:%M") if pipeline.last_update else "—",
874
  }
875
+ return pd.DataFrame(list(m.items()), columns=["Metrik", "Değer"])
876
+
877
+
878
+ def get_prediction_24h_table():
879
+ preds = pipeline.predict_24h()
880
+ if preds.empty:
881
+ return pd.DataFrame({"Bilgi": ["24h tahmini yok."]})
882
+
883
+ preds = preds.copy()
884
+ preds["Ürün"] = preds["item_id"].apply(item_display_name)
885
+ preds["Kalite"] = preds["quality"].apply(quality_name)
886
+ preds["Şehir"] = preds["city"]
887
+ preds["Mevcut"] = preds["avg_price"].round(0)
888
+ preds["24s Tahmin"] = preds["predicted_price"].round(0)
889
+ preds["Değişim %"] = preds["predicted_return_pct"].round(2)
890
+ preds["Sınıf"] = preds["pred_class"].map({0: "↓", 1: "→", 2: "↑"})
891
+ preds["P(↑)"] = (preds["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
892
+ preds["Güven"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
893
+ preds["Net %"] = preds["net_return_pct"].round(2)
894
+
895
+ cols = ["Ürün", "Kalite", "Şehir", "Mevcut", "24s Tahmin",
896
+ "Değişim %", "Sınıf", "P(↑)", "Güven", "Net %", "Dip Sinyali"]
897
+ return preds[cols].sort_values("Güven", ascending=False)
898
+
899
+
900
+ def get_prediction_90d_table():
901
+ preds = pipeline.predict_90d()
902
+ if preds.empty:
903
+ return pd.DataFrame({"Bilgi": [
904
+ "3 aylık tahmin için yeterli veri yok. "
905
+ "En az 6 ay geçmiş veri gerekir. API'den daha fazla ürün/şehir ekleyin."
906
+ ]})
907
+
908
+ preds = preds.copy()
909
+ preds["Ürün"] = preds["item_id"].apply(item_display_name)
910
+ preds["Kalite"] = preds["quality"].apply(quality_name)
911
+ preds["Şehir"] = preds["city"]
912
+ preds["Mevcut"] = preds["avg_price"].round(0)
913
+ preds["3 Ay Tahmini"] = preds["predicted_price"].round(0)
914
+ preds["Değişim %"] = preds["predicted_return_pct"].round(2)
915
+ preds["Trend"] = preds["pred_class"].map({0: "↓ AŞAĞI", 1: "→ SABİT", 2: "↑ YUKARI"})
916
+ preds["P(↑)"] = (preds["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
917
+ preds["Güven"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
918
+
919
+ cols = ["Ürün", "Kalite", "Şehir", "Mevcut", "3 Ay Tahmini",
920
+ "Değişim %", "Trend", "P(↑)", "Güven"]
921
+ return preds[cols].sort_values("Güven", ascending=False)
922
 
923
 
924
  def get_analysis_table():
925
  if pipeline.featured_df.empty:
926
  return pd.DataFrame()
927
 
928
+ latest = pipeline.get_latest()
 
 
 
929
  if not pipeline.prices_df.empty:
930
+ pi = pipeline.prices_df[["item_id", "city", "quality",
931
+ "sell_price_min", "buy_price_max"]].copy()
932
+ for c in ["sell_price_min", "buy_price_max"]:
933
+ pi[c] = pd.to_numeric(pi[c], errors="coerce").astype("float64")
934
+ latest = latest.merge(pi, on=["item_id", "city", "quality"], how="left")
935
  else:
936
  latest["sell_price_min"] = np.nan
937
  latest["buy_price_max"] = np.nan
 
939
  latest["Ürün"] = latest["item_id"].apply(item_display_name)
940
  latest["Kalite"] = latest["quality"].apply(quality_name)
941
  latest["Şehir"] = latest["city"]
942
+ latest["Fiyat"] = latest["avg_price"].round(0)
943
  latest["Hacim"] = latest["item_count"].fillna(0).astype(int)
944
  latest["Spread"] = (latest["sell_price_min"] - latest["buy_price_max"]).round(0)
945
  latest["RSI"] = latest["rsi_14"].round(0)
946
  latest["7g%"] = (latest["price_pct_7d"] * 100).round(0)
947
+ latest["30g Trend"] = latest["trend_slope_30d"].round(4)
 
 
948
 
949
+ cols = ["Ürün", "Kalite", "Şehir", "Fiyat", "Hacim", "Spread",
950
+ "RSI", "7g%", "30g Trend"]
951
  return latest[cols].sort_values("Hacim", ascending=False).head(100)
952
 
953
 
954
+ def get_dip_table():
955
+ preds = pipeline.predict_24h()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
956
  if preds.empty:
957
  return pd.DataFrame({"Bilgi": ["Tahmin yok."]})
 
958
  dip = preds[preds["Dip Sinyali"].isin(["🟢 GÜÇLÜ AL", "🟡 ZAYIF AL", "🔵 İZLE"])].copy()
959
  if dip.empty:
960
+ return pd.DataFrame({"Bilgi": ["Dip sinyali yok."]})
961
 
962
  order = {"🟢 GÜÇLÜ AL": 0, "🟡 ZAYIF AL": 1, "🔵 İZLE": 2}
963
  dip["_r"] = dip["Dip Sinyali"].map(order)
 
964
  dip["Ürün"] = dip["item_id"].apply(item_display_name)
965
  dip["Kalite"] = dip["quality"].apply(quality_name)
966
  dip["Şehir"] = dip["city"]
967
+ dip["Mevcut"] = dip["avg_price"].round(0)
968
  dip["RSI"] = dip["rsi_14"].round(0)
969
  dip["7g%"] = (dip["price_pct_7d"] * 100).round(0)
970
  dip["P(↑)"] = (dip["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
971
+ dip["Net %"] = dip["net_return_pct"].round(2)
 
972
  dip = dip.sort_values(["_r", "confidence"], ascending=[True, False])
973
+
974
+ cols = ["Dip Sinyali", "Ürün", "Kalite", "Şehir", "Mevcut",
975
+ "RSI", "7g%", "P(↑)", "Net %", "Sinyal Detayı"]
976
  return dip[cols].head(50)
977
 
978
 
 
986
  prices["quality"] = pd.to_numeric(prices["quality"], errors="coerce").fillna(1).astype(int)
987
  for c in ["sell_price_min", "buy_price_max"]:
988
  prices[c] = pd.to_numeric(prices[c], errors="coerce").astype("float64")
 
989
  prices = prices[prices["sell_price_min"] > 0]
990
  if prices.empty:
991
+ return pd.DataFrame({"Bilgi": ["Fiyat yok."]})
992
 
 
993
  buy = prices.loc[prices.groupby(["item_id", "quality"])["sell_price_min"].idxmin()][
994
  ["item_id", "quality", "city", "sell_price_min"]
995
  ].rename(columns={"city": "buy_city", "sell_price_min": "buy_price"})
 
1005
 
1006
  arb["brüt"] = arb["sell_price"] - arb["buy_price"]
1007
  arb["maliyet"] = arb["buy_price"] * SETUP_FEE_RATE + arb["sell_price"] * (SETUP_FEE_RATE + TRANSACTION_TAX_RATE)
1008
+ arb["net"] = arb["brüt"] - arb["maliyet"]
1009
+ arb["marj"] = (arb["net"] / arb["buy_price"]) * 100
1010
+ arb = arb[arb["net"] > 0].sort_values("marj", ascending=False)
1011
 
1012
  if arb.empty:
1013
  return pd.DataFrame({"Bilgi": ["Kârlı arbitraj yok."]})
1014
 
1015
  arb["Ürün"] = arb["item_id"].apply(item_display_name)
1016
  arb["Kalite"] = arb["quality"].apply(quality_name)
1017
+ arb["Alış"] = arb["buy_city"]
1018
+ arb["Satış"] = arb["sell_city"]
1019
+ arb["Alış Fiyatı"] = arb["buy_price"].round(0)
1020
+ arb["Satış Fiyatı"] = arb["sell_price"].round(0)
1021
+ arb["Net Kâr"] = arb["net"].round(0)
1022
+ arb["Marj %"] = arb["marj"].round(1)
1023
+
1024
+ cols = ["Ürün", "Kalite", "Alış", "Satış", "Alış Fiyatı", "Satış Fiyatı", "Net Kâr", "Marj %"]
1025
  return arb[cols].head(50)
1026
 
1027
 
1028
+ def get_importance_plot(horizon: str = "24h"):
1029
+ model = pipeline.model_24h if horizon == "24h" else pipeline.model_90d
1030
+ if model is None:
1031
+ return go.Figure().update_layout(title=f"{horizon} model yok", template="plotly_dark")
1032
  try:
1033
+ df = pd.DataFrame({
1034
+ "Özellik": model.feature_name_,
1035
+ "Önem": model.feature_importances_,
1036
  }).sort_values("Önem", ascending=True).tail(20)
1037
+ fig = px.bar(df, x="Önem", y="Özellik", orientation="h",
1038
+ title=f"{horizon} Feature Importance", template="plotly_dark")
1039
  fig.update_layout(height=600)
1040
  return fig
1041
  except Exception as e:
1042
  return go.Figure().update_layout(title=f"Hata: {e}", template="plotly_dark")
1043
 
1044
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1045
  def list_csv_files() -> pd.DataFrame:
1046
  rows = []
1047
  if os.path.isdir(DATA_DIR):
1048
+ for f in sorted(os.listdir(DATA_DIR)):
1049
+ if f.endswith(".csv"):
1050
+ fp = os.path.join(DATA_DIR, f)
1051
  try:
1052
  rows.append({
1053
+ "Dosya": f,
1054
+ "Boyut (KB)": round(os.path.getsize(fp) / 1024, 2),
1055
+ "Son Değişiklik": dt.datetime.fromtimestamp(os.path.getmtime(fp)).strftime("%Y-%m-%d %H:%M:%S"),
1056
  })
1057
  except OSError:
1058
  continue
1059
  return pd.DataFrame(rows) if rows else pd.DataFrame(columns=["Dosya", "Boyut (KB)", "Son Değişiklik"])
1060
 
1061
 
1062
+ def refresh_all():
1063
  try:
1064
  pipeline.fetch_data()
1065
  if not pipeline.featured_df.empty:
1066
+ pipeline.train_all()
1067
  except Exception as e:
1068
  pipeline.status_message = f"Hata: {e}"
1069
  import traceback
1070
  traceback.print_exc()
1071
 
 
 
 
 
 
 
1072
  def _safe(fn, default):
1073
  try:
1074
  return fn()
1075
+ except Exception:
1076
+ return default
1077
 
1078
  return (
1079
  pipeline.status_message,
1080
  _safe(get_dashboard_metrics, pd.DataFrame()),
1081
  _safe(get_analysis_table, pd.DataFrame()),
1082
+ _safe(get_prediction_24h_table, pd.DataFrame()),
1083
+ _safe(get_prediction_90d_table, pd.DataFrame()),
1084
+ _safe(get_dip_table, pd.DataFrame()),
1085
  _safe(get_arbitrage_table, pd.DataFrame()),
1086
+ _safe(get_importance_plot, go.Figure().update_layout(title="—", template="plotly_dark")),
1087
+ _safe(lambda: get_importance_plot("90d"), go.Figure().update_layout(title="—", template="plotly_dark")),
1088
  _safe(list_csv_files, pd.DataFrame()),
1089
  )
1090
 
 
1099
  return (
1100
  _safe(get_dashboard_metrics, pd.DataFrame({"Metrik": ["Durum"], "Değer": ["Hazır"]})),
1101
  pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame(),
1102
+ pd.DataFrame(), pd.DataFrame(),
1103
+ go.Figure().update_layout(title="Yenile", template="plotly_dark"),
1104
  go.Figure().update_layout(title="Yenile", template="plotly_dark"),
 
 
 
1105
  _safe(list_csv_files, pd.DataFrame()),
1106
  )
1107
 
 
1115
 
1116
  gr.Markdown(
1117
  """
1118
+ # 🦅 GOSHAWK ALBION MARKET AI v3.0
1119
+ **Çok ufuklu tahmin:** 24 saat + 3 ay (90 gün) | Avrupa Sunucusu
 
1120
 
1121
+ **⚠️ Risk Uyarısı:** 3 aylık tahmin **düşük doğrulukludur** (patch/sezon değişimleri modeli bozar).
1122
+ Kesin fiyat değil, **trend yönü** olarak kullanın.
1123
  """
1124
  )
1125
 
1126
  with gr.Row():
1127
+ refresh_btn = gr.Button("🔄 Verileri Yenile ve Modelleri Eğit", variant="primary", scale=1)
1128
  status_text = gr.Textbox(label="Durum", interactive=False, scale=2, value=pipeline.status_message)
1129
 
1130
  with gr.Tabs():
1131
  with gr.TabItem("📊 Dashboard"):
1132
  dashboard_table = gr.Dataframe(label="Özet", interactive=False, wrap=True)
1133
  gr.Markdown(
1134
+ "### İki Model\n"
1135
+ "**24h Model:** kısa vadeli sinyaller için, ±%3 sınırı, yüksek doğruluk\n"
1136
+ "**90d Model:** uzun vadeli trend için, ±%10 sınırı, düşük doğruluk\n\n"
1137
+ "### 3 Aylık Tahmin Sınırları\n"
1138
+ "- En az **6 ay geçmiş veri** gerekir\n"
1139
+ "- API'de çoğu ürün için 3-4 ay veri var → **sınırlı**\n"
1140
+ "- Yeni patch/sezon çıkarsa **tamamen yanılır**\n"
1141
+ "- **Trend yönü** için kullan, fiyat için değil\n\n"
 
 
1142
  "### Riskler\n"
1143
+ "- 3 aylık tahmin = düşük güven\n"
1144
+ "- Model bir önceki dönemi öğrenir, geleceği değil"
1145
  )
1146
 
1147
+ with gr.TabItem("🔮 24 Saat Tahmin"):
1148
+ pred_24h_table = gr.Dataframe(label="24s Tahminler", interactive=False)
1149
 
1150
+ with gr.TabItem("📅 3 Aylık Tahmin"):
1151
+ pred_90d_table = gr.Dataframe(label="90 Gün Tahminler", interactive=False)
1152
+ gr.Markdown(
1153
+ "*3 aylık tahmin **trend göstergesidir**, kesin fiyat değil. "
1154
+ "Yeni patch, sezon, meta değişimi modeli bozar. "
1155
+ "**Güven %50'nin altındaysa işlem yapmayın.***"
1156
+ )
1157
+
1158
+ with gr.TabItem("🎯 DİP ALIM"):
1159
+ dip_table = gr.Dataframe(label="Dip Alım Sinyalleri (24h)", interactive=False)
1160
 
1161
+ with gr.TabItem("📦 Ürün Analizi"):
1162
  analysis_table = gr.Dataframe(label="Ürün Analizi", interactive=False)
1163
 
1164
  with gr.TabItem("🚚 Arbitraj"):
1165
  arbitrage_table = gr.Dataframe(label="Arbitraj (Kalite Bazlı)", interactive=False)
 
 
 
 
1166
 
1167
+ with gr.TabItem("📈 24h Model"):
1168
+ importance_24h = gr.Plot(label="24h Feature Importance")
1169
 
1170
+ with gr.TabItem("📈 90d Model"):
1171
+ importance_90d = gr.Plot(label="90d Feature Importance")
 
 
1172
 
1173
  with gr.TabItem("🗄️ Veri"):
1174
  gr.Markdown(
1175
  "**API:** europe.albion-online-data.com\n"
1176
+ "**Kalite:** 1, 2, 3\n"
1177
+ "**Time-scale:** 6h\n"
1178
+ f"**24h sınırı:** ±%{CLASS_UP_24H*100:.0f}\n"
1179
+ f"**90d sınırı:** ±%{CLASS_UP_90D*100:.0f}"
1180
  )
1181
+ csv_files = gr.Dataframe(label="CSV", interactive=False,
1182
  headers=["Dosya", "Boyut (KB)", "Son Değişiklik"])
1183
 
1184
  refresh_btn.click(
1185
+ fn=refresh_all,
1186
  inputs=[],
1187
+ outputs=[status_text, dashboard_table, analysis_table, pred_24h_table,
1188
+ pred_90d_table, dip_table, arbitrage_table, importance_24h,
1189
+ importance_90d, csv_files],
1190
  )
1191
 
1192
  demo.load(
1193
  fn=initial_load,
1194
  inputs=[],
1195
+ outputs=[dashboard_table, analysis_table, pred_24h_table, pred_90d_table,
1196
+ dip_table, arbitrage_table, importance_24h, importance_90d, csv_files],
 
1197
  )
1198
 
1199