buy_doji: graficul TF-ului mic devine lupa pe candelele TF-ului mare

Userul nu putea vedea ce bucata din weekly e reprezentata de daily. In
perechi_tf si directie, graficul mic nu mai e o serie care vine dupa cel
mare, ci descompunerea lui reala: o candela mare = k candele mici, cu
open, close, max(high) si min(low) care coincid, verificat bara cu bara
prin assert. k per pereche: 5 zile/saptamana, 4 saptamani/luna, 7 bare
de 60 min/zi.

Corespondenta se vede: caseta de lupa peste exact candelele marite,
ghidaje catre colturile cadrului mic, si nivelul-cheie desenat ca aceeasi
linie pe ambele panouri, cu acelasi numar de ambele parti. Interdictia
"eticheta unui panou nu poate numi pretul celuilalt" capata o exceptie
explicita pentru nivelurile comune, cu assert propriu ca numarul apare
identic pe ambele panouri.

Pullback-ul si momentul intrarii sunt acum etichetate pe desen, nu doar
in text. Toate cifrele recalculate din serii.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
This commit is contained in:
Claude Agent
2026-09-20 11:42:29 +00:00
parent e9ae7e4695
commit 7beb3d42fe
8 changed files with 1702 additions and 1316 deletions

View File

@@ -1,15 +1,28 @@
#!/usr/bin/env python3
"""Buy Doji — aceeași regulă pe trei perechi de timeframe-uri.
"""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, stânga = TF mare (11 candele,
buy doji + confirmare), dreapta = TF mic (15 candele, pullback de 8 candele
la high-ul dojiului mare, doji acolo, confirmare, intrare).
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 (verificat cu assert).
Rândul A e seria din spec; B și C se CALCULEAZĂ din A prin
p' = round(100 + f*(p - 100), 2), cu f = 2.5 și 0.4. Cifrele din desen ies
din serii, iar assert-urile le compară cu valorile afirmate în spec.
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ă <g transform>), ca audit_diagrams.py să
vadă pozițiile reale ale textelor.
@@ -20,39 +33,59 @@ from pathlib import Path
OUT = Path(__file__).with_suffix(".svg")
W = 720
# --- seria A din spec: (open, high, low, close) ----------------------------
BIG_A = [(122.0, 122.6, 119.4, 120.0), (120.0, 120.5, 117.0, 117.5), (117.5, 119.0, 117.1, 118.4),
(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), # 9: doji
(103.8, 107.4, 103.5, 107.0)] # 10: confirmare
SMALL_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.5, 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)]
# --- seria fină (TF mic), rândul A: (open, high, low, close) ---------------
# 0..7 pullback de 8 candele, în jos, până la 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.5, 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
def scaled(bars, f):
return [tuple(round(100 + f * (p - 100), 2) for p in b) for b in bars]
# key, f, TF mare, TF mic, text sub rând, valori așteptate din spec
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, text sub rând, valori din spec
ROWS = [
("A", 1.0, "weekly", "daily",
"stop {p:.2f} % (pe weekly ar fi {pb:.2f} %) · 8 candele = 8 zile de bursă",
dict(dh=105.20, dl=101.90, conf=107.00, sdl=105.10, entry=106.30, sl=105.10, risk=1.20,
pct=1.13, tp=107.50, b_entry=107.00, b_sl=101.90, b_risk=5.10, b_pct=4.77)),
("B", 2.5, "monthly", "weekly",
"stop {p:.2f} % (pe monthly ar fi {pb:.2f} %) · 8 candele = 8 săptămâni",
dict(dh=113.00, dl=104.75, conf=117.50, sdl=112.75, entry=115.75, sl=112.75, risk=3.00,
pct=2.59, tp=118.75, b_entry=117.50, b_sl=104.75, b_risk=12.75, b_pct=10.85)),
("C", 0.4, "daily", "60 min",
"stop {p:.2f} % (pe daily ar fi {pb:.2f} %) · 8 candele ≈ 1,2 zile de bursă (7 bare/zi)",
dict(dh=102.08, dl=100.76, conf=102.80, sdl=102.04, entry=102.52, sl=102.04, risk=0.48,
pct=0.47, tp=103.00, b_entry=102.80, b_sl=100.76, b_risk=2.04, b_pct=1.98)),
("A", 1.0, "weekly", "daily", 5, 3,
dict(dh=105.20, dl=101.90, conf=107.00, entry=106.30, sl=105.10, risk=1.20,
pct=1.13, tp=107.50, b_entry=107.00, b_sl=101.90, b_risk=5.10, b_pct=4.77,
zhi=108.60, zlo=105.10)),
("B", 2.5, "monthly", "weekly", 4, 3,
dict(dh=113.00, dl=104.75, conf=117.50, entry=115.75, sl=112.75, risk=3.00,
pct=2.59, tp=118.75, b_entry=117.50, b_sl=104.75, b_risk=12.75, b_pct=10.85,
zhi=118.75, zlo=112.75)),
("C", 0.4, "daily", "60 min", 7, 2,
dict(dh=102.08, dl=100.76, conf=102.80, entry=102.52, sl=102.04, risk=0.48,
pct=0.47, tp=103.00, b_entry=102.80, b_sl=100.76, b_risk=2.04, b_pct=1.98,
zhi=103.20, zlo=102.04)),
]
@@ -60,206 +93,292 @@ def eq(a, b):
return abs(a - b) < 0.005
def check(key, big, small, exp):
"""Assert-urile obligatorii din spec, pe seriile calculate; întoarce nivelurile."""
for name, bars in (("mare", big), ("mic", small)):
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(big) == 11 and len(small) == 15
assert eq(small[0][0], big[10][3]), f"{key}: open mic[0] != close mare[10]"
for name, (o, h, l, c) in (("doji mare", big[9]), ("doji mic", small[8])):
assert len(fine) == k * nz, f"{key}: {len(fine)} bare fine != {k}*{nz}"
assert len(big) == len(BASE_A) + nz
# (1) AGREGAREA — fiecare candelă mare mărită e exact suma a k candele mici
for j, (o, h, l, c) in enumerate(zoom):
part = fine[j * k:(j + 1) * k]
assert eq(o, part[0][0]), f"{key}: open mare[{j}] != open primei mici"
assert eq(c, part[-1][3]), f"{key}: close mare[{j}] != close ultimei mici"
assert eq(h, max(b[1] for b in part)), f"{key}: high mare[{j}] != max(high mici)"
assert eq(l, min(b[2] for b in part)), f"{key}: low mare[{j}] != min(low mici)"
# 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"
# (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 big[10][3] > big[9][1] and small[9][3] > small[8][1], f"{key}: fără confirmare"
sl = min(b[2] for b in small[6:10])
assert eq(sl, small[8][2]), f"{key}: SL nu e low-ul dojiului mic"
entry = small[9][3]
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
sl = min(b[2] for b in fine[FINE_DOJI - 2:FINE_CONF + 1])
assert eq(sl, fine[FINE_DOJI][2]), f"{key}: SL nu e low-ul dojiului mic"
assert eq(sl, min(b[2] for b in fine)), f"{key}: low-ul dojiului nu e minimul zonei mărite"
entry = fine[FINE_CONF][3]
risk = round(entry - sl, 2)
tp = round(entry + risk, 2)
first = next(i for i in range(10, 15) if small[i][1] >= tp - 1e-9)
first = next(i for i in range(FINE_CONF + 1, len(fine)) if fine[i][1] >= tp - 1e-9)
assert first == 11, f"{key}: TP1 atins la {first}, nu la 11"
pct = risk / entry * 100
b_entry, b_sl = big[10][3], big[9][2]
# (4) pullback = revenire scurtă fără minim nou sub minimul dojiului mare
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 face minim nou"
b_entry, b_sl = base[CONF_B][3], base[DOJI_B][2]
b_risk = round(b_entry - b_sl, 2)
b_pct = b_risk / b_entry * 100
got = dict(dh=big[9][1], dl=big[9][2], conf=big[10][3], sdl=small[8][2], 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)
for k, v in exp.items():
assert eq(round(got[k], 2), v), f"{key}: {k} = {got[k]:.4f}, spec {v}"
assert abs(small[8][2] - big[9][1]) / big[9][1] <= 0.005, f"{key}: doji mic nu e la nivel"
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,
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(sl - got["dh"]) / got["dh"] <= 0.005, f"{key}: doji mic nu e la nivel"
assert 4 <= b_pct / pct <= 5, f"{key}: raport stopuri {b_pct / pct:.2f} nu e 4-5x"
return got
# --- desen ------------------------------------------------------------------
GREEN, RED, GRAY, INK = "#188a3e", "#c00", "#888", "#333"
PLOT_H = 160 # înălțimea zonei de preț, egală pe toate graficele
BX0, BGAP = 40, 16 # TF mare: x candele
SX0, SGAP = 392, 15 # TF mic
GREEN, RED, GRAY, INK, BLUE = "#188a3e", "#c00", "#888", "#333", "#1a5fb4"
PLOT_H = 160
BX0, BGAP = 105, 15 # TF mare
SX0, SGAP = 372, 15 # TF mic
BODY_W = 9
SL_X1, SL_X2 = 380, 614 # liniile de nivel pe graficul mic
COL_X = 620 # coloana cu etichetele de nivel ale graficului mic
FRAME_L = SX0 - 9 # marginea stângă a cadrului graficului mic
COL_X = 597 # coloana de etichete a graficului mic
MIDX = FRAME_L - 20 # granița stânga/dreapta pentru verificarea de panou
out = []
LABELS = [] # (x, text) al fiecărei etichete, pentru verificarea de panou
LABELS = [] # (x0, x1, text) al fiecărei etichete, pentru verificări
def text(x, y, s, fill=INK, anchor="start", bold=False, size=None):
st = []
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")
if size:
st.append(f"font-size:{size}px")
style = f' style="{";".join(st)}"' if st else ""
a = f' text-anchor="{anchor}"' if anchor != "start" else ""
LABELS.append((x, s))
out.append(f'<text x="{x:.1f}" y="{y:.1f}" fill="{fill}"{a}{style}>{s}</text>')
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'<text x="{x:.1f}" y="{y:.1f}" fill="{fill}"{a} '
f'style="{";".join(st)}">{s}</text>')
def hline(x1, x2, y, color, dash):
def hline(x1, x2, y, color, dash, width=1.5):
out.append(f'<line x1="{x1:.1f}" y1="{y:.1f}" x2="{x2:.1f}" y2="{y:.1f}" '
f'stroke="{color}" stroke-width="1.5" stroke-dasharray="{dash}"/>')
f'stroke="{color}" stroke-width="{width}" stroke-dasharray="{dash}"/>')
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'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
f'stroke="{color}" stroke-width="{width}"{d}{m}/>')
def rect(x0_, y0_, x1, y1, color, fill_op=0.10, dash=""):
d = f' stroke-dasharray="{dash}"' if dash else ""
out.append(f'<rect x="{x0_:.1f}" y="{y0_:.1f}" width="{x1 - x0_:.1f}" '
f'height="{y1 - y0_:.1f}" fill="{color}" fill-opacity="{fill_op}" '
f'stroke="{color}" stroke-width="1.2"{d}/>')
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
out.append(f'<line x1="{cx:.1f}" y1="{yf(h):.1f}" x2="{cx:.1f}" y2="{yf(l):.1f}" '
f'stroke="#1a1a1a" stroke-width="1.2"/>')
top = yf(max(o, c))
hh = max(yf(min(o, c)) - top, 1.6)
out.append(f'<rect x="{cx - BODY_W / 2:.1f}" y="{top:.1f}" width="{BODY_W}" '
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'<rect x="{cx - BODY_W / 2:.1f}" y="{top_:.1f}" width="{BODY_W}" '
f'height="{hh:.1f}" fill="{color}"/>')
out.append('<defs><marker id="pt-arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" '
'markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#555"/></marker>'
'<marker id="pt-red" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" '
'markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#c00"/></marker></defs>')
text(20, 22, "Aceeași regulă pe trei perechi — exemplu construit (fără ticker)", bold=True, size=14)
def zone(x0, y0_, x1, y1, color):
out.append(f'<rect x="{x0:.1f}" y="{y0_:.1f}" width="{x1 - x0:.1f}" height="{y1 - y0_:.1f}" '
f'fill="{color}" fill-opacity="0.10" stroke="{color}" stroke-width="1"/>')
def bracket(x, ya, yb_, color, label):
"""Paranteză verticală între două prețuri, cu eticheta la dreapta."""
out.append(f'<path d="M{x - 5:.1f},{ya:.1f} H{x:.1f} V{yb_:.1f} H{x - 5:.1f}" '
def bracket(x, ya, yb_, color, label, side="right"):
"""Paranteză verticală între două prețuri, cu eticheta în lateral."""
tip = x + 5 if side == "right" else x - 5
out.append(f'<path d="M{tip:.1f},{ya:.1f} H{x:.1f} V{yb_:.1f} H{tip:.1f}" '
f'fill="none" stroke="{color}" stroke-width="1.6"/>')
text(x + 5, (ya + yb_) / 2 + 4, label, fill=color, bold=True)
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, minsep=14):
"""Împinge în jos etichetele coloanei din dreapta ca să nu se suprapună."""
items = sorted(items, key=lambda t: t[0])
y_prev = None
for y, s, fill in items:
if y_prev is not None and y < y_prev + minsep:
y = y_prev + minsep
text(COL_X, y, s, fill=fill, size=10)
y_prev = y
return y_prev
out.append('<defs><marker id="pt-red" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" '
'markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#c00"/></marker>'
'</defs>')
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, cap, exp in ROWS:
big, small = scaled(BIG_A, f), scaled(SMALL_A, f)
g = check(key, big, small, exp)
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)
# titlu rând + subtitluri
text(20, y0 + 14, f"{key}. {tf_big} → {tf_small}", bold=True, size=13)
text(BX0 - 16, y0 + 32, f"{tf_big}: direcția", fill="#555")
text(SL_X1, y0 + 32, f"{tf_small}: intrarea", fill="#555")
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
# scară proprie pe fiecare grafic, constantă în interiorul lui
bmax = max(b[1] for b in big); bmin = min(b[2] for b in big)
smax = max(b[1] for b in small); smin = min(b[2] for b in small)
bmax, bmin = max(b[1] for b in big), min(b[2] for b in big)
smax, smin = max(b[1] for b in fine), min(b[2] for b in fine)
bs, ss = PLOT_H / (bmax - bmin), PLOT_H / (smax - smin)
yb = lambda p, t=top, m=bmax, s=bs: t + (m - p) * s # noqa: E731
ys = lambda p, t=top, m=smax, s=ss: t + (m - p) * s # noqa: E731
# TF mare: zonă peste doji + confirmare, eticheta dedesubt
zx0, zx1 = BX0 + 9 * BGAP - 9, BX0 + 10 * BGAP + 9
zone(zx0, yb(big[10][1]) - 5, zx1, yb(g["dl"]) + 5, GREEN)
text((zx0 + zx1) / 2, yb(g["dl"]) + 20, "doji + confirmare:", fill=GREEN, anchor="middle", bold=True)
text((zx0 + zx1) / 2, yb(g["dl"]) + 34, "aici se decide direcția", fill=GREEN,
anchor="middle", bold=True)
hline(BX0 - 16, BX0 + 10 * BGAP + 12, yb(g["dh"]), GRAY, "3 3")
text(BX0 - 14, yb(g["dh"]) + 14, f"high doji {tf_big} {g['dh']:.2f}", fill="#666")
# ---- TF mare -----------------------------------------------------------
# zona verde peste doji + confirmare (acolo se decide direcția)
zx0, zx1 = BX0 + DOJI_B * BGAP - 9, BX0 + CONF_B * BGAP + 9
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ă peste exact candelele mari care se sparg în dreapta
lx0, lx1 = BX0 + len(base) * BGAP - 8, BX0 + (len(big) - 1) * BGAP + 9
ly0, ly1 = yb(g["zhi"]) - 2, yb(g["zlo"]) + 2
rect(lx0, ly0, lx1, ly1, BLUE, fill_op=0.06)
# nivelul comun: high-ul dojiului mare, aceeași linie punctată pe ambele panouri
hline(BX0 - 15, lx1, yb(g["dh"]), GRAY, "3 3")
text(42, yb(g["dh"]) - 15, f"high doji {tf_big}", fill="#666", size=11)
text(42, yb(g["dh"]) - 2, f"{g['dh']:.2f}", fill="#666", size=11)
candles(big, BX0, BGAP, yb)
# săgeata-zoom: de la close-ul confirmării mari la open-ul primei bare mici (același preț)
x1, y1 = BX0 + 10 * BGAP + 8, yb(g["conf"])
x2, y2 = SX0 - 10, ys(g["conf"])
out.append(f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" stroke="#555" '
f'stroke-width="1.3" stroke-dasharray="5 3" marker-end="url(#pt-arr)"/>')
text(290, y1 + 2, "apoi cobori", fill="#555")
text(290, y1 + 16, "pe graficul mic", fill="#555")
# ---- ghidajele de lupă -------------------------------------------------
for ya, yb_ in ((ly0, top), (ly1, top + PLOT_H)):
line(lx1, ya, FRAME_L, yb_, BLUE, dash="5 3", width=1.1)
mid = (lx1 + FRAME_L) / 2
for i, s_ in enumerate(("lupă:", f"ultimele {nz}", f"candele {tf_big}")):
text(mid, top + 86 + i * 13, s_, fill=BLUE, anchor="middle", size=10)
# TF mic: niveluri
# ---- 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"
# zonă peste doji-ul mic + confirmare, eticheta dedesubt
sx0, sx1 = SX0 + 8 * SGAP - 9, SX0 + 9 * SGAP + 9
zone(sx0, y_e - 6, sx1, y_sl + 5, GREEN)
text((sx0 + sx1) / 2, y_sl + 20, "doji la nivel:", fill=GREEN, anchor="middle", bold=True)
text((sx0 + sx1) / 2, y_sl + 34, "aici intri", fill=GREEN, anchor="middle", bold=True)
hline(SL_X1, SL_X2, y_dh, GRAY, "3 3")
hline(SL_X1, SL_X2, y_e, GREEN, "6 3")
hline(SL_X1, SL_X2, y_sl, RED, "6 3")
hline(SL_X1, SL_X2, y_tp, GREEN, "2 3")
text(COL_X, y_tp + 4, f"ținta 1:1 {g['tp']:.2f}", fill=GREEN, size=11)
text(COL_X, y_e + 4, f"intrare {g['entry']:.2f}", fill=GREEN, size=11)
text(COL_X, y_dh - 5, f"nivel din {tf_big}", fill="#666", size=11)
text(COL_X, y_sl + 12, f"stop {g['sl']:.2f}", fill=RED, size=11)
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, y_sl + 4, GREEN)
line((sx0 + sx1) / 2, top + PLOT_H, (sx0 + sx1) / 2, top + 192, GREEN, width=1)
text((sx0 + sx1) / 2, top + 204, "doji la nivel, apoi confirmare: aici intri", fill=GREEN,
anchor="middle", bold=True, size=11)
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, f"nivel {tf_big} {g['dh']:.2f}", "#666"),
(y_sl + 4, f"stop {g['sl']:.2f}", RED)]
if first:
text(COL_X, y_sl + 25, "sub ultimele", fill=RED, size=11)
text(COL_X, y_sl + 38, "4 candele", fill=RED, size=11)
candles(small, SX0, SGAP, ys)
col.append((y_sl + 17, "sub ultimele 4 bare", RED))
candles(fine, SX0, SGAP, ys)
stack(col)
# pullback: paranteză sub cadru, peste primele 8 bare
pxa, pxb = SX0 - 6, SX0 + (PULLBACK_N - 1) * SGAP + 6
out.append(f'<path d="M{pxa:.1f},{top + PLOT_H + 2:.1f} V{top + PLOT_H + 8:.1f} '
f'H{pxb:.1f} V{top + PLOT_H + 2:.1f}" fill="none" stroke="#555" stroke-width="1"/>')
text((pxa + pxb) / 2, top + PLOT_H + 22, f"pullback: {PULLBACK_N} candele în jos, fără minim nou",
fill="#555", anchor="middle", size=11)
if first:
# paranteză peste pullback-ul de 8 candele (barele 0..7), doar pe primul rând
yb8 = y_tp - 12
xa, xb = SX0 - 5, SX0 + 7 * SGAP + 5
out.append(f'<path d="M{xa:.1f},{yb8 + 6:.1f} V{yb8:.1f} H{xb:.1f} V{yb8 + 6:.1f}" '
f'fill="none" stroke="#555" stroke-width="1"/>')
text((xa + xb) / 2, yb8 - 5, "pullback: 8 candele", fill="#555", anchor="middle")
# parantezele de stop: același setup, măsurat pe TF mare și pe TF mic
bx = zx1 + 8
bracket(bx, yb(g["b_entry"]), yb(g["b_sl"]), RED, f"stop {g['b_pct']:.2f} %".replace(".", ","))
kx = SX0 + 9 * SGAP + 14
# același setup, stopul măsurat pe fiecare TF
bx = 30
bracket(bx, yb(g["b_entry"]), yb(g["b_sl"]), RED,
f"stop {g['b_pct']:.2f} %".replace(".", ","))
kx = SX0 + (FINE_CONF + 1) * SGAP - 3
bracket(kx, y_e, y_sl, RED, f"stop {g['pct']:.2f} %".replace(".", ","))
ratio = g["b_pct"] / g["pct"]
assert 4 <= ratio < 4.5, f"raportul stopurilor {ratio:.2f} nu e „de 4 ori”"
# săgeata dintre ele, sub rând
ya = top + PLOT_H + 58
out.append(f'<line x1="{bx + 20:.1f}" y1="{ya:.1f}" x2="{kx + 20:.1f}" y2="{ya:.1f}" '
f'stroke="{RED}" stroke-width="1.6" marker-end="url(#pt-red)"/>')
text((bx + kx) / 2 + 20, ya + 17, "același setup, stop de 4 ori mai mic", fill=RED,
anchor="middle", bold=True)
text((bx + kx) / 2 + 20, ya + 31, "→ de 4 ori mai multe acțiuni la același risc", fill=RED,
anchor="middle", bold=True)
# fiecare preț stă pe panoul lui: prețurile TF-ului mare nu apar în dreapta
# (SX0) și cele ale TF-ului mic nu apar în stânga. Asta e exact bug-ul care
# a pus „high doji 105.20" (weekly) pe graficul daily.
MARE = {k: g[k] for k in ("dh", "dl", "conf", "b_sl", "b_entry")}
MIC = {k: g[k] for k in ("entry", "sl", "tp", "sdl")}
for x, s_ in LABELS[lab0:]:
pe_mic = x >= SL_X1 - 20
ya = top + PLOT_H + 56
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 (aici: high-ul dojiului mare) sunt desenate pe ambele
# panouri și au voie să apară de ambele părți — dar OBLIGATORIU cu același
# număr. 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"}
MARE = {kk: g[kk] for kk in ("dh", "dl", "conf", "b_sl", "b_entry") if kk not in COMUNE}
MIC = {kk: g[kk] for kk in ("entry", "sl", "tp") if kk not in COMUNE}
for x0_, _x1, _y, s_ in LABELS[lab0:]:
pe_mic = x0_ >= MIDX
rau = MARE if pe_mic else MIC
for k, v in rau.items():
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 {k}={v:.2f} al celuilalt panou")
# și invers: prețul TF-ului mare chiar e scris pe panoul mare, legat de serie
assert any(f"{g['dh']:.2f}" in s_ and x < SL_X1 - 20 for x, s_ in LABELS[lab0:]), \
f"{key}: high-ul dojiului mare nu apare pe panoul mare"
for k in ("entry", "sl", "tp"):
assert any(f"{g[k]:.2f}" in s_ and x >= SL_X1 - 20 for x, s_ in LABELS[lab0:]), \
f"{key}: {k} nu apare pe panoul mic"
print(f"{key}: doji mare {g['dh']:.2f}/{g['dl']:.2f} conf {g['conf']:.2f} | intrare {g['entry']:.2f} "
f"SL {g['sl']:.2f} risc {g['risk']:.2f} ({g['pct']:.2f} %) TP1 {g['tp']:.2f} | "
f"doar mare: risc {g['b_risk']:.2f} ({g['b_pct']:.2f} %)")
y0 += 320 if first else 270
f"{'mic' if pe_mic else 'mare'} conține prețul {kk}={v:.2f} al celuilalt panou")
# nivelul comun apare pe AMBELE panouri, cu exact același număr
for kk in COMUNE:
v = f"{g[kk]:.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 {kk}={v} lipsește de pe un panou ({st_} / {dr_})"
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} | doar mare: risc {g['b_risk']:.2f} ({g['b_pct']:.2f} %) "
f"= x{g['b_pct'] / g['pct']:.2f}")
y0 = top + (300 if first else 238)
if key != "C":
out.append(f'<line x1="20" y1="{y0 - 14:.0f}" x2="{W - 20}" y2="{y0 - 14:.0f}" stroke="#ddd"/>')
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'<svg viewBox="0 0 {W} {H:.0f}" xmlns="http://www.w3.org/2000/svg" '
f'style="width:100%;height:auto;font:12px sans-serif">\n ' + "\n ".join(out) + "\n</svg>")
assert "\n\n" not in svg