buy_doji: reparatiile din verificare (21 de puncte)

Verificarea cu agent separat a gasit doua contradictii de continut pe
care auditul mecanic nu le prinde:

- textul spunea ca pullback-ul "incape intr-o singura candela a TF-ului
  mare"; pe desen se intinde peste doua, iar prima e o saptamana rosie
  vizibila. Rescris: pe TF-ul mare vezi candelele, dar nu vezi ce e in
  ele — unde s-a oprit coborarea si doji-ul care o termina, deci n-ai
  unde intra si n-ai de unde masura stopul. Asta face lupa.
- definitia pullback-ului zicea "nu mai face minim nou", absolut, dar
  desenul arata minime tot mai joase — asa face orice pullback care
  coboara spre un nivel. Ancorata de minimul de plecare; eticheta de pe
  desen numeste referinta si trimite la panoul unde e marcata.

Restul: perechi_tf punea stopul fix pe low, fara marja de 5 centi pe
care o cere Pasul 5 si o respecta directie — aliniat, cu cifrele
recalculate; "nivel weekly" devine "retest", pentru ca high-ul unui doji
nu e suport si Pasul 3 cere altceva; conector intre nivelul comun al
celor doua panouri (nu se pot alinia — sunt scari diferite, e o lupa);
leader lines in coloana de etichete; assert-ul de agregare, care era
tautologic, verifica acum granitele candelelor mari fata de pullback si
doji — exact locul unde textul se rupsese.

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 12:14:33 +00:00
parent 6c806c4ebe
commit 383a900eaa
8 changed files with 1229 additions and 955 deletions

View File

@@ -7,7 +7,7 @@ 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).
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
@@ -32,9 +32,10 @@ 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, până la nivelul din TF-ul mare
# 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),
@@ -42,7 +43,7 @@ FINE_A = [(107.0, 107.2, 106.4, 106.6), (106.6, 106.8, 106.0, 106.1),
(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),
(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)]
@@ -56,6 +57,7 @@ BASE_A = [(118.4, 118.8, 114.3, 114.8), (114.8, 115.3, 111.5, 112.0),
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):
@@ -72,20 +74,20 @@ def aggregate(fine, k, n):
return out_
# key, f, TF mare, TF mic, k, nr. candele mari mărite, text sub rând, valori din spec
# 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.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)),
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.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)),
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=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)),
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)),
]
@@ -104,15 +106,20 @@ def check(key, base, zoom, fine, k, nz, exp):
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)"
# (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])):
@@ -120,51 +127,60 @@ def check(key, base, zoom, fine, k, nz, exp):
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"
# (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 == 11, f"{key}: TP1 atins la {first}, nu la 11"
assert first == exp["first"], f"{key}: TP1 atins la {first}, nu la {exp['first']}"
pct = risk / entry * 100
# (4) pullback = revenire scurtă fără minim nou sub minimul dojiului mare
# (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 face minim nou"
assert min(b[2] for b in pull) > base[DOJI_B][2], f"{key}: pullback-ul rupe low-ul doji mare"
b_entry, b_sl = base[CONF_B][3], base[DOJI_B][2]
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,
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(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"
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 = 597 # coloana de etichete a 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, text) al fiecărei etichete, pentru verificări
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):
@@ -210,27 +226,47 @@ def candles(bars, x0, gap, yf):
f'height="{hh:.1f}" fill="{color}"/>')
def bracket(x, ya, yb_, color, label, side="right"):
"""Paranteză verticală între două prețuri, cu eticheta în lateral."""
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"/>')
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)
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, minsep=14):
"""Împinge în jos etichetele coloanei din dreapta ca să nu se suprapună."""
items = sorted(items, key=lambda t: t[0])
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, 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
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>')
@@ -253,79 +289,126 @@ for key, f, tf_big, tf_small, k, nz, exp in ROWS:
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, 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)
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=smax, s=ss: 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 + 9
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ă 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
# 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"]) - 15, f"high doji {tf_big}", fill="#666", size=11)
text(42, yb(g["dh"]) - 2, f"{g['dh']:.2f}", fill="#666", size=11)
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)
text(42, yb(g["conf"]) - 3, "close confirmare", fill="#999", size=11)
candles(big, BX0, BGAP, yb)
# ---- 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)
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, 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,
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, f"nivel {tf_big} {g['dh']:.2f}", "#666"),
(y_sl + 4, f"stop {g['sl']:.2f}", RED)]
if first:
col.append((y_sl + 17, "sub ultimele 4 bare", RED))
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)
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 în jos, fără minim nou",
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} din stânga",
fill="#555", anchor="middle", 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 = 30
bx = 34
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
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 4 <= ratio < 4.5, f"raportul stopurilor {ratio:.2f} nu e „de 4 ori”"
ya = top + PLOT_H + 56
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(".", ","),
@@ -335,13 +418,15 @@ for key, f, tf_big, tf_small, k, nz, exp in ROWS:
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}
# 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
@@ -349,12 +434,16 @@ for key, f, tf_big, tf_small, k, nz, exp in ROWS:
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")
# 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_})"
# 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"
@@ -362,9 +451,10 @@ for key, f, tf_big, tf_small, k, nz, exp in ROWS:
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)
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"/>')