Files
atm-curs-dl/gen/strategii/buy_doji_perechi_tf.py
Claude Agent 135a9f0daa perechi_tf: referinta pullback-ului, marcata pe panoul mare
Eticheta spunea "fara sa rupa low-ul doji-ului weekly", dar nivelul nu
se vedea nicaieri — scara panoului mic nu ajunge pana la el. Extinderea
scarii ar fi facut ghidajele de lupa false si ar fi taiat magnificarea
la jumatate, asa ca referinta se marcheaza pe panoul MARE, cu o bifa
pana la low-ul doji-ului, si eticheta trimite acolo. Nivelul traieste pe
TF-ul mare, declansatorul pe cel mic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-20 12:17:14 +00:00

485 lines
25 KiB
Python

#!/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ă <g transform>), 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'<text x="{x:.1f}" y="{y:.1f}" fill="{fill}"{a} '
f'style="{";".join(st)}">{s}</text>')
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="{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
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}"/>')
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'<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"/>')
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'<path d="M{x_line + 2:.1f},{y - 4:.1f} H{x_line + 7:.1f} '
f'L{COL_X - 4:.1f},{y_lab - 4:.1f}" fill="none" stroke="{fill}" '
f'stroke-width="0.8" stroke-opacity="0.8"/>')
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('<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, 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'<path d="M{lx1:.1f},{ycb:.1f} H{mid:.1f} V{ycs:.1f} H{FRAME_L:.1f}" '
f'fill="none" stroke="{LINK}" stroke-width="1.0" stroke-dasharray="3 3"/>')
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'<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 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'<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
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})")