Spaces:
Sleeping
Sleeping
Delete knn_calibrate.py
Browse files- knn_calibrate.py +0 -277
knn_calibrate.py
DELETED
|
@@ -1,277 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
KNN KALİBRASYON v2 — rastgele arama + genişleyen pencereli walk-forward + gürültü (null) testi.
|
| 3 |
-
|
| 4 |
-
Kullanım (app.py ile aynı klasörde, internet gerekir):
|
| 5 |
-
python knn_calibrate.py # BTC-USDT 1H, 4000 bar, 120 örnek, 6 dilim
|
| 6 |
-
python knn_calibrate.py BTC-USDT 1H 6000 200 8 # sembol bar toplam_bar örnek dilim
|
| 7 |
-
python knn_calibrate.py BTC-USDT 1H 4000 120 6 --no-null --force-save
|
| 8 |
-
|
| 9 |
-
Dinamik SL/TP (dyn, tpq, slq) de aranan parametrelerdir; sabit SL/TP ile aynı walk-forward'da yarışır.
|
| 10 |
-
|
| 11 |
-
Neden bu kadar titiz:
|
| 12 |
-
* KNN eğitim gerektirmez (nedensel) -> tek bir geçmiş geçişi tüm dilimler için geçerlidir.
|
| 13 |
-
* WALK-FORWARD: dilim k için parametreler yalnızca k'dan ÖNCE tamamen sonuçlanmış işlemlere bakılarak seçilir,
|
| 14 |
-
sonra dilim k'da test edilir. Birleşik test (OOS) işlemleri "seçim PROSEDÜRÜNÜN" dürüst performansıdır.
|
| 15 |
-
* Seçim ölçütü kâr faktörü tepe değeri değil, ortalama R'nin ALT GÜVEN SINIRI (ort − hata): az işlemli şanslı
|
| 16 |
-
kombinasyonlar cezalandırılır. Win rate hedef değil; başa baş win rate her satırda gösterilir.
|
| 17 |
-
* NULL TESTİ: aynı prosedür, bar blokları karıştırılmış (zaman yapısı yok edilmiş) fiyat üzerinde çalıştırılır.
|
| 18 |
-
Gürültüde de "iyi" görünüyorsa prosedür kendini kandırıyor demektir.
|
| 19 |
-
* knn_params.json YALNIZCA test (OOS) ortalama R'si için %95 bootstrap alt sınırı > 0 ise yazılır (--force-save ile zorlanır).
|
| 20 |
-
"""
|
| 21 |
-
import datetime as dt
|
| 22 |
-
import json
|
| 23 |
-
import os
|
| 24 |
-
import sys
|
| 25 |
-
import time
|
| 26 |
-
|
| 27 |
-
os.environ["APP_NO_LAUNCH"] = "1"
|
| 28 |
-
import numpy as np
|
| 29 |
-
import pandas as pd
|
| 30 |
-
|
| 31 |
-
import app
|
| 32 |
-
|
| 33 |
-
MIN_TRAIN_TRADES = 15
|
| 34 |
-
BOOT = 2000
|
| 35 |
-
SPACE = {
|
| 36 |
-
"K": (7, 11, 15, 21, 31, 45), "edge": (0.10, 0.15, 0.20, 0.30, 0.40, 0.50),
|
| 37 |
-
"fwd": (3, 5, 8, 12), "move": (0.10, 0.15, 0.25, 0.35), "pool": (400, 800, 1500),
|
| 38 |
-
"sl": (1.0, 1.5, 2.0, 2.5), "tp": (1.5, 2.0, 2.5, 3.5, 5.0), "thr": (2, 3, 4),
|
| 39 |
-
"dyn": (0, 1), "tpq": (35, 50, 65), "slq": (60, 75, 90), # dyn=1: KNN dinamik SL/TP (sl/tp = yedek sabit değerler)
|
| 40 |
-
# --- v3: giriş filtreleri + zaman çıkışı (skorlar önbellekten gelir, bunlar KNN taramasını yeniden çalıştırmaz) ---
|
| 41 |
-
"align": (0, 1), # 1: piyasa yapısı sinyal yönüne ters ise işlem yok
|
| 42 |
-
"adx": (0, 15, 20, 25), # >0: ADX alt sınırı
|
| 43 |
-
"fee_r": (0.0, 0.30, 0.20, 0.12), # >0: komisyonun R maliyeti üst sınırı (düşük volatilite = pahalı işlem)
|
| 44 |
-
"hold": (12, 24, 48), # zaman çıkışı (bar)
|
| 45 |
-
}
|
| 46 |
-
BASELINE = {"K": 15, "edge": 0.15, "fwd": 5, "move": 0.15, "pool": 1500, "sl": 1.5, "tp": 2.5, "thr": 2,
|
| 47 |
-
"dyn": 1, "tpq": 50, "slq": 75, "align": 0, "adx": 0, "fee_r": 0.0, "hold": 24}
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
# ---------------------------------------------------------------------------
|
| 51 |
-
def fetch_history(inst_id="BTC-USDT", bar="1H", total=4000):
|
| 52 |
-
rows, after = [], None
|
| 53 |
-
while len(rows) < total:
|
| 54 |
-
p = {"instId": inst_id, "bar": bar, "limit": 100}
|
| 55 |
-
if after:
|
| 56 |
-
p["after"] = after
|
| 57 |
-
raw = app._get("/api/v5/market/history-candles", p)
|
| 58 |
-
if not raw:
|
| 59 |
-
break
|
| 60 |
-
rows += raw
|
| 61 |
-
after = raw[-1][0] # en eski mum -> sonraki sayfa daha eskiyi getirir
|
| 62 |
-
time.sleep(0.12)
|
| 63 |
-
cols = ["ts", "o", "h", "l", "c", "vol", "volCcy", "volCcyQuote", "confirm"]
|
| 64 |
-
df = pd.DataFrame(rows, columns=cols).drop_duplicates("ts")
|
| 65 |
-
for c in ["o", "h", "l", "c", "vol"]:
|
| 66 |
-
df[c] = df[c].astype(float)
|
| 67 |
-
df["confirm"] = df["confirm"].astype(int)
|
| 68 |
-
df["ts"] = pd.to_datetime(df["ts"].astype(np.int64), unit="ms")
|
| 69 |
-
df = df[df["confirm"] == 1].sort_values("ts").reset_index(drop=True)
|
| 70 |
-
return df.tail(total).reset_index(drop=True)
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def shuffle_blocks(df, block=24, seed=0):
|
| 74 |
-
"""NULL verisi: bar getirilerini bloklar halinde karıştırır (dağılım korunur, zamansal yapı yok olur)."""
|
| 75 |
-
rng = np.random.default_rng(seed)
|
| 76 |
-
prev = df["c"].shift(1)
|
| 77 |
-
rel = pd.DataFrame({k: df[k] / prev for k in ("o", "h", "l", "c")}).iloc[1:].reset_index(drop=True)
|
| 78 |
-
vol = df["vol"].iloc[1:].reset_index(drop=True)
|
| 79 |
-
idx = np.arange(len(rel))
|
| 80 |
-
blocks = [idx[i:i + block] for i in range(0, len(idx), block)]
|
| 81 |
-
order = np.concatenate([blocks[j] for j in rng.permutation(len(blocks))])
|
| 82 |
-
rel, vol = rel.iloc[order].reset_index(drop=True), vol.iloc[order].reset_index(drop=True)
|
| 83 |
-
c = float(df["c"].iloc[0]) * rel["c"].cumprod()
|
| 84 |
-
pc = np.r_[float(df["c"].iloc[0]), c.to_numpy()[:-1]]
|
| 85 |
-
out = pd.DataFrame({"ts": df["ts"].iloc[1:].reset_index(drop=True), "o": rel["o"] * pc, "h": rel["h"] * pc,
|
| 86 |
-
"l": rel["l"] * pc, "c": c, "vol": vol})
|
| 87 |
-
out["h"] = out[["o", "h", "l", "c"]].max(axis=1)
|
| 88 |
-
out["l"] = out[["o", "h", "l", "c"]].min(axis=1)
|
| 89 |
-
return out
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def sample_params(rng, n):
|
| 93 |
-
out = [dict(BASELINE)] # 0. örnek = mevcut varsayılanlar (kıyas için)
|
| 94 |
-
seen = {tuple(BASELINE.values())}
|
| 95 |
-
while len(out) < n:
|
| 96 |
-
p = {k: (type(v[0])(rng.choice(v))) for k, v in SPACE.items()}
|
| 97 |
-
if not p["dyn"]:
|
| 98 |
-
p["tpq"], p["slq"] = BASELINE["tpq"], BASELINE["slq"] # dyn=0 iken yüzdelikler anlamsız -> tekrarı önle
|
| 99 |
-
if p["tp"] <= p["sl"] * 0.5 or tuple(p.values()) in seen:
|
| 100 |
-
continue
|
| 101 |
-
seen.add(tuple(p.values()))
|
| 102 |
-
out.append(p)
|
| 103 |
-
return out
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
def _set_knn(p):
|
| 107 |
-
app.KNN_K, app.KNN_MIN_EDGE, app.KNN_FORWARD = p["K"], p["edge"], p["fwd"]
|
| 108 |
-
app.KNN_MOVE_ATR, app.KNN_POOL_MAX = p["move"], p["pool"]
|
| 109 |
-
app.KNN_TIERS = app._knn_tiers(p["edge"])
|
| 110 |
-
app.DYNAMIC_EXITS, app.DYN_TP_PCT, app.DYN_SL_PCT = bool(p["dyn"]), p["tpq"], p["slq"]
|
| 111 |
-
app.ENTRY_REQUIRE_ALIGN, app.ENTRY_MIN_ADX, app.ENTRY_MAX_FEE_R = False, 0.0, 0.0 # önbellek = FİLTRESİZ skorlar
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def _set_exec(p):
|
| 115 |
-
"""Skor önbelleğinden bağımsız parametreler: SL/TP, zaman çıkışı, giriş filtreleri."""
|
| 116 |
-
app.SL_ATR, app.TP_ATR, app.MAX_HOLD_BARS = p["sl"], p["tp"], int(p["hold"])
|
| 117 |
-
app.ENTRY_REQUIRE_ALIGN, app.ENTRY_MIN_ADX, app.ENTRY_MAX_FEE_R = bool(p["align"]), float(p["adx"]), float(p["fee_r"])
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
def evaluate_all(df, params, start0, log=True):
|
| 121 |
-
"""Her parametre kümesi için TÜM zamanın işlemlerini üretir (skorlar KNN anahtarına göre önbelleğe alınır)."""
|
| 122 |
-
n = len(df)
|
| 123 |
-
ind = app.compute_indicators(df)
|
| 124 |
-
cache, results = {}, []
|
| 125 |
-
t0 = time.time()
|
| 126 |
-
for k, p in enumerate(params):
|
| 127 |
-
key = (p["K"], p["edge"], p["fwd"], p["move"], p["pool"], p["dyn"], p["tpq"], p["slq"])
|
| 128 |
-
if key not in cache:
|
| 129 |
-
_set_knn(p)
|
| 130 |
-
ind["_arr"]["knn_y"] = app.knn_labels(df, ind, forward=p["fwd"], move=p["move"])
|
| 131 |
-
cache[key] = [app._score_at(ind, i) for i in range(start0, n)]
|
| 132 |
-
_set_exec(p)
|
| 133 |
-
raw = cache[key]
|
| 134 |
-
if p["align"] or p["adx"] or p["fee_r"]:
|
| 135 |
-
raw = [app.apply_entry_filters(sc, ind["_arr"], start0 + j) for j, sc in enumerate(raw)]
|
| 136 |
-
trades = app.simulate_trades(df, ind, start0, raw, threshold=p["thr"])
|
| 137 |
-
results.append((p, trades))
|
| 138 |
-
if log and (k + 1) % 20 == 0:
|
| 139 |
-
print(f" {k + 1}/{len(params)} kombinasyon ({time.time() - t0:.0f}s)", flush=True)
|
| 140 |
-
return results
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
def objective(trades):
|
| 144 |
-
"""Ortalama R'nin alt güven sınırı (ort − hata). İşlem azsa None."""
|
| 145 |
-
if len(trades) < MIN_TRAIN_TRADES:
|
| 146 |
-
return None
|
| 147 |
-
r = np.array([t["r"] for t in trades])
|
| 148 |
-
return float(r.mean() - r.std(ddof=1) / np.sqrt(len(r)))
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
def summarize(trades, seed=1):
|
| 152 |
-
if not trades:
|
| 153 |
-
return None
|
| 154 |
-
r = np.array([t["r"] for t in trades])
|
| 155 |
-
pnl = np.array([t["pnl"] for t in trades])
|
| 156 |
-
gl = abs(pnl[pnl <= 0].sum())
|
| 157 |
-
rng = np.random.default_rng(seed)
|
| 158 |
-
bm = rng.choice(r, size=(BOOT, len(r)), replace=True).mean(axis=1)
|
| 159 |
-
win_r, loss_r = r[pnl > 0], r[pnl <= 0]
|
| 160 |
-
be = float(abs(loss_r.mean()) / (win_r.mean() + abs(loss_r.mean()))) if len(win_r) and len(loss_r) else float("nan")
|
| 161 |
-
return {"n": len(r), "wr": float((pnl > 0).mean()), "pf": float(pnl[pnl > 0].sum() / gl) if gl > 0 else float("inf"),
|
| 162 |
-
"mean_r": float(r.mean()), "ci_lo": float(np.percentile(bm, 2.5)), "ci_hi": float(np.percentile(bm, 97.5)),
|
| 163 |
-
"ret": float(np.prod(1 + pnl) - 1), "be_wr": be}
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
def walk_forward(results, start0, n, folds):
|
| 167 |
-
edges = np.linspace(start0, n, folds + 1).astype(int)
|
| 168 |
-
oos, picks = [], []
|
| 169 |
-
for k in range(1, folds):
|
| 170 |
-
cut = edges[k]
|
| 171 |
-
best = None
|
| 172 |
-
for p, tr in results:
|
| 173 |
-
sc = objective([t for t in tr if t["exit_i"] < cut]) # yalnızca tamamen sonuçlanmış geçmiş
|
| 174 |
-
if sc is not None and (best is None or sc > best[0]):
|
| 175 |
-
best = (sc, p, tr)
|
| 176 |
-
if best is None:
|
| 177 |
-
continue
|
| 178 |
-
seg = [t for t in best[2] if edges[k] <= t["signal_i"] < edges[k + 1]]
|
| 179 |
-
oos += seg
|
| 180 |
-
picks.append((k, best[1], len(seg), summarize(seg)))
|
| 181 |
-
base = [t for t in results[0][1] if t["signal_i"] >= edges[1]]
|
| 182 |
-
return oos, picks, base
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
def final_pick(results):
|
| 186 |
-
best = None
|
| 187 |
-
for p, tr in results:
|
| 188 |
-
sc = objective(tr)
|
| 189 |
-
if sc is not None and (best is None or sc > best[0]):
|
| 190 |
-
best = (sc, p, tr)
|
| 191 |
-
return best
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
def procedure(df, n_samples, folds, seed=7, log=True):
|
| 195 |
-
n = len(df)
|
| 196 |
-
start0 = app.WARMUP_BARS + app.KNN_MIN_POOL + max(SPACE["fwd"])
|
| 197 |
-
params = sample_params(np.random.default_rng(seed), n_samples)
|
| 198 |
-
results = evaluate_all(df, params, start0, log)
|
| 199 |
-
oos, picks, base = walk_forward(results, start0, n, folds)
|
| 200 |
-
return {"results": results, "oos": oos, "picks": picks, "base": base, "final": final_pick(results), "n": n, "start0": start0}
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
def _pdesc(p):
|
| 204 |
-
ex = f"DİNAMİK TP%{p['tpq']}/SL%{p['slq']} (yedek {p['sl']}/{p['tp']})" if p["dyn"] else f"SABİT SL/TP={p['sl']}/{p['tp']}"
|
| 205 |
-
flt = f"filtre[yapı={'uyumlu' if p['align'] else '-'} ADX≥{p['adx']} fee≤{p['fee_r']}R] tutuş={p['hold']}"
|
| 206 |
-
return f"K={p['K']} edge={p['edge']} fwd={p['fwd']} move={p['move']} pool={p['pool']} {ex} eşik={p['thr']} {flt}"
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
def _fmt(s):
|
| 210 |
-
if not s:
|
| 211 |
-
return "—"
|
| 212 |
-
return (f"n={s['n']:<3d} WR=%{s['wr'] * 100:4.1f} (başa baş %{s['be_wr'] * 100:4.1f}) PF={min(s['pf'], 99):4.2f} "
|
| 213 |
-
f"getiri={s['ret'] * 100:+.1f}% ort.R={s['mean_r']:+.3f} [%95 GA {s['ci_lo']:+.3f}…{s['ci_hi']:+.3f}]")
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
def main():
|
| 217 |
-
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
| 218 |
-
flags = {a for a in sys.argv[1:] if a.startswith("--")}
|
| 219 |
-
inst = args[0] if len(args) > 0 else "BTC-USDT"
|
| 220 |
-
bar = args[1] if len(args) > 1 else "1H"
|
| 221 |
-
total = int(args[2]) if len(args) > 2 else 4000
|
| 222 |
-
n_samples = int(args[3]) if len(args) > 3 else 200
|
| 223 |
-
folds = int(args[4]) if len(args) > 4 else 6
|
| 224 |
-
|
| 225 |
-
print(f"{inst} {bar}: {total} bar indiriliyor...")
|
| 226 |
-
df = fetch_history(inst, bar, total)
|
| 227 |
-
print(f"{len(df)} bar: {df['ts'].iloc[0]} → {df['ts'].iloc[-1]}")
|
| 228 |
-
print(f"\n[1/2] GERÇEK veri: {n_samples} kombinasyon, {folds} dilimli walk-forward")
|
| 229 |
-
real = procedure(df, n_samples, folds)
|
| 230 |
-
oos, base = summarize(real["oos"]), summarize(real["base"])
|
| 231 |
-
print("\n── Dilim dilim seçilen parametreler (her biri yalnızca geçmişe bakarak) ──")
|
| 232 |
-
for k, p, m, s in real["picks"]:
|
| 233 |
-
print(f" dilim {k}: {_pdesc(p)} → test {_fmt(s)}")
|
| 234 |
-
print("\n── BİRLEŞİK TEST (OOS) ──")
|
| 235 |
-
print(" Kalibre prosedür :", _fmt(oos))
|
| 236 |
-
print(" Sabit varsayılan :", _fmt(base))
|
| 237 |
-
|
| 238 |
-
null = None
|
| 239 |
-
if "--no-null" not in flags:
|
| 240 |
-
print(f"\n[2/2] NULL testi (bar blokları karıştırılmış fiyat; aynı prosedür)")
|
| 241 |
-
nl = procedure(shuffle_blocks(df), n_samples, folds, log=False)
|
| 242 |
-
null = summarize(nl["oos"])
|
| 243 |
-
print(" Null OOS :", _fmt(null))
|
| 244 |
-
|
| 245 |
-
fin = real["final"]
|
| 246 |
-
print("\n── DÜRÜST HÜKÜM ──")
|
| 247 |
-
evidence = bool(oos and oos["ci_lo"] > 0 and oos["n"] >= 30 and oos["pf"] > 1.0 and oos["ret"] > 0)
|
| 248 |
-
pos = [s for _, _, _, s in real["picks"] if s]
|
| 249 |
-
if pos and sum(1 for s in pos if s["mean_r"] > 0) < len(pos) / 2:
|
| 250 |
-
print(f" ⚠ Test dilimlerinin yarısından fazlasında ort. R ≤ 0 ({sum(1 for s in pos if s['mean_r'] > 0)}/{len(pos)} pozitif): kâr birkaç dilime sıkışmış.")
|
| 251 |
-
evidence = False
|
| 252 |
-
if null and null["mean_r"] > 0.05:
|
| 253 |
-
print(f" ⚠ Null testinde bile ort. R {null['mean_r']:+.3f}: prosedür gürültüden 'kâr' üretiyor, sonuçlara temkinli yaklaş.")
|
| 254 |
-
evidence = evidence and oos["mean_r"] > null["mean_r"] + 0.05
|
| 255 |
-
if evidence:
|
| 256 |
-
print(f" ✔ OOS ort. R %95 alt sınırı > 0 ({oos['ci_lo']:+.3f}) ve ≥30 işlem: istatistiksel kanıt VAR (garanti değil, rejim değişebilir).")
|
| 257 |
-
else:
|
| 258 |
-
print(" ✘ Kalibre edilmiş sistemin OOS kârlılığı için yeterli istatistiksel kanıt YOK." + (" (--force-save: yine de yazılıyor)" if "--force-save" in flags else " Parametreler dosyaya yazılmaz."))
|
| 259 |
-
if fin:
|
| 260 |
-
p = fin[1]
|
| 261 |
-
print(f" Tüm veride en iyi (iyimser, in-sample): {_pdesc(p)} → {_fmt(summarize(fin[2]))}")
|
| 262 |
-
if evidence or "--force-save" in flags:
|
| 263 |
-
out = {"instrument": inst, "bar": bar, "bars": len(df), "created": dt.datetime.now().isoformat(timespec="seconds"),
|
| 264 |
-
"params": {"KNN_K": p["K"], "KNN_MIN_EDGE": p["edge"], "KNN_FORWARD": p["fwd"], "KNN_MOVE_ATR": p["move"],
|
| 265 |
-
"KNN_POOL_MAX": p["pool"], "SL_ATR": p["sl"], "TP_ATR": p["tp"], "SIGNAL_THRESHOLD": float(p["thr"]),
|
| 266 |
-
"DYNAMIC_EXITS": bool(p["dyn"]), "DYN_TP_PCT": p["tpq"], "DYN_SL_PCT": p["slq"],
|
| 267 |
-
"MAX_HOLD_BARS": int(p["hold"]), "ENTRY_REQUIRE_ALIGN": bool(p["align"]),
|
| 268 |
-
"ENTRY_MIN_ADX": float(p["adx"]), "ENTRY_MAX_FEE_R": float(p["fee_r"])},
|
| 269 |
-
"oos": oos, "null_oos": null}
|
| 270 |
-
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "knn_params.json")
|
| 271 |
-
with open(path, "w", encoding="utf-8") as f:
|
| 272 |
-
json.dump(out, f, indent=2, ensure_ascii=False)
|
| 273 |
-
print(f" → {path} yazıldı. app.py bir sonraki açılışta bu parametreleri otomatik yükler.")
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
if __name__ == "__main__":
|
| 277 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|