#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ analyze_l2_offsets.py — ทดสอบความเปราะต่อ bar alignment (EA-SPEC §4.3 ข้อ 3) อ่าน CSV จาก MaePla_L2.mq5 (แท่ง H4 สังเคราะห์ offset 0/1/2/3) แล้ววัด N · tp1_before_sl · การเข้าไม้สุ่มที่ถ่วงทิศเหมือนกัน · เปรียบ · t สถิติทุกตัวใช้ฟังก์ชันเดียวกับ analyze_l1_filters.py (คัดลอกมาตรงๆ ไม่แก้) """ import csv, math, sys, itertools from collections import Counter PT=0.01; GRID=5.00; TOUCH_IN=0.50; OVL=3.00; COST=40.0 SL_PTS,TP_PTS=300.0,1500.0 def load(path, t0=None, t1=None): rows=[] for r in csv.DictReader(open(path)): if t0 and r["datetime"] < t0: continue if t1 and r["datetime"] > t1: continue d = 1 if r["direction"]=="BUY" else -1 o=dict(kind=r["kind"],dt=r["datetime"],year=int(r["year"]),d=d, n=int(r["pa_bars"]) if r["pa_bars"] not in ("","0") else 1, status=int(r["status"]),win=int(r["tp1_before_sl"]),sl=int(r["min_to_sl300"]), fwd20=float(r["fwd20_points"]),fwd6=float(r["fwd6_points"]),atr=float(r["atr14"]), lo=[float(r["lo1"]),float(r["lo2"]),float(r["lo3"])], hi=[float(r["hi1"]),float(r["hi2"]),float(r["hi3"])], bbot=float(r["bbot"]),btop=float(r["btop"]), a8=float(r["anch8"]),a4=float(r["anch4"]), d1b=int(r["d1b"]),h4b=int(r["h4b"]),sw=int(r["swmask"])) o["R"]=(TP_PTS-COST)/SL_PTS if o["win"]==1 else (-(SL_PTS+COST)/SL_PTS if o["sl"]>=0 else (o["fwd20"]-COST)/SL_PTS) o["fwd6atr"]=(o["fwd6"]*PT/o["atr"]) if o["atr"]>0 else 0.0 rows.append(o) S=[r for r in rows if r["kind"]=="SIG" and r["status"]==0] k=set((r["dt"],r["d"]) for r in S) C=[r for r in rows if r["kind"]=="CTRL" and (r["dt"],r["d"]) not in k] return S,C def frame_hit(r,span,lines=None): if r["d"]>0: lo=r["lo"][span-1] cand=[k*GRID for k in range(math.ceil((lo-TOUCH_IN)/GRID-1e-9),math.floor((lo+OVL)/GRID+1e-9)+1)] if lines is None \ else [f for f in lines if lo-TOUCH_IN-1e-9<=f<=lo+OVL+1e-9] return any(fr["btop"]+1e-9 for f in cand) def dayl(r,w="a8"): return None if r[w]<=0 else [r[w]-GRID,r[w],r[w]+GRID] F=[("F1 กรอบ", lambda r: frame_hit(r,1)), ("F2 Day", lambda r: r["d1b"]==r["d"]), ("F3 H4", lambda r: r["h4b"]==r["d"]), ("F4 ไม่SW", lambda r: r["sw"]!=15), ("F5 กรอบวัน", lambda r: dayl(r) is not None and frame_hit(r,1,dayl(r)))] def mean(v): return sum(v)/len(v) if v else float("nan") def varm(v): n=len(v) if n<2: return float("nan") m=mean(v); return sum((x-m)**2 for x in v)/(n-1) def edge(sp,cp,key): """เปรียบ = SIG − CTRL(ถ่วงทิศ) ภายใน subset เดียวกัน — สูตรเดียวกับ L1""" if len(sp)<2: return (float("nan"),)*4 s=[key(r) for r in sp]; ms,vs=mean(s),varm(s)/len(s) w=Counter(r["d"] for r in sp); tot=len(sp); mc=0.0; vc=0.0 for d,c in w.items(): sub=[key(r) for r in cp if r["d"]==d] if len(sub)<2: return (float("nan"),)*4 mc+=(c/tot)*mean(sub); vc+=(c/tot)**2*varm(sub)/len(sub) return ms-mc, math.sqrt(vs+vc), ms, mc kW=lambda r: float(r["win"]); kR=lambda r: r["R"]; kF=lambda r: r["fwd6atr"] OUT=[] def say(s=""): print(s); OUT.append(s) def block(title, files, t0, t1): say("="*108); say(f"### {title}"); say(f" หน้าต่างเวลา: {t0} → {t1} (เหมือนกันทุก offset)"); say("="*108) say(f"{'offset':<9}{'N(SIG)':>7}{'CTRL':>8}{'BUY/SELL':>10}{'SIG%':>8}{'สุ่มถ่วงทิศ%':>13}" f"{'เปรียบpp':>10}{'t':>8}{'เปรียบR':>10}{'tR':>8}{'fwd6ATR':>10}{'t':>7}") res={} for off,path in files: S,C=load(path,t0,t1) mix=Counter('B' if r['d']>0 else 'S' for r in S) ew,sw,msw,mcw = edge(S,C,kW) er,sr,_,_ = edge(S,C,kR) ef,sf,_,_ = edge(S,C,kF) res[off]=dict(N=len(S),C=len(C),rate=msw,ctrl=mcw,e=ew,se=sw,t=ew/sw, eR=er,tR=er/sr,eF=ef,tF=ef/sf,S=S,CT=C) say(f"{'+'+str(off)+' ชม.':<9}{len(S):>7}{len(C):>8}{str(mix['B'])+'/'+str(mix['S']):>10}" f"{100*msw:>8.1f}{100*mcw:>13.1f}{100*ew:>+10.2f}{ew/sw:>+8.2f}" f"{er:>+10.3f}{er/sr:>+8.2f}{ef:>+10.3f}{ef/sf:>+7.2f}") es=[res[o]["e"] for o in res] pos=sum(1 for e in es if e>0) say("") say(f" เปรียบเป็นบวกกี่ offset : {pos}/4 ช่วงของเปรียบ: {100*min(es):+.2f}pp .. {100*max(es):+.2f}pp" f" (กว้าง {100*(max(es)-min(es)):.2f}pp)") say(f" การเข้าไม้สุ่ม (สุ่มถ่วงทิศ) แต่ละ offset: " + " · ".join(f"{100*res[o]['ctrl']:.1f}%" for o in res) + " ← ต้องนิ่ง = ตลาดเดียวกัน เปลี่ยนแค่วิธีแบ่งแท่ง") return res def filters_block(title, files, t0, t1): say(""); say("-"*108); say(f"### ตัวกรองระดับ 1 ข้าม offset — {title} (Δเปรียบ เป็น R เทียบ 'ไม่มีตัวกรอง')"); say("-"*108) say(f"{'ตัวกรอง':<12}" + "".join(f"{'off+'+str(o):>18}" for o,_ in files)) data={} for off,path in files: S,C=load(path,t0,t1); data[off]=(S,C) for name,f in F: line=f"{name:<12}" for off,_ in files: S,C=data[off] e0,_,_,_=edge(S,C,kR) sp=[r for r in S if f(r)]; cp=[r for r in C if f(r)] er,sr,_,_=edge(sp,cp,kR) if er!=er: line+=f"{'N น้อย':>18}" else: line+=f"{er-e0:>+11.3f}R(N{len(sp):<3})" say(line) if __name__=="__main__": import os, random, datetime as dtm random.seed(20260830) MQ=[(o,f"data/maepla_l2_mq_off{o}.csv") for o in range(4)] EX=[(o,f"data/maepla_l2_exness_off{o}.csv") for o in range(4)] allf=[p for _,p in MQ+EX if os.path.exists(p)] mins=[];maxs=[] for p in allf: d=[r["datetime"] for r in csv.DictReader(open(p))]; mins.append(min(d)); maxs.append(max(d)) T0,T1=max(mins),min(maxs) mqmin=max(min(r["datetime"] for r in csv.DictReader(open(p))) for _,p in MQ) mqmax=min(max(r["datetime"] for r in csv.DictReader(open(p))) for _,p in MQ) say("#"*108); say("# L2 — เทสความเปราะต่อจุดเริ่มแท่ง H4 (EA-SPEC §4.3 ข้อ 3)"); say("#"*108); say("") rMF = block("MetaQuotes XAUUSD — เต็มช่วง 7.5 ปี (กำลังทดสอบสูงสุดที่มี)", MQ, mqmin, mqmax) say("") rE = block("EXNESS XAUUSDm", EX, T0, T1) say("") rM = block("MetaQuotes XAUUSD — หน้าต่างเดียวกับ Exness", MQ, T0, T1) # --- offset ต่างกันอย่างมีนัยสำคัญไหม say(""); say("-"*108); say("### สอง offset ต่างกันเกินความบังเอิญไหม (MetaQuotes เต็มช่วง)"); say("-"*108) for a,b in [(0,1),(0,2),(0,3),(1,2)]: d=rMF[b]["e"]-rMF[a]["e"]; sd=math.sqrt(rMF[a]["se"]**2+rMF[b]["se"]**2) say(f" off{a} vs off{b}: Δเปรียบ={100*d:+6.2f}pp SE={100*sd:.2f}pp t={d/sd:+.2f}" + (" ← เกินความบังเอิญ" if abs(d/sd)>1.96 else "")) # --- รวม 4 offset AS=[];AC=[] for o,p in MQ: s,c=load(p,mqmin,mqmax); AS+=s; AC+=c e,se,ms,mc=edge(AS,AC,kW); er,ser,_,_=edge(AS,AC,kR) say(""); say(f"### รวมทั้ง 4 offset (ประมาณเปรียบที่ไม่ขึ้นกับการแบ่งแท่ง): N={len(AS)} " f"SIG={100*ms:.1f}% สุ่ม={100*mc:.1f}% เปรียบ={100*e:+.2f}pp t={e/se:+.2f} · R {er:+.3f} t={er/ser:+.2f}") say(" ⚠️ t เป็นขอบบน — 4 offset ใช้ตลาดช่วงเดียวกัน ไม่อิสระจริง") # --- ความซ้อนทับของชุดสัญญาณ def tt(x): return dtm.datetime.strptime(x,"%Y.%m.%d %H:%M") S={};C={} for o,p in MQ: S[o],C[o]=load(p,mqmin,mqmax) say(""); say("-"*108); say("### สัญญาณของแต่ละ offset ซ้ำกันแค่ไหน (ทิศเดียวกัน + เวลาต่าง <4 ชม.)"); say("-"*108) say(" "+"".join(f"{'off'+str(o):>10}" for o in range(4))) for a in range(4): A=[(tt(r['dt']),r['d']) for r in S[a]] row=f" off{a} " for b in range(4): B=[(tt(r['dt']),r['d']) for r in S[b]] m=sum(1 for t,d in A if any(dd==d and abs((t-u).total_seconds())<14400 for u,dd in B)) row+=f"{100*m/len(A):>9.0f}%" say(row) # --- แท่งริมช่องว่าง say(""); say("-"*108); say("### ตัดแท่งริมช่องว่าง (สุดสัปดาห์/วันหยุด) ออก — เปรียบยังกระจายเหมือนเดิมไหม"); say("-"*108) for o in range(4): ts=sorted(set(tt(r['dt']) for r in S[o]+C[o])) bad=set() for i,t in enumerate(ts): ok = (i>0 and (t-ts[i-1]).total_seconds()==14400) and (i500: say(f" เลื่อน Exness {d:+d} ชม. → median |Δ open| = {statistics.median(v):7.3f} USD (n={len(v)})") say(" ⇒ ตรงกันที่ +3 ชม. (เม.ย.-ต.ค.) และ +2 ชม. (พ.ย.-มี.ค.) = MetaQuotes GMT+2/+3 · Exness GMT+0") # --- กริด UTC def dst(x): return int(x[5:7]) in (4,5,6,7,8,9,10) def utcgrid(u, rng): su,wi=(u+3)%4,(u+2)%4; A=[];B=[] for o,want in ((su,True),(wi,False)): s,c=load(f"data/maepla_l2_mq_off{o}.csv",*rng) A+=[r for r in s if dst(r["dt"])==want]; B+=[r for r in c if dst(r["dt"])==want] return A,B say(""); say("-"*108); say("### 4 กริด UTC บนดาต้า MetaQuotes ชุดเดียว 7.5 ปี (คุม DST)"); say("-"*108) say(f" {'กริด':<10}{'N':>6}{'SIG%':>8}{'สุ่ม%':>8}{'เปรียบpp':>11}{'t':>8}{'เปรียบR':>10}{'tR':>7}") U={} for u in range(4): A,B=utcgrid(u,(mqmin,mqmax)); e,se,ms,mc=edge(A,B,kW); er,ser,_,_=edge(A,B,kR); U[u]=(e,se) say(f" {'UTC-'+str(u):<10}{len(A):>6}{100*ms:>8.1f}{100*mc:>8.1f}{100*e:>+11.2f}{e/se:>+8.2f}{er:>+10.3f}{er/ser:>+7.2f}") pk=U[0][0] say(f" plateau §4.3 ข้อ 3: UTC-3 = {100*U[3][0]/pk:.1f}% ของยอด · UTC-1 = {100*U[1][0]/pk:.1f}% ของยอด → ยอดแหลมโดดเดี่ยว ❌") A,B=utcgrid(0,(mqmin,mqmax)) mid="2022.11.01" for lab,f in [("ครึ่งแรก",lambda r:r['dt']=mid)]: a=[r for r in A if f(r)]; b=[r for r in B if f(r)]; e,se,ms,mc=edge(a,b,kW) say(f" UTC-0 {lab}: N={len(a):<4} SIG={100*ms:5.1f}% สุ่ม={100*mc:5.1f}% เปรียบ={100*e:+6.2f}pp t={e/se:+.2f}") # --- จำลองกริด Exness บนดาต้า MetaQuotes say(""); say("-"*108); say("### เอาดาต้า MetaQuotes มาแบ่งด้วย 'กริดเวลาของ Exness' แล้วเทียบผลจริงบน Exness"); say("-"*108) say(f" {'EX offset':<11}{'จำลองบน MQ':>26}{'ของจริงบน Exness':>28}") for k in range(4): su,wi=(k+3)%4,(k+2)%4; A=[];B=[] for o,want in ((su,True),(wi,False)): s,c=load(f"data/maepla_l2_mq_off{o}.csv",T0,T1) A+=[r for r in s if dst(r["dt"])==want]; B+=[r for r in c if dst(r["dt"])==want] e,se,ms,mc=edge(A,B,kW) p=f"data/maepla_l2_exness_off{k}.csv" if os.path.exists(p): s2,c2=load(p,T0,T1); e2,se2,_,_=edge(s2,c2,kW) real=f"N={len(s2):<4}{100*e2:+7.2f}pp t={e2/se2:+5.2f}" else: real="(ไม่มีไฟล์)" say(f" {'+'+str(k)+' ชม.':<11} N={len(A):<4}{100*e:+7.2f}pp t={e/se:+5.2f} {real:>28}") filters_block("MetaQuotes เต็มช่วง", MQ, mqmin, mqmax) if len(allf)==8: filters_block("Exness", EX, T0, T1) open("data/l2_offsets.txt","w",encoding="utf-8").write("\n".join(OUT)) print("\n-> data/l2_offsets.txt")