#!/usr/bin/env python3 """Buy Doji — aceeași regulă pe trei perechi de timeframe-uri, cu lupă. Citește `gen/strategii/buy_doji_perechi_tf.spec.txt` și scrie `gen/strategii/buy_doji_perechi_tf.svg`. Trei rânduri (weekly→daily, monthly→weekly, daily→60 min). Pe fiecare rând: stânga = TF mare, dreapta = TF mic. Graficul mic NU e o serie care vine după cel mare — e **descompunerea reală** a ultimelor candele mari: fiecare candelă mare din caseta de lupă se sparge în `k` candele mici, cu open/close/high/low agregate exact. Sursa de adevăr e seria FINĂ (`FINE`). Candelele mari se CALCULEAZĂ din ea prin agregare pe k bare; barele mari dinaintea lupei (`BASE`) sunt date separat și se leagă de lupă prin close == open. rând TF mare → TF mic k candele mari mărite bare mici A weekly → daily 5 3 15 B monthly → weekly 4 3 12 C daily → 60 min 7 2 14 Rândurile B și C se obțin din A prin p' = round(100 + f*(p - 100), 2), cu f = 2.5 și 0.4. Transformarea e afină, deci agregarea se păstrează; gruparea (k) diferă însă per rând, deci barele mari se recalculează per rând. Toate cifrele din desen ies din serii; niciuna nu e scrisă de mână. Coordonatele sunt absolute (fără ), ca audit_diagrams.py să vadă pozițiile reale ale textelor. Rulare: python3 gen/strategii/buy_doji_perechi_tf.py """ from pathlib import Path OUT = Path(__file__).with_suffix(".svg") W = 720 MARJA = 0.05 # 5 cenți sub cel mai jos low (regula capitolului, Pasul 5) # --- seria fină (TF mic), rândul A: (open, high, low, close) --------------- # 0..7 pullback de 8 candele, în jos, spre nivelul din TF-ul mare # 8 doji la nivel 9 confirmare => intrare 10..14 drumul spre țintă FINE_A = [(107.0, 107.2, 106.4, 106.6), (106.6, 106.8, 106.0, 106.1), (106.1, 106.6, 105.95, 106.4), (106.4, 106.5, 105.8, 105.9), (105.9, 106.0, 105.5, 105.6), (105.6, 105.95, 105.5, 105.8), (105.8, 105.9, 105.4, 105.5), (105.5, 105.6, 105.25, 105.35), (105.35, 105.75, 105.10, 105.40), # 8: doji la nivel (105.40, 106.40, 105.30, 106.30), # 9: confirmare => intrare (106.3, 107.0, 106.2, 106.9), (106.9, 107.7, 106.7, 107.4), (107.4, 107.55, 107.0, 107.2), (107.2, 108.0, 107.1, 107.9), (107.9, 108.6, 107.8, 108.5)] # --- barele TF-ului mare dinaintea casetei de lupă, rândul A --------------- # 6 = doji mare, 7 = confirmare mare (close 107.0 = open-ul primei bare fine) BASE_A = [(118.4, 118.8, 114.3, 114.8), (114.8, 115.3, 111.5, 112.0), (112.0, 113.4, 111.6, 112.9), (112.9, 113.2, 109.1, 109.6), (109.6, 110.0, 106.3, 106.8), (106.8, 107.2, 103.6, 104.0), (104.0, 105.2, 101.9, 103.8), # 6: doji mare (103.8, 107.4, 103.5, 107.0)] # 7: confirmare mare DOJI_B, CONF_B = 6, 7 PULLBACK_N = 8 # câte bare mici are pullback-ul (0..7) FINE_DOJI, FINE_CONF = 8, 9 STOP_BARS = 4 # stopul se măsoară sub ultimele 4 bare (doji + 3 dinainte) def scaled(bars, f): return [tuple(round(100 + f * (p - 100), 2) for p in b) for b in bars] def aggregate(fine, k, n): """n candele mari, fiecare din k bare fine consecutive.""" out_ = [] for j in range(n): part = fine[j * k:(j + 1) * k] out_.append((part[0][0], max(b[1] for b in part), min(b[2] for b in part), part[-1][3])) return out_ # key, f, TF mare, TF mic, k, nr. candele mari mărite, valori din spec ROWS = [ ("A", 1.0, "weekly", "daily", 5, 3, dict(dh=105.20, dl=101.90, conf=107.00, entry=106.30, sl=105.05, risk=1.25, pct=1.18, tp=107.55, b_entry=107.00, b_sl=101.85, b_risk=5.15, b_pct=4.81, zhi=108.60, zlo=105.10, first=11, span=2, doji_big=1)), ("B", 2.5, "monthly", "weekly", 4, 3, dict(dh=113.00, dl=104.75, conf=117.50, entry=115.75, sl=112.70, risk=3.05, pct=2.63, tp=118.80, b_entry=117.50, b_sl=104.70, b_risk=12.80, b_pct=10.89, zhi=119.25, zlo=112.75, first=11, span=2, doji_big=2)), ("C", 0.4, "daily", "60 min", 7, 2, dict(dh=102.08, dl=100.76, conf=102.80, entry=102.52, sl=101.99, risk=0.53, pct=0.52, tp=103.05, b_entry=102.80, b_sl=100.71, b_risk=2.09, b_pct=2.03, zhi=103.20, zlo=102.04, first=11, span=2, doji_big=1)), ] def eq(a, b): return abs(a - b) < 0.005 def check(key, base, zoom, fine, k, nz, exp): """Assert-urile obligatorii din spec; întoarce nivelurile folosite în desen.""" big = base + zoom for name, bars in (("mare", big), ("mic", fine)): for i, (o, h, l, c) in enumerate(bars): assert h >= max(o, c) and l <= min(o, c), f"{key} {name}[{i}] h/l incoerent" if i: assert eq(o, bars[i - 1][3]), f"{key} {name}[{i}] open != close precedent" assert len(fine) == k * nz, f"{key}: {len(fine)} bare fine != {k}*{nz}" assert len(big) == len(BASE_A) + nz # (1) UNDE CAD GRANIȚELE candelelor mari peste poveste. Nu reface calculul # agregării (ar fi tautologic: zoom vine chiar din aggregate) — afirmă ce # spune textul capitolului și poate fi fals: peste câte candele mari se # întinde pullback-ul și în a câta candelă mare cade doji-ul mic. span = len({i // k for i in range(PULLBACK_N)}) assert span == exp["span"], f"{key}: pullback-ul se întinde peste {span} candele mari" assert span >= 2, f"{key}: pullback-ul ar încăpea într-o singură candelă mare" doji_big = FINE_DOJI // k assert doji_big == exp["doji_big"], f"{key}: doji-ul mic cade în candela mare {doji_big}" assert doji_big < nz, f"{key}: doji-ul mic cade în afara zonei mărite" # lupa se leagă de restul graficului mare: confirmarea mare dă open-ul lupei assert eq(fine[0][0], base[CONF_B][3]), f"{key}: open fine[0] != close confirmare mare" assert eq(zoom[doji_big][2], fine[FINE_DOJI][2]), \ f"{key}: low-ul candelei mari cu doji-ul nu e low-ul doji-ului mic" # (2) setup-ul pe TF mare: doji + confirmare for name, (o, h, l, c) in (("doji mare", base[DOJI_B]), ("doji mic", fine[FINE_DOJI])): assert abs(o - c) <= 0.10 * (h - l) + 1e-9, f"{key}: {name} nu e doji" assert base[CONF_B][3] > base[DOJI_B][1], f"{key}: fără confirmare pe TF mare" assert fine[FINE_CONF][3] > fine[FINE_DOJI][1], f"{key}: fără confirmare pe TF mic" # (3) trade-ul pe TF mic: stopul e cu MARJA sub minimul ultimelor 4 bare low4 = min(b[2] for b in fine[FINE_CONF - STOP_BARS + 1:FINE_CONF + 1]) assert eq(low4, fine[FINE_DOJI][2]), f"{key}: minimul celor 4 bare nu e low-ul dojiului" assert eq(low4, min(b[2] for b in fine)), f"{key}: low-ul dojiului nu e minimul zonei mărite" sl = round(low4 - MARJA, 2) entry = fine[FINE_CONF][3] risk = round(entry - sl, 2) tp = round(entry + risk, 2) first = next(i for i in range(FINE_CONF + 1, len(fine)) if fine[i][1] >= tp - 1e-9) assert first == exp["first"], f"{key}: TP1 atins la {first}, nu la {exp['first']}" pct = risk / entry * 100 # (4) pullback = revenire scurtă care coboară spre nivel, dar NU rupe # low-ul doji-ului de pe TF-ul mare (referința mișcării) pull = fine[:PULLBACK_N] assert pull[0][3] > pull[-1][3], f"{key}: pullback-ul nu coboară" assert min(b[2] for b in pull) > base[DOJI_B][2], f"{key}: pullback-ul rupe low-ul doji mare" b_entry = base[CONF_B][3] b_sl = round(base[DOJI_B][2] - MARJA, 2) b_risk = round(b_entry - b_sl, 2) b_pct = b_risk / b_entry * 100 got = dict(dh=base[DOJI_B][1], dl=base[DOJI_B][2], conf=base[CONF_B][3], entry=entry, sl=sl, risk=risk, pct=pct, tp=tp, b_entry=b_entry, b_sl=b_sl, b_risk=b_risk, b_pct=b_pct, first=first, span=span, doji_big=doji_big, zhi=max(b[1] for b in fine), zlo=min(b[2] for b in fine)) for kk, v in exp.items(): assert eq(round(got[kk], 2), v), f"{key}: {kk} = {got[kk]:.4f}, spec {v}" # (5) zona mărită de pe graficul mare are EXACT extremele graficului mic assert eq(got["zhi"], max(b[1] for b in zoom)) and eq(got["zlo"], min(b[2] for b in zoom)) # (6) doji-ul mic chiar atinge nivelul din TF-ul mare (sub pragul strict de 0,5 %) assert abs(got["zlo"] - got["dh"]) / got["dh"] <= 0.005, f"{key}: doji mic nu e la nivel" assert 3.9 <= b_pct / pct <= 4.5, f"{key}: raport stopuri {b_pct / pct:.2f} nu e ~4x" # (7) nivelul legat între panouri (close-ul confirmării mari = open fine[0]) # cade în mijlocul zonei mărite, nu lipit de o extremă t = (got["conf"] - sl) / (got["zhi"] - sl) assert 0.2 <= t <= 0.8, f"{key}: nivelul comun e la {t:.2f} din interval, prea la margine" return got # --- desen ------------------------------------------------------------------ GREEN, RED, GRAY, INK, BLUE = "#188a3e", "#c00", "#888", "#333", "#1a5fb4" LINK = "#aaa" # nivelul comun legat între panouri PLOT_H = 160 BX0, BGAP = 105, 15 # TF mare SX0, SGAP = 372, 15 # TF mic BODY_W = 9 FRAME_L = SX0 - 9 # marginea stângă a cadrului graficului mic COL_X = 604 # coloana de etichete a graficului mic MIDX = FRAME_L - 20 # granița stânga/dreapta pentru verificarea de panou PAD = 0.06 # aer sus/jos pe graficul mic (nu lipi liniile de cadru) out = [] LABELS = [] # (x0, x1, y, text) al fiecărei etichete, pentru verificări def text(x, y, s, fill=INK, anchor="start", bold=False, size=12): st = [f"font-size:{size}px"] if bold: st.append("font-weight:bold") a = f' text-anchor="{anchor}"' if anchor != "start" else "" w = len(s) * 5.7 if size >= 12 else len(s) * size * 0.5 x0 = x if anchor == "start" else (x - w / 2 if anchor == "middle" else x - w) assert 4 <= x0 and x0 + w <= W - 4, f"text iese din viewBox: {s!r} ({x0:.0f}..{x0 + w:.0f})" LABELS.append((x0, x0 + w, y, s)) out.append(f'{s}') def hline(x1, x2, y, color, dash, width=1.5): out.append(f'') def line(x1, y1, x2, y2, color, dash="", width=1.2, marker=""): d = f' stroke-dasharray="{dash}"' if dash else "" m = f' marker-end="url(#{marker})"' if marker else "" out.append(f'') def rect(x0_, y0_, x1, y1, color, fill_op=0.10, dash=""): d = f' stroke-dasharray="{dash}"' if dash else "" out.append(f'') def candles(bars, x0, gap, yf): for i, (o, h, l, c) in enumerate(bars): cx = x0 + i * gap color = GREEN if c >= o else RED line(cx, yf(h), cx, yf(l), "#1a1a1a", width=1.2) top_ = yf(max(o, c)) hh = max(yf(min(o, c)) - top_, 1.6) out.append(f'') def bracket(x, ya, yb_, color, label, side="right", below=False): """Paranteză verticală între două prețuri, cu eticheta în lateral sau dedesubt.""" tip = x + 5 if side == "right" else x - 5 out.append(f'') if below: text((x + tip) / 2, max(ya, yb_) + 14, label, fill=color, anchor="middle", bold=True, size=11) else: text(tip + (4 if side == "right" else -4), (ya + yb_) / 2 + 4, label, fill=color, anchor="start" if side == "right" else "end", bold=True, size=11) def stack(items, x_line, minsep=15, lead=13): """Coloana din dreapta: împinge etichetele ca să nu se suprapună și trage o liniuță de legătură de la linia de preț la eticheta ei, ca perechea etichetă-linie să rămână neambiguă chiar dacă eticheta a fost împinsă.""" y_prev = None for y, lines, fill in sorted(items, key=lambda t: t[0]): y_lab = y if y_prev is None or y >= y_prev + minsep else y_prev + minsep if y_lab - y > 2: out.append(f'') else: line(x_line + 2, y - 4, COL_X - 4, y_lab - 4, fill, width=0.8) for j, s in enumerate(lines): text(COL_X, y_lab + j * lead, s, fill=fill, size=10) y_prev = y_lab + (len(lines) - 1) * lead return y_prev def seg_dist(p, a, b): """Distanța de la punctul p la segmentul ab.""" (px, py), (ax, ay), (bx, by) = p, a, b dx, dy = bx - ax, by - ay t = 0.0 if dx == dy == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy))) return ((px - ax - t * dx) ** 2 + (py - ay - t * dy) ** 2) ** 0.5 out.append('' '') text(20, 22, "Aceeași regulă pe trei perechi — graficul mic e lupa pe ultimele candele mari", bold=True, size=13) y0 = 40 for key, f, tf_big, tf_small, k, nz, exp in ROWS: fine = scaled(FINE_A, f)[:k * nz] base = scaled(BASE_A, f) zoom = aggregate(fine, k, nz) big = base + zoom g = check(key, base, zoom, fine, k, nz, exp) first = key == "A" lab0 = len(LABELS) text(20, y0 + 14, f"{key}. {tf_big} → {tf_small}", bold=True, size=13) text(20, y0 + 32, f"{tf_big}: direcția", fill="#555", size=11) text(FRAME_L, y0 + 32, f"{tf_small}: descompunerea ultimelor {nz} candele {tf_big}", fill="#555", size=11) top = y0 + 50 # Intervalul de preț al lupei: de la stop (sub minim) la maximul zonei, cu # aer sus/jos. ACELAȘI interval e încadrat pe ambele panouri — asta fac # ghidajele; altfel linia de stop ar cădea exact pe marginea cadrului. zlo_, zhi_ = g["sl"], g["zhi"] pad = PAD * (zhi_ - zlo_) lo, hi = zlo_ - pad, zhi_ + pad # scară proprie pe fiecare grafic, constantă în interiorul lui bmax = max(b[1] for b in big) bmin = min(min(b[2] for b in big), g["b_sl"], lo) bs, ss = PLOT_H / (bmax - bmin), PLOT_H / (hi - lo) yb = lambda p, t=top, m=bmax, s=bs: t + (m - p) * s # noqa: E731 ys = lambda p, t=top, m=hi, s=ss: t + (m - p) * s # noqa: E731 # ---- TF mare ----------------------------------------------------------- # zona verde peste doji + confirmare (acolo se decide direcția) zx0, zx1 = BX0 + DOJI_B * BGAP - 9, BX0 + CONF_B * BGAP + 6.5 rect(zx0, yb(base[CONF_B][1]) - 5, zx1, yb(g["dl"]) + 6, GREEN) text((zx0 + zx1) / 2, top + 178, "doji + confirmare:", fill=GREEN, anchor="middle", bold=True, size=11) text((zx0 + zx1) / 2, top + 192, "aici se decide direcția", fill=GREEN, anchor="middle", bold=True, size=11) # caseta de lupă (punctată, ca în buy_doji_directie) peste exact candelele # mari care se sparg în dreapta, pe intervalul de preț al cadrului mic lx0, lx1 = BX0 + len(base) * BGAP - 6, BX0 + (len(big) - 1) * BGAP + 9 assert lx0 > zx1 + 1, f"{key}: caseta de lupă intră peste caseta verde" ly0, ly1 = yb(hi), yb(lo) rect(lx0, ly0, lx1, ly1, BLUE, fill_op=0.06, dash="4 3") # nivelul-reper de pe TF-ul mare: high-ul dojiului, retestat jos hline(BX0 - 15, lx1, yb(g["dh"]), GRAY, "3 3") text(42, yb(g["dh"]) + 11, f"high doji {tf_big}", fill="#666", size=11) text(42, yb(g["dh"]) + 24, f"{g['dh']:.2f}", fill="#666", size=11) # nivelul comun legat de panoul mic: close-ul confirmării mari = open-ul # primei bare mici (prin construcție), deci e exact același preț pe ambele hline(BX0 - 15, lx1, yb(g["conf"]), LINK, "3 3", width=1.0) # BIFA: minimul pe care pullback-ul de pe panoul mic nu are voie să-l rupă. # Nu se poate desena pe panoul mic (e cu mult sub scara lui, iar coborând # scara ghidajele ar deveni false), deci trăiește aici, pe TF-ul mare, # sub paranteza de stop — nu în interiorul ei. line(BX0 - 19, yb(g["dl"]), BX0 + DOJI_B * BGAP + BODY_W / 2, yb(g["dl"]), "#555", dash="2 3", width=1.2) candles(big, BX0, BGAP, yb) # ---- ghidajele de lupă ------------------------------------------------- guides = [((lx1, ly0), (FRAME_L, top)), ((lx1, ly1), (FRAME_L, top + PLOT_H))] for a_, b_ in guides: line(a_[0], a_[1], b_[0], b_[1], BLUE, dash="5 3", width=1.1) # ---- nivelul comun: conector subțire între cele două panouri ----------- # Scările au pante diferite (e o lupă), deci același preț cade la înălțimi # diferite. Conectorul le leagă; numărul e scris o singură dată, în cot. ycb, ycs = yb(g["conf"]), ys(g["conf"]) def clearance(mx): conn = [((lx1, ycb), (mx, ycb)), ((mx, ycb), (mx, ycs)), ((mx, ycs), (FRAME_L, ycs))] pts = [(sx + (ex - sx) * t / 24, sy + (ey - sy) * t / 24) for (sx, sy), (ex, ey) in conn for t in range(25)] return min(seg_dist(q, ga, gb) for q in pts if q[0] >= lx1 + 10 for ga, gb in guides) # cotul se alege unde conectorul stă cel mai departe de ghidajele albastre: # pantele diferă per rând, deci mijlocul nu e mereu bun. mid = max(range(int(lx1) + 14, int(FRAME_L) - 13), key=clearance) dmin = clearance(mid) assert dmin > 10, f"{key}: conectorul trece la {dmin:.1f} px de un ghidaj" out.append(f'') tx = min(mid + 5, FRAME_L - 62) text(tx, (ycb + ycs) / 2 + 4, f"{g['conf']:.2f}", fill="#777", size=10) text(tx, (ycb + ycs) / 2 + 18, "același preț", fill="#999", size=10) print(f" conector: cot la x={mid}, la {dmin:.1f} px de ghidaje") # ---- TF mic ------------------------------------------------------------ y_dh, y_e, y_sl, y_tp = ys(g["dh"]), ys(g["entry"]), ys(g["sl"]), ys(g["tp"]) assert abs((y_sl - y_e) / (y_e - y_tp) - 1) < 1e-6, f"{key}: SL-intrare != intrare-TP1 în px" assert top + PLOT_H - y_sl > 6, f"{key}: linia de stop e lipită de marginea cadrului" assert y_sl > ys(g["zlo"]), f"{key}: stopul nu e sub minimul barelor" frame_r = SX0 + (len(fine) - 1) * SGAP + 9 rect(FRAME_L, top, frame_r, top + PLOT_H, BLUE, fill_op=0.03) # zona verde peste doji-ul mic + confirmare sx0, sx1 = SX0 + FINE_DOJI * SGAP - 9, SX0 + FINE_CONF * SGAP + 9 rect(sx0, y_e - 6, sx1, ys(g["zlo"]) + 4, GREEN) line((sx0 + sx1) / 2, top + PLOT_H, (sx0 + sx1) / 2, top + PLOT_H + 40, GREEN, width=1) text((sx0 + sx1) / 2, top + PLOT_H + 52, "doji la nivel + confirmare: aici intri", fill=GREEN, anchor="middle", bold=True, size=11) hline(FRAME_L, frame_r, ycs, LINK, "3 3", width=1.0) hline(FRAME_L, frame_r, y_dh, GRAY, "3 3") hline(FRAME_L, frame_r, y_e, GREEN, "6 3") hline(FRAME_L, frame_r, y_sl, RED, "6 3") hline(FRAME_L, frame_r, y_tp, GREEN, "2 3") col = [(y_tp + 4, [f"ținta 1:1 {g['tp']:.2f}"], GREEN), (y_e + 4, [f"intrare {g['entry']:.2f}"], GREEN), (y_dh + 4, ["retest: high doji", f"{tf_big} {g['dh']:.2f}"], "#666"), (y_sl + 4, [f"stop {g['sl']:.2f}"] + (["5 cenți sub minimul", f"ultimelor {STOP_BARS} bare"] if first else []), RED)] candles(fine, SX0, SGAP, ys) stack(col, frame_r) # pullback: paranteză sub cadru, peste primele 8 bare pxa, pxb = SX0 - 6, SX0 + (PULLBACK_N - 1) * SGAP + 6 out.append(f'') text((pxa + pxb) / 2, top + PLOT_H + 22, f"pullback: {PULLBACK_N} candele care coboară spre nivel", fill="#555", anchor="middle", size=11) text((pxa + pxb) / 2, top + PLOT_H + 35, f"fără să rupă low-ul doji-ului {tf_big} (bifa din stânga)", fill="#555", anchor="middle", size=11) text(20, top + PLOT_H + 52, f"bifa de sub doji = {g['dl']:.2f}, minimul care nu se rupe", fill="#555", size=11) if key == "C": text(20, top + PLOT_H + 72, "(șapte bare de 60 min pe zi: ședința 9:30-16:00 are 6 ore și jumătate" " — ultima bară a zilei e de 30 min)", fill="#777", size=10) if first: # același setup, stopul măsurat pe fiecare TF bx = 34 bracket(bx, yb(g["b_entry"]), yb(g["b_sl"]), RED, f"stop {g['b_pct']:.2f} %".replace(".", ","), below=True) # paranteza stă în golul dintre două candele, ca să nu atingă niciun fitil kx = SX0 + (FINE_CONF + 1) * SGAP + BODY_W / 2 + 0.3 assert min(abs(kx + d - (SX0 + i * SGAP)) for i in range(len(fine)) for d in (0, 5)) \ > BODY_W / 2, "paranteza de stop atinge o candelă" bracket(kx, y_e, y_sl, RED, f"stop {g['pct']:.2f} %".replace(".", ",")) ratio = g["b_pct"] / g["pct"] assert 3.9 <= ratio < 4.5, f"raportul stopurilor {ratio:.2f} nu e „de 4 ori”" ya = top + PLOT_H + 76 line(bx, ya, kx, ya, RED, width=1.6, marker="pt-red") text((bx + kx) / 2, ya + 17, f"același setup, stop de {ratio:.1f} ori mai mic".replace(".", ","), fill=RED, anchor="middle", bold=True, size=11) text((bx + kx) / 2, ya + 31, f"→ de {ratio:.1f} ori mai multe acțiuni la același risc".replace(".", ","), fill=RED, anchor="middle", bold=True, size=11) # --- fiecare preț stă pe panoul lui, cu excepția NIVELURILOR COMUNE ----- # Nivelurile comune (high-ul dojiului mare, retestat jos; close-ul # confirmării mari = open-ul primei bare mici) au voie de ambele părți. # Restul prețurilor rămân fiecare pe panoul lui: un preț al TF-ului mare # pe panoul mic se citește greșit, ca trigger al panoului mic. COMUNE = {"dh", "conf"} cval = {round(g[kk], 2) for kk in COMUNE} MARE = {kk: g[kk] for kk in ("dh", "dl", "conf", "b_sl", "b_entry") if round(g[kk], 2) not in cval} MIC = {kk: g[kk] for kk in ("entry", "sl", "tp") if round(g[kk], 2) not in cval} for x0_, _x1, _y, s_ in LABELS[lab0:]: pe_mic = x0_ >= MIDX rau = MARE if pe_mic else MIC for kk, v in rau.items(): assert f"{v:.2f}" not in s_, ( f"{key}: eticheta {s_!r} de pe panoul " f"{'mic' if pe_mic else 'mare'} conține prețul {kk}={v:.2f} al celuilalt panou") # dh apare pe AMBELE panouri, cu exact același număr v = f"{g['dh']:.2f}" st_ = [s_ for x0_, _x1, _y, s_ in LABELS[lab0:] if v in s_ and x0_ < MIDX] dr_ = [s_ for x0_, _x1, _y, s_ in LABELS[lab0:] if v in s_ and x0_ >= MIDX] assert st_ and dr_, f"{key}: nivelul comun dh={v} lipsește de pe un panou ({st_} / {dr_})" # conf apare o SINGURĂ dată, în cotul conectorului dintre panouri v = f"{g['conf']:.2f}" cx_ = [x0_ for x0_, _x1, _y, s_ in LABELS[lab0:] if v in s_] assert len(cx_) == 1 and lx1 < cx_[0] < FRAME_L, \ f"{key}: {v} nu e scris exact o dată, în cotul conectorului ({cx_})" for kk in ("entry", "sl", "tp"): assert any(f"{g[kk]:.2f}" in s_ and x0_ >= MIDX for x0_, _x1, _y, s_ in LABELS[lab0:]), \ f"{key}: {kk} nu apare pe panoul mic" print(f"{key}: {tf_big}→{tf_small} k={k} ({nz} candele mari = {len(fine)} mici) | " f"doji mare {g['dh']:.2f}/{g['dl']:.2f} conf {g['conf']:.2f} | lupă {g['zlo']:.2f}-{g['zhi']:.2f} | " f"intrare {g['entry']:.2f} SL {g['sl']:.2f} risc {g['risk']:.2f} ({g['pct']:.2f} %) " f"TP1 {g['tp']:.2f} la bara {g['first']} | doar mare: SL {g['b_sl']:.2f} risc " f"{g['b_risk']:.2f} ({g['b_pct']:.2f} %) = x{g['b_pct'] / g['pct']:.2f} | " f"pullback peste {g['span']} candele mari, doji în candela mare {g['doji_big']}") y0 = top + (296 if first else (274 if key == "C" else 254)) if key != "C": out.append(f'') H = y0 - 20 # nicio pereche de etichete nu se suprapune for a in range(len(LABELS)): for b in range(a + 1, len(LABELS)): xa0, xa1, ya_, sa = LABELS[a] xb0, xb1, yb_, sb = LABELS[b] assert not (abs(ya_ - yb_) < 13 and xa0 < xb1 - 4 and xb0 < xa1 - 4), \ f"etichete suprapuse: {sa!r} / {sb!r}" assert 0 < LABELS[a][2] < H, f"etichetă în afara înălțimii: {LABELS[a][3]!r}" svg = (f'\n ' + "\n ".join(out) + "\n") assert "\n\n" not in svg assert ".md:" not in svg and "Surse" not in svg, "trimiteri la surse în SVG" OUT.write_text(svg, encoding="utf-8") print(f"scris {OUT} ({W}x{H:.0f})")