GoshawkVortexAI commited on
Commit
9fffddb
·
verified ·
1 Parent(s): a88bc98

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +256 -221
app.py CHANGED
@@ -1,6 +1,6 @@
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,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, mean_absolute_error
19
 
20
  import gradio as gr
21
 
@@ -105,25 +105,28 @@ 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
@@ -132,7 +135,6 @@ 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",
@@ -145,15 +147,14 @@ FEATURE_COLS_24H = [
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",
@@ -165,7 +166,7 @@ FEATURE_COLS_90D = [
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)
@@ -189,7 +190,6 @@ def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
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]
@@ -213,7 +213,6 @@ def fetch_current_prices(item_ids: List[str], cities: List[str], qualities: str
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]
@@ -295,7 +294,7 @@ def save_to_csv(df: pd.DataFrame, name: str) -> str:
295
 
296
 
297
  # ---------------------------------------------------------------------------
298
- # FEATURE ENGINEERING
299
  # ---------------------------------------------------------------------------
300
 
301
 
@@ -308,13 +307,11 @@ def compute_rsi(series: pd.Series, period: int = 14) -> pd.Series:
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
 
@@ -322,7 +319,6 @@ def _linear_trend_slope(y: np.ndarray) -> float:
322
  def build_features(df: pd.DataFrame) -> pd.DataFrame:
323
  if df.empty:
324
  return df
325
-
326
  if "quality" not in df.columns:
327
  df["quality"] = 1
328
 
@@ -339,12 +335,16 @@ def build_features(df: pd.DataFrame) -> pd.DataFrame:
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(
@@ -357,13 +357,6 @@ def build_features(df: pd.DataFrame) -> pd.DataFrame:
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
  )
@@ -381,7 +374,6 @@ def build_features(df: pd.DataFrame) -> pd.DataFrame:
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
  )
@@ -397,27 +389,25 @@ def build_features(df: pd.DataFrame) -> pd.DataFrame:
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):
418
  if len(g) < 5:
419
  continue
420
-
421
  g = g.sort_values("timestamp")
