SniperGold_ML/ml/download_bars.py

230 lines
9 KiB
Python
Raw Permalink Normal View History

# -*- coding: utf-8 -*-
"""
Algo Forge - Tahap 4 (ML Hibrida): download_bars.py
====================================================
Unduh data OHLCV HISTORIS PANJANG (±20 tahun) dari terminal MetaTrader 5
(HFM Demo, simbol XAUUSD) secara chunked per tahun, lalu VERIFIKASI
kontinuitas (gap detection + sanity OHLC) dan simpan ke:
MQL5\\Files\\AlgoForge\\Data\\XAUUSD_<TF>.csv (arsip teks, audit)
MQL5\\Files\\AlgoForge\\Data\\XAUUSD_<TF>.npz (arrays numpy, cepat)
TIDAK menyentuh Files\\SniperGold_ML (cache baseline Fase 3 = deploy freeze).
Cara pakai:
python download_bars.py [--symbol XAUUSD] [--tf M15,H1,H4,D1] [--force] [--probe]
--probe : hanya lapor ketersediaan (bar count, rentang tanggal) tanpa unduh.
--force : unduh ulang walau file sudah ada.
--start : ISO date awal (default: 2000-01-01 -> terminal kembalikan yang ada).
Verifikasi kontinuitas:
- urutan waktu strict naik, tanpa duplikat
- step antar bar dibandingkan dengan step TF; gap <= 3x step dianggap normal
- gap 3x..4 hari = weekend/holiday (dicatat, bukan error)
- gap > 4 hari = ANOMALI (dilaporkan; total & terpanjang)
- sanity OHLC: high >= max(o,c), low <= min(o,c), volume >= 0
"""
import os
import sys
import json
import argparse
import datetime as dt
import numpy as np
import MetaTrader5 as mt5
# ----------------------------------------------------------------------
BASE = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "..", "..", "Files", "AlgoForge", "Data")
BASE = os.path.normpath(BASE)
TF_MAP = {
"M15": mt5.TIMEFRAME_M15,
"H1": mt5.TIMEFRAME_H1,
"H4": mt5.TIMEFRAME_H4,
"D1": mt5.TIMEFRAME_D1,
}
STEP = {"M15": 15 * 60, "H1": 3600, "H4": 4 * 3600, "D1": 86400}
GAP_ANOMALY = 4 * 86400 # > 4 hari = anomali
GAP_WEEKEND = 3 * 3600 # batas "normal" (3x step utk M15)
DEFAULT_TFS = ["M15", "H1", "H4", "D1"]
def ensure_dir(path):
os.makedirs(path, exist_ok=True)
def fetch_range_chunked(symbol, tf_enum, start, end, step_sec):
"""Unduh [start, end] per-chunk ~ 2 tahun, gabung, urutkan, dedup."""
chunks = []
cur = start
while cur < end:
nxt = min(cur + dt.timedelta(days=730), end)
rates = mt5.copy_rates_range(symbol, tf_enum, cur, nxt)
if rates is None or len(rates) == 0:
err = mt5.last_error()
print(f" [chunk {cur.date()}..{nxt.date()}] kosong ({err})")
else:
chunks.append(rates)
print(f" [chunk {cur.date()}..{nxt.date()}] {len(rates)} bar")
cur = nxt
if not chunks:
return None
allr = np.concatenate(chunks)
# urutkan & dedup (jaga-jaga overlap chunk)
allr = allr[np.argsort(allr["time"], kind="stable")]
_, idx = np.unique(allr["time"], return_index=True)
allr = allr[idx]
return allr
def verify_continuity(rates, tf_name, out_report):
"""Cek urutan, duplikat, gap, sanity OHLC. Kembali dict laporan."""
t = rates["time"].astype(np.int64)
step = STEP[tf_name]
rep = {"tf": tf_name, "n": int(len(rates))}
if len(rates) == 0:
rep.update(status="EMPTY")
return rep
dup = int(np.sum(np.diff(t) == 0))
not_inc = int(np.sum(np.diff(t) < 0))
d = np.diff(t)
gaps = d[d > step * 3] # lebih dari 3x step
normal_gaps = int(np.sum((d > step) & (d <= step * 3)))
weekend_gaps = int(np.sum((d > step * 3) & (d <= GAP_ANOMALY)))
anomaly_gaps = int(np.sum(d > GAP_ANOMALY))
longest = int(d.max()) if len(d) else 0
# sanity OHLC
bad_hl = int(np.sum(rates["high"] < np.maximum(rates["open"], rates["close"])))
bad_low = int(np.sum(rates["low"] > np.minimum(rates["open"], rates["close"])))
bad_vol = int(np.sum(rates["tick_volume"] < 0))
first = dt.datetime.utcfromtimestamp(int(t[0])).strftime("%Y-%m-%d %H:%M")
last = dt.datetime.utcfromtimestamp(int(t[-1])).strftime("%Y-%m-%d %H:%M")
span_years = (t[-1] - t[0]) / (365.25 * 86400)
rep.update(
status="OK",
first=first, last=last,
span_years=round(float(span_years), 2),
dup=dup, not_increasing=not_inc,
normal_gaps=normal_gaps, weekend_gaps=weekend_gaps,
anomaly_gaps=anomaly_gaps, longest_gap_sec=int(longest),
bad_hl=bad_hl, bad_low=bad_low, bad_vol=bad_vol,
)
if dup or not_inc or anomaly_gaps or bad_hl or bad_low or bad_vol:
rep["status"] = "WARN"
return rep
def save(symbol, tf_name, rates):
path_csv = os.path.join(BASE, f"{symbol}_{tf_name}.csv")
path_npz = os.path.join(BASE, f"{symbol}_{tf_name}.npz")
with open(path_csv, "w", encoding="utf-8") as f:
f.write("time,open,high,low,close,tick_volume,spread\n")
for r in rates:
tt = dt.datetime.utcfromtimestamp(int(r["time"])).strftime("%Y-%m-%d %H:%M:%S")
f.write(f"{tt},{r['open']:.5f},{r['high']:.5f},{r['low']:.5f},"
f"{r['close']:.5f},{int(r['tick_volume'])},{int(r['spread'])}\n")
np.savez_compressed(path_npz,
time=rates["time"].astype(np.int64),
open=rates["open"].astype(np.float64),
high=rates["high"].astype(np.float64),
low=rates["low"].astype(np.float64),
close=rates["close"].astype(np.float64),
tick_volume=rates["tick_volume"].astype(np.int64),
spread=rates["spread"].astype(np.int64))
print(f" -> {path_csv} ({len(rates)} bar)")
print(f" -> {path_npz}")
return path_csv, path_npz
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--symbol", default="XAUUSD")
ap.add_argument("--tf", default=",".join(DEFAULT_TFS))
ap.add_argument("--start", default="2000-01-01")
ap.add_argument("--force", action="store_true")
ap.add_argument("--probe", action="store_true")
args = ap.parse_args()
tfs = [x.strip().upper() for x in args.tf.split(",") if x.strip()]
for x in tfs:
if x not in TF_MAP:
print(f"TF tidak dikenal: {x} (pilih {list(TF_MAP)})")
sys.exit(2)
if not mt5.initialize():
print("Gagal initialize():", mt5.last_error())
sys.exit(1)
info = mt5.terminal_info()
si = mt5.symbol_info(args.symbol)
if info is None or si is None:
print(f"Terminal/simbol {args.symbol} tidak ditemukan:", mt5.last_error())
mt5.shutdown()
sys.exit(1)
print(f"Terminal: {info.name} | {info.path}")
print(f"Simbol : {args.symbol} | {si.description} | digits={si.digits}")
ensure_dir(BASE)
start = dt.datetime.strptime(args.start, "%Y-%m-%d")
end = dt.datetime.now(dt.timezone.utc).replace(tzinfo=None) + dt.timedelta(days=2)
all_reports = []
for tf_name in tfs:
tf_enum = TF_MAP[tf_name]
print(f"\n=== {args.symbol} {tf_name} ===")
# probe: bar pertama & jumlah total
r0 = mt5.copy_rates_from_pos(args.symbol, tf_enum, 0, 1)
bars_total = mt5.symbol_info(args.symbol).history_bars(tf_enum, start, end) \
if hasattr(mt5.symbol_info(args.symbol), "history_bars") else -1
try:
r_first = mt5.copy_rates_range(args.symbol, tf_enum, start, end)
except Exception as e: # noqa
r_first = None
print(f" [probe gagal] {e}")
if r0 is not None and len(r0):
print(f" bar terbaru : {dt.datetime.utcfromtimestamp(int(r0[0]['time']))}")
if r_first is not None and len(r_first):
print(f" probe range : {len(r_first)} bar | "
f"mulai {dt.datetime.utcfromtimestamp(int(r_first[0]['time']))} | "
f"akhir {dt.datetime.utcfromtimestamp(int(r_first[-1]['time']))}")
full = r_first
rep = verify_continuity(full, tf_name, None)
all_reports.append(rep)
print(f" [probe] n={rep['n']} span={rep['span_years']}y "
f"anomali_gap={rep['anomaly_gaps']} status={rep['status']}")
if args.probe:
continue
out_csv, out_npz = save(args.symbol, tf_name, full)
rep.update(csv=os.path.basename(out_csv), npz=os.path.basename(out_npz))
else:
print(f" [probe] tidak ada data di range (mulai {args.start})")
mt5.shutdown()
# laporan ringkas
print("\n=== RINGKASAN KONTINUITAS ===")
for rep in all_reports:
print(f" {rep['tf']:4s} n={rep['n']:7d} span={rep['span_years']:6.2f}y "
f"{rep.get('first')} .. {rep.get('last')} | dup={rep.get('dup',0)} "
f"anom={rep.get('anomaly_gaps',0)} longest={rep.get('longest_gap_sec',0)}s "
f"status={rep.get('status')}")
if all_reports and not args.probe:
path_rep = os.path.join(BASE, "download_report.json")
with open(path_rep, "w", encoding="utf-8") as f:
json.dump({"generated": dt.datetime.utcnow().isoformat(),
"symbol": args.symbol, "reports": all_reports},
f, indent=2)
print(f"\nLaporan: {path_rep}")
print("Selesai.")
if __name__ == "__main__":
main()