422
  left = pd.DataFrame({
423
  "timestamp": g["timestamp"].values,
@@ -429,43 +419,39 @@ def _compute_forward_targets(df: pd.DataFrame) -> pd.DataFrame:
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
 
@@ -502,12 +488,11 @@ def enrich_spread_features(featured_df: pd.DataFrame, prices_df: pd.DataFrame) -
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
 
@@ -524,9 +509,8 @@ def _prepare_model_df(featured_df: pd.DataFrame, horizon: str) -> Tuple[pd.DataF
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
 
@@ -534,22 +518,20 @@ def _prepare_model_df(featured_df: pd.DataFrame, horizon: str) -> Tuple[pd.DataF
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
@@ -560,8 +542,10 @@ def train_model(X: pd.DataFrame, y: pd.Series, horizon: str) -> Optional[lgb.LGB
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:]
@@ -577,13 +561,16 @@ def train_model(X: pd.DataFrame, y: pd.Series, horizon: str) -> Optional[lgb.LGB
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
 
@@ -597,24 +584,22 @@ def predict(model, latest_features: pd.DataFrame, horizon: str) -> pd.DataFrame:
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
 
@@ -625,19 +610,19 @@ def predict(model, latest_features: pd.DataFrame, horizon: str) -> pd.DataFrame:
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)
@@ -645,6 +630,45 @@ def predict(model, latest_features: pd.DataFrame, horizon: str) -> pd.DataFrame:
645
  return out
646
 
647
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
648
  # ---------------------------------------------------------------------------
649
  # DİP SİNYALİ
650
  # ---------------------------------------------------------------------------
@@ -654,39 +678,28 @@ 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
-
680
- if passed >= 5:
681
- sig = "🟢 GÜÇLÜ AL"
682
- elif passed == 4:
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
 
@@ -701,18 +714,19 @@ class DataPipeline:
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
@@ -721,7 +735,7 @@ class DataPipeline:
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,7 +754,7 @@ class DataPipeline:
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")
@@ -755,18 +769,21 @@ class DataPipeline:
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:
@@ -783,30 +800,32 @@ class DataPipeline:
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:
@@ -839,11 +858,18 @@ class DataPipeline:
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()
@@ -862,14 +888,15 @@ def get_dashboard_metrics():
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"])
@@ -879,46 +906,51 @@ 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():
@@ -945,7 +977,6 @@ def get_analysis_table():
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)
@@ -970,7 +1001,6 @@ def get_dip_table():
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)
@@ -1020,13 +1050,12 @@ def get_arbitrage_table():
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:
@@ -1069,22 +1098,29 @@ def refresh_all():
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
 
@@ -1095,10 +1131,10 @@ def initial_load():
1095
  return fn()
1096
  except Exception:
1097
  return default
1098
-
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"),
@@ -1115,68 +1151,67 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
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"])
@@ -1185,15 +1220,15 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
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
 
 
1
  # app.py
2
+ # GOSHAWK ALBION MARKET AI v3.1
3
+ # 24h model + 90d hibrit (model varsa model, yoksa trend projeksiyonu)
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
19
 
20
  import gradio as gr
21
 
 
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_30D = os.path.join(MODEL_DIR, "gos_hawk_30d.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
116
 
117
+ # 24h model
118
  CLASS_DOWN_24H = -0.03
119
  CLASS_UP_24H = 0.03
120
  TARGET_TOLERANCE_HOURS_24H = 12
121
 
122
+ # 30 GÜN model (3 ay yerine 30 gün — veri bulunabilirliği için)
123
+ HORIZON_DAYS_30D = 30
124
+ CLASS_DOWN_30D = -0.10
125
+ CLASS_UP_30D = 0.10
126
+ TARGET_TOLERANCE_HOURS_30D = 48
127
+
128
+ # Trend projeksiyon (30d veri yoksa fallback)
129
+ TREND_PROJECTION_DAYS = 90
130
 
131
  # Dip sinyali
132
  DIP_RSI_MAX = 40.0
 
135
  DIP_MIN_PREDICTED_PCT = -1.0
136
  DIP_MAX_SPREAD_PCT = 0.15
137
 
 
138
  FEATURE_COLS_24H = [
139
  "hour", "dayofweek",
140
  "lag_24h_log", "lag_168h_log", "lag_336h_log",
 
147
  "item_code", "city_code",
148
  ]
149
 
150
+ FEATURE_COLS_30D = [
 
151
  "month", "dayofweek",
152
+ "lag_168h_log", "lag_336h_log", "lag_720h_log",
153
+ "chg_168h", "chg_336h", "chg_720h",
154
  "rsi_14", "volatility_24h", "volatility_168h",
155
  "volume_ratio",
156
+ "ma_168h_ratio", "ma_720h_ratio",
157
+ "trend_slope_30d",
158
  "city_price_deviation",
159
  "spread_pct", "buy_sell_ratio", "qual_code",
160
  "item_code", "city_code",
 
166
 
167
 
168
  def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
169
+ headers = {"User-Agent": "GOSHAWK-ALBION-MARKET-AI/3.1", "Accept": "application/json"}
170
  for attempt in range(MAX_RETRIES):
171
  try:
172
  resp = requests.get(url, params=params, headers=headers, timeout=REQUEST_TIMEOUT)
 
190
 
191
 
192
  def fetch_current_prices(item_ids: List[str], cities: List[str], qualities: str = "1,2,3") -> pd.DataFrame:
 
193
  all_data = []
194
  for i in range(0, len(item_ids), 40):
195
  chunk = item_ids[i:i+40]
 
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
  all_records = []
217
  for i in range(0, len(item_ids), 15):
218
  chunk = item_ids[i:i+15]
 
294
 
295
 
296
  # ---------------------------------------------------------------------------
297
+ # FEATURES
298
  # ---------------------------------------------------------------------------
299
 
300
 
 
307
 
308
 
309
  def _linear_trend_slope(y: np.ndarray) -> float:
 
310
  if len(y) < 5:
311
  return 0.0
312
  x = np.arange(len(y), dtype=float)
313
  try:
314
+ return float(np.polyfit(x, y, 1)[0])
 
315
  except Exception:
316
  return 0.0
317
 
 
319
  def build_features(df: pd.DataFrame) -> pd.DataFrame:
320
  if df.empty:
321
  return df
 
322
  if "quality" not in df.columns:
323
  df["quality"] = 1
324
 
 
335
  group_cols = ["item_id", "city", "quality"]
336
  df["log_price"] = np.log(df["avg_price"].replace(0, np.nan))
337
 
338
+ # 24h lag'ler
339
  df["lag_24h_log"] = df.groupby(group_cols)["log_price"].shift(24)
340
  df["lag_168h_log"] = df.groupby(group_cols)["log_price"].shift(168)
341
  df["lag_336h_log"] = df.groupby(group_cols)["log_price"].shift(336)
342
 
343
+ # 30d lag'ler
344
+ df["lag_720h_log"] = df.groupby(group_cols)["log_price"].shift(720)
345
+ df["lag_1440h_log"] = df.groupby(group_cols)["log_price"].shift(1440)
346
+
347
+ for chg in [1, 6, 24, 168, 336, 720, 1440]:
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(
 
357
  )
358
  df["ma_168h_ratio"] = df["ma_168h_raw"] / df["avg_price"].replace(0, np.nan) - 1.0
359
 
 
 
 
 
 
 
 
360
  df["ma_720h_raw"] = df.groupby(group_cols)["avg_price"].transform(
361
  lambda x: x.rolling(720, min_periods=72).mean()
362
  )
 
374
  )
375
  df["volume_ratio"] = df["item_count"] / df["volume_ma_24h"].replace(0, np.nan)
376
 
 
377
  df["trend_slope_30d"] = df.groupby(group_cols)["log_price"].transform(
378
  lambda x: x.rolling(720, min_periods=72).apply(_linear_trend_slope, raw=True)
379
  )
 
389
  )
390
  df["is_downtrend"] = ((df["chg_6h"] < 0) & (df["chg_24h"] < 0)).astype(int)
391
 
392
+ df = _compute_targets(df)
393
  return df
394
 
395
 
396
+ def _compute_targets(df: pd.DataFrame) -> pd.DataFrame:
397
  if df.empty or "log_price" not in df.columns:
398
+ for c in ["target_log_return_24h", "target_class_24h",
399
+ "target_log_return_30d", "target_class_30d"]:
400
+ df[c] = np.nan
 
401
  return df
402
 
403
  df["target_log_return_24h"] = np.nan
404
+ df["target_log_return_30d"] = np.nan
405
 
406
  group_cols = ["item_id", "city", "quality"]
407
 
408
+ for _, g in df.groupby(group_cols):
409
  if len(g) < 5:
410
  continue
 
411
  g = g.sort_values("timestamp")
412
  left = pd.DataFrame({
413
  "timestamp": g["timestamp"].values,
 
419
  "log_price_future": g["log_price"].values,
420
  }).sort_values("timestamp")
421
 
422
+ # 24h
423
  left["target_t_24h"] = left["timestamp"] + pd.Timedelta(hours=24)
424
  m24 = pd.merge_asof(
425
  left.sort_values("target_t_24h"),
426
  right.sort_values("timestamp"),
427
+ left_on="target_t_24h", right_on="timestamp",
428
+ direction="nearest", tolerance=pd.Timedelta(hours=TARGET_TOLERANCE_HOURS_24H),
 
 
429
  suffixes=("", "_f"),
430
  )
431
  df.loc[m24["idx"].values, "target_log_return_24h"] = (
432
  m24["log_price_future"].values - m24["log_price_now"].values
433
  )
434
 
435
+ # 30g
436
+ left["target_t_30d"] = left["timestamp"] + pd.Timedelta(days=HORIZON_DAYS_30D)
437
+ m30 = pd.merge_asof(
438
+ left.sort_values("target_t_30d"),
439
  right.sort_values("timestamp"),
440
+ left_on="target_t_30d", right_on="timestamp",
441
+ direction="nearest", tolerance=pd.Timedelta(hours=TARGET_TOLERANCE_HOURS_30D),
 
 
442
  suffixes=("", "_f"),
443
  )
444
+ df.loc[m30["idx"].values, "target_log_return_30d"] = (
445
+ m30["log_price_future"].values - m30["log_price_now"].values
446
  )
447
 
448
  t24 = df["target_log_return_24h"]
449
  df["target_class_24h"] = np.where(t24 < CLASS_DOWN_24H, 0, np.where(t24 > CLASS_UP_24H, 2, 1))
450
  df.loc[t24.isna(), "target_class_24h"] = np.nan
451
 
452
+ t30 = df["target_log_return_30d"]
453
+ df["target_class_30d"] = np.where(t30 < CLASS_DOWN_30D, 0, np.where(t30 > CLASS_UP_30D, 2, 1))
454
+ df.loc[t30.isna(), "target_class_30d"] = np.nan
455
 
456
  return df
457
 
 
488
 
489
 
490
  # ---------------------------------------------------------------------------
491
+ # MODEL EĞİTİMİ
492
  # ---------------------------------------------------------------------------
493
 
494
 
495
  def _prepare_model_df(featured_df: pd.DataFrame, horizon: str) -> Tuple[pd.DataFrame, pd.Series, pd.DataFrame]:
 
496
  if featured_df.empty:
497
  return pd.DataFrame(), pd.Series(dtype=int), pd.DataFrame()
498
 
 
509
  df = df.dropna(subset=[target_col]).copy()
510
  df[target_col] = df[target_col].astype(int)
511
 
512
+ fill_chg = ["chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h", "chg_720h", "chg_1440h"]
513
+ for c in fill_chg:
 
514
  if c in df.columns:
515
  df[c] = df[c].fillna(0.0)
516
 
 
518
  if c in df.columns:
519
  df[c] = df[c].fillna(df["log_price"])
520
 
521
+ df["rsi_14"] = df["rsi_14"].fillna(50.0)
 
522
  for c in ["volatility_24h", "volatility_168h", "volume_ratio", "ma_24h_ratio",
523
  "ma_168h_ratio", "ma_720h_ratio", "trend_slope_30d", "city_price_deviation",
524
  "spread_pct", "buy_sell_ratio"]:
525
  if c in df.columns:
526
  df[c] = df[c].fillna(0.0)
527
 
528
+ fcols = FEATURE_COLS_24H if horizon == "24h" else FEATURE_COLS_30D
529
+ fcols = [c for c in fcols if c in df.columns]
530
+ model_df = df.dropna(subset=fcols + [target_col]).copy()
 
531
  if model_df.empty:
532
  return pd.DataFrame(), pd.Series(dtype=int), pd.DataFrame()
533
 
534
+ X = model_df[fcols].astype("float64")
535
  y = model_df[target_col]
536
  meta = model_df[["item_id", "city", "quality", "timestamp", "avg_price"]].copy()
537
  return X, y, meta
 
542
  return None
543
  cc = y.value_counts().to_dict()
544
  print(f"[{horizon}] sınıf: {cc}")
545
+ if len(cc) < 2:
546
  return None
547
+ if len(cc) < 3:
548
+ print(f"[{horizon}] UYARI: sadece {len(cc)} sınıf var")
549
 
550
  split = int(len(X) * 0.8)
551
  X_tr, X_val = X.iloc[:split], X.iloc[split:]
 
561
  reg_alpha=0.5, reg_lambda=1.0,
562
  verbose=-1, random_state=42, n_jobs=-1,
563
  )
564
+ try:
565
+ model.fit(
566
+ X_tr, y_tr, eval_set=[(X_val, y_val)],
567
+ callbacks=[lgb.early_stopping(30, verbose=False), lgb.log_evaluation(0)],
568
+ )
569
+ acc = accuracy_score(y_val, model.predict(X_val))
570
+ print(f"[{horizon}] val_accuracy={acc:.4f}")
571
+ except Exception as e:
572
+ print(f"[{horizon}] eğitim hatası: {e}")
573
+ return None
574
  return model
575
 
576
 
 
584
  if "qual_code" not in df.columns:
585
  df["qual_code"] = df.get("quality", 1)
586
 
587
+ for c in ["chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h", "chg_720h", "chg_1440h"]:
 
588
  if c in df.columns:
589
  df[c] = df[c].fillna(0.0)
590
  for c in ["lag_24h_log", "lag_168h_log", "lag_336h_log", "lag_720h_log", "lag_1440h_log"]:
591
  if c in df.columns:
592
  df[c] = df[c].fillna(df["log_price"])
593
+ df["rsi_14"] = df["rsi_14"].fillna(50.0)
 
594
  for c in ["volatility_24h", "volatility_168h", "volume_ratio", "ma_24h_ratio",
595
  "ma_168h_ratio", "ma_720h_ratio", "trend_slope_30d", "city_price_deviation",
596
  "spread_pct", "buy_sell_ratio"]:
597
  if c in df.columns:
598
  df[c] = df[c].fillna(0.0)
599
 
600
+ fcols = FEATURE_COLS_24H if horizon == "24h" else FEATURE_COLS_30D
601
+ fcols = [c for c in fcols if c in df.columns]
602
+ X_ready = df[fcols].astype("float64").dropna()
603
  if X_ready.empty:
604
  return pd.DataFrame()
605
 
 
610
  exp_log = probs[:, 0] * CLASS_DOWN_24H + probs[:, 2] * CLASS_UP_24H
611
  clipped = np.clip(exp_log, CLASS_DOWN_24H, CLASS_UP_24H)
612
  else:
613
+ exp_log = probs[:, 0] * CLASS_DOWN_30D + probs[:, 2] * CLASS_UP_30D
614
+ clipped = np.clip(exp_log, CLASS_DOWN_30D, CLASS_UP_30D)
615
 
616
  keep = ["item_id", "city", "quality", "avg_price", "item_count",
617
  "volatility_24h", "rsi_14", "volume_ratio", "price_pct_7d",
618
+ "is_downtrend", "chg_6h", "chg_24h", "chg_168h", "trend_slope_30d",
619
  "spread_pct", "buy_sell_ratio"]
620
  keep = [c for c in keep if c in df.columns]
621
  out = df.loc[X_ready.index, keep].copy()
622
  out["pred_class"] = pred_class
623
  out["prob_down"] = probs[:, 0]
624
+ out["prob_flat"] = probs[:, 1] if probs.shape[1] > 1 else 0.0
625
+ out["prob_up"] = probs[:, 2] if probs.shape[1] > 2 else 0.0
626
  out["predicted_log_return"] = clipped
627
  out["predicted_return_pct"] = (np.exp(clipped) - 1) * 100
628
  out["predicted_price"] = out["avg_price"] * np.exp(clipped)
 
630
  return out
631
 
632
 
633
+ # ---------------------------------------------------------------------------
634
+ # TREND PROJEKSİYONU (30d model yoksa fallback)
635
+ # ---------------------------------------------------------------------------
636
+
637
+
638
+ def trend_projection_90d(latest: pd.DataFrame) -> pd.DataFrame:
639
+ """
640
+ 30d model yoksa, trend_slope_30d kullanarak 90 günlük basit projeksiyon.
641
+ Formül: fiyat × exp(slope × gün_sayısı)
642
+ slope günlük log değişim → 90 günde slope × 90
643
+ """
644
+ if latest.empty:
645
+ return pd.DataFrame()
646
+
647
+ df = latest.copy()
648
+ df["trend_slope_30d"] = df.get("trend_slope_30d", pd.Series(0.0, index=df.index)).fillna(0.0)
649
+
650
+ # slope * 90 gün = 90 günlük log getiri
651
+ log_return_90d = df["trend_slope_30d"] * TREND_PROJECTION_DAYS
652
+
653
+ # Aşırı projeksiyonu clip'le (90 günde ±%100)
654
+ log_return_90d = np.clip(log_return_90d, -1.0, 1.0)
655
+
656
+ df["predicted_log_return"] = log_return_90d
657
+ df["predicted_return_pct"] = (np.exp(log_return_90d) - 1) * 100
658
+ df["predicted_price"] = df["avg_price"] * np.exp(log_return_90d)
659
+
660
+ # Güven: trend gücüne göre
661
+ df["confidence"] = np.abs(df["trend_slope_30d"]).clip(0, 0.005) / 0.005 * 0.6 + 0.3
662
+
663
+ df["pred_class"] = np.where(log_return_90d < -0.05, 0,
664
+ np.where(log_return_90d > 0.05, 2, 1))
665
+ df["prob_up"] = np.where(log_return_90d > 0.05, 0.6, 0.2)
666
+ df["prob_down"] = np.where(log_return_90d < -0.05, 0.6, 0.2)
667
+ df["prob_flat"] = 1.0 - df["prob_up"] - df["prob_down"]
668
+
669
+ return df
670
+
671
+
672
  # ---------------------------------------------------------------------------
673
  # DİP SİNYALİ
674
  # ---------------------------------------------------------------------------
 
678
  reasons, passed = [], 0
679
  rsi = row.get("rsi_14", 50)
680
  if pd.notna(rsi) and rsi < DIP_RSI_MAX:
681
+ passed += 1; reasons.append(f"RSI={rsi:.0f}")
 
682
  pct = row.get("price_pct_7d", 0.5)
683
  if pd.notna(pct) and pct < DIP_PERCENTILE_MAX:
684
+ passed += 1; reasons.append(f"7g%={pct*100:.0f}")
 
685
  vr = row.get("volume_ratio", 1.0)
686
  if pd.notna(vr) and vr >= DIP_MIN_VOLUME_RATIO:
687
+ passed += 1; reasons.append(f"H×{vr:.1f}")
 
688
  pp = row.get("predicted_return_pct", 0)
689
  if pd.notna(pp) and pp >= DIP_MIN_PREDICTED_PCT:
690
+ passed += 1; reasons.append(f"M={pp:+.1f}%")
 
691
  sp = row.get("spread_pct", np.nan)
692
  if pd.notna(sp) and abs(sp) < DIP_MAX_SPREAD_PCT:
693
+ passed += 1; reasons.append(f"Sp={sp*100:.1f}%")
 
694
  down = row.get("is_downtrend", 0)
695
  if not down or (pd.notna(pp) and pp > 2):
696
+ passed += 1; reasons.append("Trend✓")
697
+
698
+ if passed >= 5: sig = "🟢 GÜÇLÜ AL"
699
+ elif passed == 4: sig = "🟡 ZAYIF AL"
700
+ elif passed == 3 and pd.notna(rsi) and rsi < 45: sig = "🔵 İZLE"
701
+ elif down and pd.notna(pp) and pp < -2: sig = "🔴 ALMA"
702
+ else: sig = "⚪ BEKLE"
 
 
 
 
 
 
703
  return sig, " · ".join(reasons) if reasons else "—"
704
 
705
 
 
714
  self.prices_df = pd.DataFrame()
715
  self.featured_df = pd.DataFrame()
716
  self.model_24h = None
717
+ self.model_30d = None
718
  self.last_update = None
719
  self.status_message = "Veri yüklenmedi."
720
  self._diagnostics = {}
721
+ self._projection_mode = "MODEL" # veya "TREND"
722
 
723
  if _HAS_JOBLIB:
724
  try:
725
  if os.path.exists(MODEL_PATH_24H):
726
  self.model_24h = joblib.load(MODEL_PATH_24H)
727
+ if os.path.exists(MODEL_PATH_30D):
728
+ self.model_30d = joblib.load(MODEL_PATH_30D)
729
+ if self.model_24h or self.model_30d:
730
  self.status_message = "Kayıtlı model(ler) yüklendi."
731
  except Exception:
732
  pass
 
735
  item_ids = item_ids or ITEM_IDS
736
  cities = cities or CITIES
737
 
738
+ self.status_message = "Public API'den veri çekiliyor..."
739
  ok, msg = check_api_health()
740
  if not ok:
741
  self.status_message = f"API erişilemiyor: {msg}"
 
754
 
755
  self.history_df = clean_market_data(hist)
756
  self.prices_df = prices
757
+ n_q = self.history_df["quality"].nunique()
758
  self.status_message = f"✓ {len(self.history_df)} satır ({n_q} kalite), {len(self.prices_df)} anlık."
759
 
760
  save_to_csv(self.history_df, "history")
 
769
  if df.empty:
770
  self._diagnostics = {}
771
  return
772
+ n24 = int(df["target_class_24h"].notna().sum())
773
+ n30 = int(df["target_class_30d"].notna().sum())
774
  cd24 = df["target_class_24h"].value_counts().to_dict() if n24 > 0 else {}
775
+ cd30 = df["target_class_30d"].value_counts().to_dict() if n30 > 0 else {}
776
+ date_range_days = (df["timestamp"].max() - df["timestamp"].min()).days if len(df) > 0 else 0
777
+
778
  self._diagnostics = {
779
  "n_total": len(df),
780
  "n_24h": n24,
781
+ "n_30d": n30,
782
  "class_24h": cd24,
783
+ "class_30d": cd30,
784
+ "date_range_days": date_range_days,
785
  }
786
+ print(f"[TANI] total={len(df)}, 24h={n24}, 30d={n30}, gün={date_range_days}")
787
 
788
  def train_all(self):
789
  if self.featured_df.empty:
 
800
  except Exception:
801
  pass
802
 
803
+ # 30d
804
+ X30, y30, _ = _prepare_model_df(self.featured_df, "30d")
805
+ n30 = len(X30)
806
+ if n30 >= MIN_TRAIN_SAMPLES:
807
+ self.model_30d = train_model(X30, y30, "30d")
808
+ if _HAS_JOBLIB and self.model_30d:
809
  try:
810
+ joblib.dump(self.model_30d, MODEL_PATH_30D)
811
  except Exception:
812
  pass
813
 
814
+ # Projeksiyon modu belirle
815
+ if self.model_30d is not None:
816
+ self._projection_mode = "MODEL"
 
 
 
 
817
  else:
818
+ self._projection_mode = "TREND"
819
 
820
+ msgs = []
821
+ msgs.append("24h ✓" if self.model_24h else "24h ✗")
822
+ if self.model_30d:
823
+ msgs.append(f"30d ✓ ({n30})")
824
+ else:
825
+ msgs.append(f"30d ✗ ({n30}) → trend projeksiyonu")
826
+ self.status_message = " · ".join(msgs)
827
  self.last_update = dt.datetime.now()
828
+ return True
829
 
830
  def get_latest(self) -> pd.DataFrame:
831
  if self.featured_df.empty:
 
858
  preds["Sinyal Detayı"] = dip[1]
859
  return preds
860
 
861
+ def predict_long_horizon(self) -> Tuple[pd.DataFrame, str]:
862
+ """30d model varsa model, yoksa trend projeksiyonu."""
863
  latest = self.get_latest()
864
+ if latest.empty:
865
+ return pd.DataFrame(), "veri_yok"
866
+
867
+ if self.model_30d is not None:
868
+ preds = predict(self.model_30d, latest, "30d")
869
+ return preds, "model_30d"
870
+ else:
871
+ preds = trend_projection_90d(latest)
872
+ return preds, "trend_projection"
873
 
874
 
875
  pipeline = DataPipeline()
 
888
  df = pipeline.featured_df
889
  m = {
890
  "Veri Kaynağı": "Public AODP API",
891
+ "Veri Süresi": f"{diag.get('date_range_days', 0)} gün",
892
  "Toplam Gözlem": len(df),
893
  "Hedefli 24h": diag.get("n_24h", 0),
894
+ "Hedefli 30d": diag.get("n_30d", 0),
895
+ "Sınıf 24h": str(diag.get("class_24h", {})),
896
+ "Sınıf 30d": str(diag.get("class_30d", {})),
897
+ "24h Model": "Eğitildi" if pipeline.model_24h else "Yok",
898
+ "30d Model": "Eğitildi" if pipeline.model_30d else "Yok",
899
+ "Uzun Vadeli Mod": pipeline._projection_mode,
900
  "Son Güncelleme": pipeline.last_update.strftime("%Y-%m-%d %H:%M") if pipeline.last_update else "—",
901
  }
902
  return pd.DataFrame(list(m.items()), columns=["Metrik", "Değer"])
 
906
  preds = pipeline.predict_24h()
907
  if preds.empty:
908
  return pd.DataFrame({"Bilgi": ["24h tahmini yok."]})
 
909
  preds = preds.copy()
910
  preds["Ürün"] = preds["item_id"].apply(item_display_name)
911
  preds["Kalite"] = preds["quality"].apply(quality_name)
912
  preds["Şehir"] = preds["city"]
913
  preds["Mevcut"] = preds["avg_price"].round(0)
914
+ preds["24s"] = preds["predicted_price"].round(0)
915
  preds["Değişim %"] = preds["predicted_return_pct"].round(2)
916
  preds["Sınıf"] = preds["pred_class"].map({0: "↓", 1: "→", 2: "↑"})
917
  preds["P(↑)"] = (preds["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
918
  preds["Güven"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
919
  preds["Net %"] = preds["net_return_pct"].round(2)
920
+ cols = ["Ürün", "Kalite", "Şehir", "Mevcut", "24s", "Değişim %",
921
+ "Sınıf", "P(↑)", "Güven", "Net %", "Dip Sinyali"]
 
922
  return preds[cols].sort_values("Güven", ascending=False)
923
 
924
 
925
+ def get_prediction_long_table():
926
+ preds, mode = pipeline.predict_long_horizon()
927
  if preds.empty:
928
+ return pd.DataFrame({"Bilgi": ["Uzun vadeli tahmin yok."]}), mode
 
 
 
929
 
930
  preds = preds.copy()
931
  preds["Ürün"] = preds["item_id"].apply(item_display_name)
932
  preds["Kalite"] = preds["quality"].apply(quality_name)
933
  preds["Şehir"] = preds["city"]
934
  preds["Mevcut"] = preds["avg_price"].round(0)
 
 
 
 
 
935
 
936
+ if mode == "model_30d":
937
+ preds["30 Gün Tahmini"] = preds["predicted_price"].round(0)
938
+ preds["Değişim %"] = preds["predicted_return_pct"].round(2)
939
+ preds["Trend"] = preds["pred_class"].map({0: "↓ AŞAĞI", 1: "→ SABİT", 2: "↑ YUKARI"})
940
+ preds["P(↑)"] = (preds["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
941
+ preds["Güven"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
942
+ cols = ["Ürün", "Kalite", "Şehir", "Mevcut", "30 Gün Tahmini",
943
+ "Değişim %", "Trend", "P(↑)", "Güven"]
944
+ else:
945
+ # Trend projeksiyon
946
+ preds["90 Gün Tahmini"] = preds["predicted_price"].round(0)
947
+ preds["Değişim %"] = preds["predicted_return_pct"].round(2)
948
+ preds["Trend"] = preds["pred_class"].map({0: "↓ AŞAĞI", 1: "→ SABİT", 2: "↑ YUKARI"})
949
+ preds["Güven"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
950
+ cols = ["Ürün", "Kalite", "Şehir", "Mevcut", "90 Gün Tahmini",
951
+ "Değişim %", "Trend", "Güven"]
952
+
953
+ return preds[cols].sort_values("Güven", ascending=False), mode
954
 
955
 
956
  def get_analysis_table():
 
977
  latest["RSI"] = latest["rsi_14"].round(0)
978
  latest["7g%"] = (latest["price_pct_7d"] * 100).round(0)
979
  latest["30g Trend"] = latest["trend_slope_30d"].round(4)
 
980
  cols = ["Ürün", "Kalite", "Şehir", "Fiyat", "Hacim", "Spread",
981
  "RSI", "7g%", "30g Trend"]
982
  return latest[cols].sort_values("Hacim", ascending=False).head(100)
 
1001
  dip["P(↑)"] = (dip["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
1002
  dip["Net %"] = dip["net_return_pct"].round(2)
1003
  dip = dip.sort_values(["_r", "confidence"], ascending=[True, False])
 
1004
  cols = ["Dip Sinyali", "Ürün", "Kalite", "Şehir", "Mevcut",
1005
  "RSI", "7g%", "P(↑)", "Net %", "Sinyal Detayı"]
1006
  return dip[cols].head(50)
 
1050
  arb["Satış Fiyatı"] = arb["sell_price"].round(0)
1051
  arb["Net Kâr"] = arb["net"].round(0)
1052
  arb["Marj %"] = arb["marj"].round(1)
 
1053
  cols = ["Ürün", "Kalite", "Alış", "Satış", "Alış Fiyatı", "Satış Fiyatı", "Net Kâr", "Marj %"]
1054
  return arb[cols].head(50)
1055
 
1056
 
1057
  def get_importance_plot(horizon: str = "24h"):
1058
+ model = pipeline.model_24h if horizon == "24h" else pipeline.model_30d
1059
  if model is None:
1060
  return go.Figure().update_layout(title=f"{horizon} model yok", template="plotly_dark")
1061
  try:
 
1098
  import traceback
1099
  traceback.print_exc()
1100
 
1101
+ try:
1102
+ long_table, long_mode = get_prediction_long_table()
1103
+ except Exception:
1104
+ long_table, long_mode = pd.DataFrame(), "error"
1105
+
1106
  def _safe(fn, default):
1107
  try:
1108
  return fn()
1109
  except Exception:
1110
  return default
1111
 
1112
+ mode_msg = f"Uzun vade: {'MODEL (30d)' if long_mode == 'model_30d' else 'TREND PROJEKSİYONU (90d)'}"
1113
+
1114
  return (
1115
+ f"{pipeline.status_message} | {mode_msg}",
1116
  _safe(get_dashboard_metrics, pd.DataFrame()),
1117
  _safe(get_analysis_table, pd.DataFrame()),
1118
  _safe(get_prediction_24h_table, pd.DataFrame()),
1119
+ long_table,
1120
  _safe(get_dip_table, pd.DataFrame()),
1121
  _safe(get_arbitrage_table, pd.DataFrame()),
1122
  _safe(get_importance_plot, go.Figure().update_layout(title="—", template="plotly_dark")),
1123
+ _safe(lambda: get_importance_plot("30d"), go.Figure().update_layout(title="—", template="plotly_dark")),
1124
  _safe(list_csv_files, pd.DataFrame()),
1125
  )
1126
 
 
1131
  return fn()
1132
  except Exception:
1133
  return default
 
1134
  return (
1135
  _safe(get_dashboard_metrics, pd.DataFrame({"Metrik": ["Durum"], "Değer": ["Hazır"]})),
1136
+ pd.DataFrame(), pd.DataFrame(), pd.DataFrame(),
1137
+ pd.DataFrame({"Bilgi": ["Yenile'ye basın"]}),
1138
  pd.DataFrame(), pd.DataFrame(),
1139
  go.Figure().update_layout(title="Yenile", template="plotly_dark"),
1140
  go.Figure().update_layout(title="Yenile", template="plotly_dark"),
 
1151
 
1152
  gr.Markdown(
1153
  """
1154
+ # 🦅 GOSHAWK ALBION MARKET AI v3.1
1155
+ **Hibrit yaklaşım:** 24h model + 30d model (yoksa 90d trend projeksiyonu)
1156
 
1157
+ **⚠️ Uyarı:** Uzun vadeli tahmin **düşük güvenilirlikte**. Trend yönü için kullanın.
 
1158
  """
1159
  )
1160
 
1161
  with gr.Row():
1162
+ refresh_btn = gr.Button("🔄 Verileri Yenile", variant="primary", scale=1)
1163
  status_text = gr.Textbox(label="Durum", interactive=False, scale=2, value=pipeline.status_message)
1164
 
1165
  with gr.Tabs():
1166
  with gr.TabItem("📊 Dashboard"):
1167
+ dashboard_table = gr.Dataframe(label="Özet & Teşhis", interactive=False, wrap=True)
1168
  gr.Markdown(
1169
+ "### Hibrit Yaklaşım\n"
1170
+ "**24h Model:** Her zaman denenir\n"
1171
+ "**30d Model:** Yeterli veri varsa (min 150 örnek) eğitilir\n"
1172
+ "**Trend Projeksiyonu:** 30d model yoksa, 30 günlük lineer trend eğiminden 90 günlük tahmin\n\n"
1173
+ "### Neden 30d, 90d değil?\n"
1174
+ "- 90d model için **6 ay geçmiş veri** gerekir\n"
1175
+ "- API'de genelde 2-4 ay var → 90d model eğitilemez\n"
1176
+ "- 30d model için **2 ay geçmiş** yeterli → çoğu üründe çalışır\n\n"
1177
+ "### Trend Projeksiyonu Formülü\n"
1178
+ "`fiyat × exp(trend_slope_30d × 90 gün)`\n"
1179
+ "- Basit ama az veriyle çalışır\n"
1180
+ "- Patch/sezon değişimini **görmez**"
1181
  )
1182
 
1183
+ with gr.TabItem("🔮 24 Saat"):
1184
  pred_24h_table = gr.Dataframe(label="24s Tahminler", interactive=False)
1185
 
1186
+ with gr.TabItem("📅 Uzun Vade"):
1187
+ pred_long_table = gr.Dataframe(label="30 Gün Modeli veya 90 Gün Trend Projeksiyonu", interactive=False)
1188
  gr.Markdown(
1189
+ "*30d model eğitildiyse 30 günlük tahmin, aksi halde 30 günlük trendden 90 günlük projeksiyon. "
1190
+ "**Güven %50 altındaysa işlem yapmayın.** Patch değişimleri modeli tamamen bozar.*"
 
1191
  )
1192
 
1193
  with gr.TabItem("🎯 DİP ALIM"):
1194
+ dip_table = gr.Dataframe(label="Dip Alım", interactive=False)
1195
 
1196
  with gr.TabItem("📦 Ürün Analizi"):
1197
  analysis_table = gr.Dataframe(label="Ürün Analizi", interactive=False)
1198
 
1199
  with gr.TabItem("🚚 Arbitraj"):
1200
+ arbitrage_table = gr.Dataframe(label="Arbitraj", interactive=False)
1201
 
1202
  with gr.TabItem("📈 24h Model"):
1203
+ importance_24h = gr.Plot(label="24h Önem")
1204
 
1205
+ with gr.TabItem("📈 30d Model"):
1206
+ importance_30d = gr.Plot(label="30d Önem")
1207
 
1208
  with gr.TabItem("🗄️ Veri"):
1209
  gr.Markdown(
1210
  "**API:** europe.albion-online-data.com\n"
1211
+ "**Kalite:** 1-3\n"
1212
+ f"**24h sınır:** ±%{CLASS_UP_24H*100:.0f}\n"
1213
+ f"**30d sınır:** ±%{CLASS_UP_30D*100:.0f}\n"
1214
+ f"**Trend projeksiyon:** {TREND_PROJECTION_DAYS} gün"
1215
  )
1216
  csv_files = gr.Dataframe(label="CSV", interactive=False,
1217
  headers=["Dosya", "Boyut (KB)", "Son Değişiklik"])
 
1220
  fn=refresh_all,
1221
  inputs=[],
1222
  outputs=[status_text, dashboard_table, analysis_table, pred_24h_table,
1223
+ pred_long_table, dip_table, arbitrage_table, importance_24h,
1224
+ importance_30d, csv_files],
1225
  )
1226
 
1227
  demo.load(
1228
  fn=initial_load,
1229
  inputs=[],
1230
+ outputs=[dashboard_table, analysis_table, pred_24h_table, pred_long_table,
1231
+ dip_table, arbitrage_table, importance_24h, importance_30d, csv_files],
1232
  )
1233
 
1234