diff --git a/gen/eticheta_libera.py b/gen/eticheta_libera.py
index c12a6eb..98046e3 100644
--- a/gen/eticheta_libera.py
+++ b/gen/eticheta_libera.py
@@ -23,7 +23,10 @@ def _bbox(m, min_px):
"""Caseta etichetei, lărgită cu marginea cerută."""
tx, ty, attrs, s = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
w = CW * len(s)
- x0 = tx if 'anchor="start"' in attrs else tx - w if 'anchor="end"' in attrs else tx - w / 2
+ # fara text-anchor, SVG aliniaza la stanga (start), nu centrat. Presupunerea
+ # ca e centrat dadea alarme false pe etichetele de Level cu label_at="left",
+ # care se deseneaza fara anchor.
+ x0 = tx - w if 'anchor="end"' in attrs else tx if 'anchor="middle"' not in attrs else tx - w / 2
return s, (x0 - min_px, x0 + w + min_px, ty - 9 - min_px, ty + 3 + min_px)
diff --git a/gen/strategii/dhrt.py b/gen/strategii/dhrt.py
new file mode 100644
index 0000000..a94b1f4
--- /dev/null
+++ b/gen/strategii/dhrt.py
@@ -0,0 +1,570 @@
+#!/usr/bin/env python3
+"""Diagramele pentru summaries/17_STRATEGII/10_dhrt.md (strategia 8, DHRT).
+
+Un singur generator pentru toate cele șase diagrame ale capitolului: toate
+desenează același trade, pe aceeași serie, deci o cifră schimbată într-un loc
+trebuie să cadă peste tot — de asta nu sunt șase fișiere.
+
+REGULA DE CIFRE, din care iese tot restul fișierului: în lecția 8 (DHRT)
+singurul preț rostit e **51** (ținta de pe XLF, varianta short — transcrierea
+`05_7_STRATEGIA_8_DHRTMECANICĂ.txt:285`). Capitolul nu are diagramă cu XLF,
+deci **niciun preț de pe diagramele de aici nu e al cursului**: toate vin din
+exemplul construit de mine pe geometria lui, iar fiecare diagramă o declară
+în titlu („exemplu construit"). Cifrele sunt aceleași cu ale sumarizării
+lecției (`summaries/06_MODUL_5/05_strategia_8_dhrt.md`), ca să nu existe două
+versiuni ale aceluiași trade în repo.
+
+Geometria exemplului:
+ cap 52.00 · nivelul umerilor 58.50 · H = 6.50
+ treimea de jos: 52.00 .. 54.17 — stopul stă în ea, 54.17 e cel mai sus
+ la care are voie (riscul cel mai mic permis)
+ linia gâtului 63.00, trasată din interiorul gâtului (vârfurile ating 63.30)
+ intrare 65.00 (close-ul candelei de breakout) · R = 10.83 = 16,7% din preț
+ TP1 = 65.00 + 10.83 = 75.83 (distanța stopului)
+ TP2 = 63.00 + 11.00 = 74.00 (proiecția gâtului) — sub TP1, ordinea nu e fixă
+
+Rulează: .venv/bin/python gen/strategii/dhrt.py
+"""
+import re
+import sys
+from dataclasses import replace
+from pathlib import Path
+
+sys.path.insert(0, "/workspace/atm-curs-dl")
+sys.path.insert(0, "/workspace/atm-curs-dl/gen")
+from eticheta_libera import eticheta_libera # noqa: E402
+from svg_candles import Arrow, Candle, Chart, Level, Mark, Zone, from_closes # noqa: E402
+
+OUT = Path("/workspace/atm-curs-dl/gen/strategii")
+GREEN, RED, BLUE, PURPLE, ORANGE, GREY, BLACK = (
+ "#188a3e", "#c00", "#1a5fb4", "#6a3d9a", "#b06000", "#555", "#333")
+
+CAP, UMAR = 52.00, 58.50 # low-ul capului, nivelul umerilor
+H = round(UMAR - CAP, 2) # 6.50 — înălțimea cap → umăr drept
+TREIME = round(CAP + H / 3, 2) # 54.17 — marginea de sus a treimii
+GAT, GAT_VARF = 63.00, 63.30 # linia gâtului / vârfurile zonei
+ENTRY = 65.00
+R = round(ENTRY - TREIME, 2) # 10.83
+TP1 = round(ENTRY + R, 2) # 75.83
+TP2 = round(GAT + (GAT - CAP), 2) # 74.00
+CONS_SUS, CONS_JOS = GAT_VARF, UMAR # dreptunghiul umărului drept
+CONS_JUM = round((CONS_SUS + CONS_JOS) / 2, 2) # 60.90 — jumătatea consolidării
+
+assert H == 6.50 and TREIME == 54.17 and R == 10.83 and TP1 == 75.83 and TP2 == 74.00
+assert TP2 < TP1, "pe exemplul ăsta TP1 iese peste TP2 — asta ilustrează capitolul"
+assert round(R / ENTRY * 100, 1) == 16.7, round(R / ENTRY * 100, 1)
+assert CONS_JUM == 60.90, CONS_JUM
+
+SVG = {}
+
+
+# ------------------------------------------------------------------ utilitare
+def panou(ch: Chart, w: int):
+ """Corpul unui Chart, fără ")], float(m.group(2))
+
+
+def nivel(ch, w, price, texte, color, below=False, la_stanga=True, x0=24):
+ """Linie punctată + etichetă pe mai multe rânduri, desenată de mână."""
+ y = ch._y(price)
+ out = [f'']
+ for k, s in enumerate(texte):
+ ty = (y + 15 + 14 * k) if below else (y - 6 - 14 * (len(texte) - 1 - k))
+ tx, anchor = (x0 + 6, "start") if la_stanga else (w - 24, "end")
+ out.append(f'{s}')
+ return out
+
+
+def zona(ch, i0, i1, label, color, sus=None, jos=None, lift=0.0, pad=10):
+ cds = ch.candles[i0:i1 + 1]
+ x0, x1 = ch._cx(i0) - pad, ch._cx(i1) + pad
+ y0 = ch._y(sus if sus is not None else max(c.h for c in cds)) - 8 - lift
+ y1 = ch._y(jos if jos is not None else min(c.l for c in cds)) + 8
+ return [f'',
+ f'{label}']
+
+
+def ancorate(ch: Chart) -> set:
+ """Etichetele lipite prin construcție de ceva desenat: nivelurile (y − 6 față
+ de linia lor) și textele de săgeată (stau la capătul propriei cozi). Pentru
+ ele se verifică doar candelele, nu și segmentele."""
+ out = {lv.text for lv in ch.levels} | {mk.text for mk in ch.marks}
+ for a in ch.arrows:
+ out |= set(a.text.split("\n"))
+ return out
+
+
+def stiva(nume, w, panouri, captions, pe_linie=()):
+ """Lipește panouri unul sub altul și verifică etichetele (pânză + suprapuneri)."""
+ parts, y, bodies = [], 0.0, []
+ for ch, extra in panouri:
+ body, h = panou(ch, w)
+ # extra se construiește DUPĂ randare: `_y()` are nevoie de `_pmax`,
+ # iar `_render()` mută `top` — altfel desenul de mână iese deplasat.
+ extra = list(extra(ch) if callable(extra) else extra)
+ parts.append(f'{"".join(extra)}{body}')
+ bodies.append((body + "".join(extra), y, ancorate(ch) | set(pe_linie)))
+ y += h
+ cap_top = y + 18
+ height = cap_top + 17 * (len(captions) - 1) + 8
+ head = (f'"]).replace("\n ", "\n")
+ verifica(nume, svg, w, height, bodies)
+ SVG[nume] = svg
+ return svg
+
+
+def singur(nume, ch, w, extra=(), captions=(), pe_linie=()):
+ """Un singur panou, pe aceeași cale de verificare ca stiva."""
+ return stiva(nume, w, [(ch, extra)], list(captions), pe_linie)
+
+
+TEXT = re.compile(r']*)>(.*?)', re.S)
+
+
+def verifica(nume, svg, w, height, bodies):
+ """Etichetele stau în pânză, nu se suprapun între ele și nu stau peste desen.
+
+ `pe_linie` = etichetele de nivel: alea stau lipite de linia lor prin
+ construcție (y − 6), deci pentru ele se verifică doar candelele.
+ """
+ assert "\n\n" not in svg, f"{nume}: linie goală în SVG (markdown rupe blocul)"
+ for body, off, pe_linie in bodies:
+ toate = {m.group(4) for m in TEXT.finditer(body)}
+ eticheta_libera(body, nume, toate - pe_linie, rects=True)
+ eticheta_libera(body, nume, toate & pe_linie, rects=True, segmente=False)
+ et = []
+ for m in TEXT.finditer(body):
+ x, y, attrs, s = float(m.group(1)), float(m.group(2)) + off, m.group(3), m.group(4)
+ bold = "bold" in attrs
+ ww = len(s) * 12 * (0.62 if bold else 0.52)
+ left = (x - ww if 'anchor="end"' in attrs
+ else x - ww / 2 if 'anchor="middle"' in attrs else x)
+ et.append((left, left + ww, y, s))
+ assert left >= -2 and left + ww <= w + 2, f"{nume}: text iese din pânză: {s!r}"
+ assert 0 <= y <= height, f"{nume}: text în afara înălțimii: {s!r} (y={y:.0f})"
+ for i in range(len(et)):
+ for j in range(i + 1, len(et)):
+ li, ri, yi, si = et[i]
+ lj, rj, yj, sj = et[j]
+ if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
+ raise AssertionError(f"{nume}: texte suprapuse: {si!r} / {sj!r}")
+
+
+def check(cds, i0=None):
+ """h/l cuprind o/c, și open = close-ul precedentei (serie fără goluri)."""
+ for i, c in enumerate(cds):
+ assert c.l <= min(c.o, c.c) and c.h >= max(c.o, c.c), f"bara {i}: h/l nu cuprind o/c"
+ if i:
+ assert abs(c.o - cds[i - 1].c) < 1e-9, f"open[{i}] != close[{i-1}]"
+ return cds
+
+
+
+def felie(cds, a, b):
+ """Copie a barelor a..b-1 — ca o diagramă să nu mute culoarea alteia."""
+ return [replace(c) for c in cds[a:b]]
+
+
+def bracket(ch, x, p_jos, p_sus, texte, color, dx=8, mijloc=True):
+ """Acoladă verticală între două prețuri, cu text la mijloc (sau sus)."""
+ y1, y2 = ch._y(p_sus), ch._y(p_jos)
+ out = [f'']
+ y0 = (y1 + y2) / 2 + 4 if mijloc else y1 + 12
+ for k, t in enumerate(texte):
+ out.append(f'{t}')
+ return out
+
+
+# --------------------------------------------------------------- seria de bază
+# Patternul întreg, o singură dată: toate diagramele pornesc din `serie()`.
+# 0-5 coborârea spre umărul stâng 6 umărul stâng (low 58.50)
+# 7-10 urcarea în gât (high 63.30) 11-15 coborârea în cap
+# 16 capul (low 52.00) 17-22 urcarea înapoi în gât
+# 23-32 consolidarea = umărul drept 33 candela de breakout (close 65.00)
+# 34-43 drumul spre ținte
+CLOSES = [
+ 65.80, 64.10, 62.60, 61.40, 60.10, 59.00, # 0-5
+ 58.70, # 6 umărul stâng
+ 60.20, 61.60, 62.70, 63.05, # 7-10 gâtul, stânga
+ 61.30, 59.20, 57.00, 55.10, 53.20, # 11-15
+ 52.30, # 16 capul
+ 54.60, 56.90, 58.80, 60.70, 62.20, 63.00, # 17-22 gâtul, dreapta
+ 61.10, 59.30, 58.80, 60.40, 62.10, 60.90, # 23-28 consolidarea
+ 59.10, 60.30, 59.40, 58.70, # 29-32 consolidarea
+ 65.00, # 33 breakout (marubozu)
+ 66.40, 68.10, 70.30, 72.20, 74.10, 75.90, # 34-39
+ 75.20, 76.40, 75.60, 76.10, # 40-43
+]
+WICKS = {6: (0.35, 0.20), 10: (0.25, 0.30), 16: (0.40, 0.30), 22: (0.30, 0.25),
+ 24: (0.25, 0.60), 29: (0.30, 0.60), 32: (0.30, 0.20), 33: (0.30, 0.30)}
+VOLS = [44, 46, 43, 45, 42, 44,
+ 66, # umărul stâng — vârf de volum (stâng, pe volum)
+ 48, 45, 43, 44,
+ 46, 48, 52, 56, 60,
+ 82, # capul pe volum
+ 50, 46, 44, 43, 45, 44,
+ 42, 40, 58, 44, 46, 43,
+ 41, 46, 44, 42, # 29-32: volum care se stinge în consolidare
+ 96, # breakout: peste vârful anterior de volum
+ 70, 62, 58, 56, 54, 52,
+ 48, 50, 46, 47]
+
+
+def serie():
+ cds = from_closes(CLOSES, wick=0.30,
+ wicks=[WICKS.get(i, (0.30, 0.30)) for i in range(len(CLOSES))],
+ vols=[float(v) for v in VOLS], start=66.90)
+ cds[33] = Candle(58.70, 65.30, 58.50, ENTRY, vol=float(VOLS[33])) # marubozu de breakout
+ check(cds)
+ # cifrele pe care le afirmă etichetele, verificate pe serie
+ assert round(cds[6].l, 2) == UMAR, cds[6].l
+ assert round(cds[16].l, 2) == CAP, cds[16].l
+ assert round(max(c.h for c in cds[7:11]), 2) == GAT_VARF, max(c.h for c in cds[7:11])
+ assert round(max(c.h for c in cds[17:33]), 2) == GAT_VARF, max(c.h for c in cds[17:33])
+ assert round(min(c.l for c in cds[23:33]), 2) == UMAR, min(c.l for c in cds[23:33])
+ assert cds[16].l < cds[6].l and cds[16].l < min(c.l for c in cds[23:33]), "capul nu e cel mai jos"
+ assert abs(cds[33].c - ENTRY) < 1e-9 and cds[33].c > GAT_VARF, "breakout-ul nu trece gâtul"
+ assert cds[33].c - cds[33].o >= 6.0, "candela de breakout nu e marubozu"
+ assert max(VOLS[:33]) == 82 and VOLS[33] == 96, "volumul de breakout nu e peste vârful anterior"
+ assert max(c.h for c in cds[34:]) >= TP1, "seria nu atinge TP1"
+ primul_tp2 = next(i for i in range(34, len(cds)) if cds[i].h >= TP2)
+ primul_tp1 = next(i for i in range(34, len(cds)) if cds[i].h >= TP1)
+ assert primul_tp2 < primul_tp1, "TP2 (74.00) trebuie atins înaintea lui TP1 (75.83)"
+ assert min(c.l for c in cds[34:]) > TREIME, "stopul ar fi fost lovit după intrare"
+ return cds
+
+
+# ---------------------------------------------------------------- 1. ansamblu
+def d_ansamblu():
+ cds = serie()
+ w = 740
+ niveluri = [
+ Level(TP1, f"TP1 {TP1:.2f} = distanța stopului", BLUE, label_at="left"),
+ Level(TP2, f"TP2 {TP2:.2f} = proiecția gâtului", BLUE, label_at="left"),
+ Level(ENTRY, f"intrare {ENTRY:.2f}", GREEN),
+ Level(GAT, f"linia gâtului {GAT:.2f}", PURPLE),
+ Level(TREIME, f"SL {TREIME:.2f} (cel mai sus permis)", RED, label_at="left"),
+ ]
+ ch = Chart(
+ candles=cds,
+ levels=niveluri,
+ zones=[Zone(23, 32, "umărul drept = consolidarea", GREEN, p_top=GAT_VARF + 0.3)],
+ arrows=[
+ Arrow(16, "capul 52.00, pe suport", where="below", color=BLACK, dx=-20, dy=16),
+ Arrow(6, "umărul stâng 58.50", where="below", color=BLACK, dx=-14, dy=46),
+ Arrow(33, "AICI INTRI: close 65.00,\npeste consolidare și peste gât",
+ where="below", price=ENTRY, color=GREEN, dx=-96, dy=78),
+ ],
+ scale=15.0, gap=15, body_w=9, left=58, width=w, vol_h=66,
+ vol_label="volum", top=34,
+ title="DHRT — tot trade-ul (exemplu construit: niciun preț de aici nu e rostit în lecție)",
+ )
+ singur("ansamblu", ch, w, (), [
+ "Serie construită (open = close-ul barei anterioare), la scară, pe geometria lecției.",
+ f"cap {CAP:.2f} · umeri {UMAR:.2f} · H {H:.2f} · gât {GAT:.2f} · "
+ f"intrare {ENTRY:.2f} · SL {TREIME:.2f} · R {R:.2f} (16,7% din preț).",
+ f"TP1 ({TP1:.2f}) iese peste TP2 ({TP2:.2f}) — ordinea lor nu e fixă.",
+ ], pe_linie=[lv.text for lv in niveluri])
+
+
+
+# -------------------------------------------------------------------- 4. stop
+def d_stop():
+ """H = cap → nivelul umărului drept, împărțit în trei. Stopul stă în treimea
+ de jos; 54.17 e marginea ei de sus, adică cel mai sus la care are voie."""
+ cds = felie(serie(), 12, 36) # capul, urcarea, consolidarea, breakout-ul
+ w = 760
+ i_cap, i_brk = 16 - 12, 33 - 12
+ doua_treimi = round(CAP + 2 * H / 3, 2)
+ assert doua_treimi == 56.33, doua_treimi
+ niveluri = [
+ Level(ENTRY, f"intrare {ENTRY:.2f} = close-ul candelei de breakout", GREEN,
+ label_at="left"),
+ Level(UMAR, f"umărul drept {UMAR:.2f}", BLACK, label_at="left"),
+ Level(doua_treimi, f"{doua_treimi:.2f} = 2/3", GREY, dash="2 4"),
+ Level(TREIME, f"SL {TREIME:.2f} = 1/3 din H", RED, label_at="left"),
+ Level(CAP, f"capul {CAP:.2f}", BLACK, label_at="left"),
+ ]
+ ch = Chart(
+ candles=cds,
+ levels=niveluri,
+ arrows=[
+ Arrow(i_cap, f"{TREIME:.2f} e cel mai SUS la care are voie stopul\n"
+ f"(riscul cel mai mic permis); mai jos, oricât",
+ where="below", price=TREIME, color=RED, dx=-30, dy=72),
+ ],
+ scale=25.0, gap=20, body_w=11, left=200, width=w,
+ title="Stopul: treimea de jos din H (exemplu construit — cifrele nu sunt rostite în lecție)",
+ )
+
+ def extra(ch):
+ return (bracket(ch, 160, CAP, UMAR, [f"H = {H:.2f}", "cap → umăr"], BLACK, dx=6)
+ + bracket(ch, ch._cx(i_brk) + 52, TREIME, ENTRY,
+ [f"R = {R:.2f}"], RED, dx=8))
+
+ singur("stop", ch, w, extra, [
+ "H se măsoară de la low-ul capului la nivelul umărului drept — nu la linia gâtului.",
+ f"{CAP:.2f} + {H:.2f}/3 = {TREIME:.2f}. Peste {TREIME:.2f} stopul nu are voie; sub, oricât.",
+ f"R se măsoară de la intrare ({ENTRY:.2f}) la stop, nu de la nivelul umărului.",
+ ], pe_linie=([lv.text for lv in niveluri]
+ + [f"H = {H:.2f}", "cap → umăr", f"R = {R:.2f}"]))
+
+
+# ------------------------------------------------------------------- 5. ținte
+def d_tinte():
+ """TP1 = distanța stopului; TP2 = proiecția gâtului. Linia gâtului se trage
+ din interiorul gâtului (63.00), nu prin vârfuri (63.30) și nu ca medie."""
+ cds = felie(serie(), 16, 44)
+ w = 760
+ i_brk, i_tp2, i_tp1 = 33 - 16, 38 - 16, 39 - 16
+ inaltime = round(GAT - CAP, 2)
+ assert inaltime == 11.00 and round(GAT + inaltime, 2) == TP2
+ niveluri = [
+ Level(TP1, f"TP1 {TP1:.2f} = intrarea + R ({R:.2f})", BLUE, label_at="left"),
+ Level(TP2, f"TP2 {TP2:.2f} = gâtul + {inaltime:.2f}", BLUE, label_at="left"),
+ Level(ENTRY, f"intrare {ENTRY:.2f}", GREEN, label_at="left"),
+ Level(GAT, f"linia gâtului {GAT:.2f}", PURPLE),
+ Level(CAP, f"capul {CAP:.2f}", BLACK),
+ ]
+ ch = Chart(
+ candles=cds,
+ levels=niveluri,
+ arrows=[
+ Arrow(6, f"vârfurile gâtului ating {GAT_VARF:.2f}; linia se trage din\n"
+ f"INTERIORUL gâtului, la {GAT:.2f} — nu e media lor",
+ where="above", price=GAT_VARF, color=PURPLE, dx=-30, dy=-50),
+ Arrow(i_tp2, f"TP2 ({TP2:.2f}) vine înaintea lui TP1 ({TP1:.2f})\n"
+ f"pe exemplul ăsta — ordinea nu e fixă",
+ where="below", price=TP2, color=BLUE, dx=-290, dy=30),
+ ],
+ scale=15.0, gap=22, body_w=11, left=92, width=w,
+ title="Cele două ținte (exemplu construit — cifrele nu sunt rostite în lecție)",
+ )
+
+ def extra(ch):
+ return (bracket(ch, 58, CAP, GAT, [f"{inaltime:.2f}"], PURPLE, dx=6)
+ + bracket(ch, ch._cx(i_tp1) + 44, GAT, TP2, [f"{inaltime:.2f}"], BLUE, dx=8))
+
+ singur("tinte", ch, w, extra, [
+ f"TP1 = {ENTRY:.2f} + {R:.2f} = {TP1:.2f}. TP2 = {GAT:.2f} + ({GAT:.2f} − {CAP:.2f}) = {TP2:.2f}.",
+ "Cât de jos tragi linia gâtului mută TP2 cu exact atât — e o alegere a ta, nu o formulă.",
+ "Lecția spune doar «din interiorul gâtului, iau un average»; formulă rostită nu există.",
+ ], pe_linie=[lv.text for lv in niveluri] + [f"{inaltime:.2f}"])
+
+
+# ---------------------------------------------------------------- 3. trigger
+# Panoul NU: preț care ține consolidarea, dar volum cu semn (stil on balance
+# volume) care face minime tot mai joase. Seria e construită doar pentru
+# diagrama asta — niciun preț de aici nu e rostit în lecție.
+NU_CLOSES = [61.80, 60.20, 61.70, 59.90, 61.90, 60.10, 61.60, 59.80,
+ 61.80, 60.00, 61.40, 59.70, 57.40, 56.10]
+NU_VOLS = [8.0, 5.0, 6.0, -4.0, 3.0, -7.0, 2.0, -10.0,
+ 1.0, -13.0, -2.0, -16.0, -9.0, -20.0]
+
+
+def d_trigger():
+ cds_da = felie(serie(), 12, 37)
+ w = 760
+ i_brk = 33 - 12
+ niv_da = [
+ Level(ENTRY, f"intrare {ENTRY:.2f} = close-ul trigger-ului", GREEN, label_at="left"),
+ Level(GAT, f"linia gâtului {GAT:.2f}", PURPLE, label_at="left"),
+ ]
+ ch_da = Chart(
+ candles=cds_da,
+ levels=niv_da,
+ zones=[Zone(23 - 12, 32 - 12, "consolidarea umărului drept", GREEN,
+ p_top=GAT_VARF + 0.3)],
+ arrows=[
+ Arrow(i_brk, "TRIGGER: close peste consolidare ȘI peste gât,\n"
+ "cu volumul peste vârful anterior de volum",
+ where="below", price=ENTRY, color=GREEN, dx=-276, dy=36),
+ ],
+ scale=19.0, gap=20, body_w=11, left=60, width=w, vol_h=60,
+ vol_label="volum", title="DA — trigger în umărul drept (exemplu construit)",
+ )
+
+ cds_nu = from_closes(NU_CLOSES, wick=0.35, vols=NU_VOLS, start=62.40)
+ check(cds_nu)
+ minime = [NU_VOLS[i] for i in range(1, len(NU_VOLS) - 1)
+ if NU_VOLS[i] < NU_VOLS[i - 1] and NU_VOLS[i] < NU_VOLS[i + 1]]
+ assert minime == sorted(minime, reverse=True) and len(minime) >= 4, minime
+ assert all(c.l >= UMAR for c in cds_nu[:11]), "prețul trebuie să țină consolidarea"
+ assert cds_nu[-1].c < UMAR, "la final prețul cedează"
+ ch_nu = Chart(
+ candles=cds_nu,
+ levels=[Level(UMAR, f"baza consolidării {UMAR:.2f}", GREY, label_at="left")],
+ arrows=[
+ Arrow(13, "prețul ține consolidarea, dar minimele volumului coboară:\n"
+ "aștepți breakdown, nu intri long",
+ where="below", price=56.20, color=RED, dx=-215, dy=10),
+ ],
+ scale=19.0, gap=28, body_w=13, left=72, width=w, vol_h=70, vol_zero=0.0,
+ vol_label="volum cu semn (on balance volume)",
+ title="NU — breakout de preț fără breakout de volum (exemplu construit)",
+ )
+ stiva("trigger", w, [(ch_da, ()), (ch_nu, ())], [
+ "Sus: trigger-ul e candela de breakout; buy doji / buy hammer / buy momentum",
+ "din umărul drept sunt la fel de valide, cu intrarea la close-ul lor.",
+ "Jos: singurul «nu intra» formulat ca regulă mecanică — volumul rupe înaintea prețului.",
+ ], pe_linie=[lv.text for lv in niv_da] + [f"baza consolidării {UMAR:.2f}", "0"])
+
+
+# ------------------------------------------------------------------ 2. setup
+# Panoul NU: triple bottom la al treilea test, cu volum tot mai mic. Serie
+# proprie, cu altă scară de preț decât restul capitolului — regulile sunt
+# aceleași, cifrele nu au legătură cu exemplul construit al trade-ului.
+TB_CLOSES = [54.50, 53.20, 51.80, 50.40, 50.20, 51.60, 52.80, 51.90, 50.80, 50.30,
+ 51.50, 52.40, 51.30, 50.60, 50.25, 51.00, 50.40, 49.60, 48.70]
+TB_BAZA = 50.00
+TB_LOWS = (4, 9, 14)
+TB_VOLS = [40.0, 44.0, 52.0, 60.0, 74.0, 46.0, 42.0, 44.0, 48.0, 54.0,
+ 40.0, 38.0, 39.0, 40.0, 38.0, 36.0, 34.0, 44.0, 52.0]
+
+
+def d_setup():
+ cds_da = felie(serie(), 0, 33)
+ w = 760
+ niv_da = [
+ Level(UMAR, f"pivotul lunii {UMAR:.2f}", BLACK),
+ Level(CAP, f"suportul lunii {CAP:.2f}", BLUE, label_at="left"),
+ ]
+ ch_da = Chart(
+ candles=cds_da,
+ levels=niv_da,
+ marks=[Mark(6, "umărul stâng", GREY, "below", dy=14)],
+ zones=[Zone(23, 32, "umărul drept = consolidarea", GREEN, p_top=GAT_VARF + 0.3)],
+ arrows=[
+ Arrow(16, "capul se oprește pe suport, umărul drept pe pivot.\n"
+ "Ajunge unul din două; fără niciunul, setup-ul nu e verificabil.",
+ where="below", price=CAP, color=BLUE, dx=10, dy=22),
+ ],
+ scale=15.0, gap=17, body_w=9, left=58, width=w,
+ title="DA — HS răsturnat cu capul și umărul drept pe niveluri (exemplu construit)",
+ )
+
+ cds_nu = from_closes(TB_CLOSES, wick=0.30,
+ wicks=[(0.30, 0.30) if i not in TB_LOWS else
+ (0.25, round(TB_CLOSES[i] - TB_BAZA, 2)) for i in range(19)],
+ vols=TB_VOLS, start=55.60)
+ check(cds_nu)
+ for i in TB_LOWS:
+ assert round(cds_nu[i].l, 2) == TB_BAZA, (i, cds_nu[i].l)
+ assert TB_VOLS[4] > TB_VOLS[9] > TB_VOLS[14], "volumul trebuie să scadă la fiecare picior"
+ assert cds_nu[-1].c < TB_BAZA, "al patrulea test cedează"
+ ch_nu = Chart(
+ candles=cds_nu,
+ levels=[Level(TB_BAZA, f"baza {TB_BAZA:.2f}, testată de trei ori", GREY)],
+ marks=[Mark(4, "1", GREY, "below"), Mark(9, "2", GREY, "below"),
+ Mark(14, "3", RED, "below")],
+ arrows=[
+ Arrow(14, "al treilea test, cu volum mai mic la fiecare picior:\n"
+ "șansele se întorc spre breakdown — nu intri",
+ where="below", price=TB_BAZA, color=RED, dx=-90, dy=42),
+ ],
+ scale=38.0, gap=26, body_w=13, left=70, width=w, vol_h=56,
+ vol_label="volum", title="NU — triple bottom la al treilea test (altă scară de preț)",
+ )
+ stiva("setup", w, [(ch_da, ()), (ch_nu, ())], [
+ "Sus: filtrul de la pasul 1 — capul și/sau umărul drept pe un suport, pivot sau PPA.",
+ "Simetria umerilor e analiza autorului, nu un prag: «depinde cum măsori».",
+ "Jos: motivul e citat de autor din literatură, nu din testele lui.",
+ ], pe_linie=[lv.text for lv in niv_da]
+ + [f"baza {TB_BAZA:.2f}, testată de trei ori"])
+
+
+# ----------------------------------------------------------------- 6. retest
+# „Locul crimei" = dreptunghiul din care s-a plecat. Ambele panouri pornesc din
+# aceeași consolidare și din aceeași candelă de breakout; diferă doar adâncimea
+# întoarcerii. Serii construite, pe aceeași geometrie ca restul capitolului.
+CONS = [61.00, 59.20, 62.90, 59.00, 61.50, 58.80]
+CONS_W = [(0.30, 0.30), (0.30, 0.50), (0.40, 0.30), (0.30, 0.50),
+ (0.30, 0.30), (0.30, 0.30)]
+RETEST_DA = [64.20, 62.40, 61.40, 61.20, 62.30, 64.10, 66.00, 67.60]
+RETEST_NU = [64.00, 62.10, 60.20, 58.20, 56.80, 55.40]
+
+
+def _cu_retest(cozi):
+ cds = from_closes(CONS, wicks=CONS_W, start=62.20)
+ cds.append(Candle(CONS[-1], 65.30, 58.60, ENTRY))
+ cds += from_closes(cozi, wick=0.30, start=ENTRY)
+ check(cds)
+ assert round(max(c.h for c in cds[:6]), 2) == CONS_SUS, max(c.h for c in cds[:6])
+ assert round(min(c.l for c in cds[:6]), 2) == CONS_JOS, min(c.l for c in cds[:6])
+ return cds
+
+
+def d_retest():
+ w = 740
+ niv = lambda: [
+ Level(ENTRY, f"breakout {ENTRY:.2f}", GREEN),
+ Level(CONS_SUS, f"vârful consolidării {CONS_SUS:.2f}", GREY, dash="2 4"),
+ Level(CONS_JUM, f"1/2 din consolidare {CONS_JUM:.2f}", BLUE),
+ Level(CONS_JOS, f"baza consolidării {CONS_JOS:.2f}", RED),
+ ]
+ cds_da = _cu_retest(RETEST_DA)
+ fund = round(min(c.l for c in cds_da[7:]), 2)
+ assert fund == CONS_JUM, fund
+ assert cds_da[-1].c > ENTRY, "după retest trade-ul repleacă"
+ ch_da = Chart(
+ candles=cds_da,
+ levels=niv(),
+ zones=[Zone(0, 5, "locul crimei", ORANGE, p_top=CONS_SUS + 0.2, p_bot=CONS_JOS - 0.2)],
+ arrows=[
+ Arrow(10, f"retest până la jumătatea consolidării ({CONS_JUM:.2f}):\n"
+ f"a doua fereastră de intrare",
+ where="below", price=CONS_JUM, color=BLUE, dx=-120, dy=24),
+ ],
+ scale=26.0, gap=34, body_w=15, left=80, width=w,
+ title="DA — retest la 1/2 din consolidare (exemplu construit)",
+ )
+ cds_nu = _cu_retest(RETEST_NU)
+ assert min(c.l for c in cds_nu[7:]) < CONS_JOS, "panoul NU trebuie să rupă baza"
+ ch_nu = Chart(
+ candles=cds_nu,
+ levels=niv(),
+ zones=[Zone(0, 5, "locul crimei", ORANGE, p_top=CONS_SUS + 0.2, p_bot=CONS_JOS - 0.2)],
+ arrows=[
+ Arrow(11, "sub bază ideea s-a inversat:\nde aici se shortează, nu se cumpără",
+ where="below", price=CONS_JOS - 1.6, color=RED, dx=-130, dy=10),
+ ],
+ scale=26.0, gap=34, body_w=15, left=80, width=w,
+ title="NU — retest care trece sub bază (exemplu construit)",
+ )
+ stiva("retest", w, [(ch_da, ()), (ch_nu, ())], [
+ "Singura cifră a secțiunii: «unul pe doi retestează din consolidare» — jumătatea.",
+ "Între jumătate și bază e zonă de avertizare; sub bază, ideea s-a inversat.",
+ "Retestul nu e garantat: de aceea intrarea se face în tranșe, nu așteptând.",
+ ], pe_linie=[lv.text for lv in niv()])
+
+
+if __name__ == "__main__":
+ d_ansamblu()
+ d_setup()
+ d_stop()
+ d_tinte()
+ d_trigger()
+ d_retest()
+ for nume, svg in SVG.items():
+ p = OUT / f"dhrt_{nume}.svg"
+ p.write_text(svg, encoding="utf-8")
+ print(f"scris {p.name} ({len(svg)} octeți)")
diff --git a/gen/strategii/dhrt_ansamblu.svg b/gen/strategii/dhrt_ansamblu.svg
new file mode 100644
index 0000000..35fafff
--- /dev/null
+++ b/gen/strategii/dhrt_ansamblu.svg
@@ -0,0 +1,164 @@
+
\ No newline at end of file
diff --git a/gen/strategii/dhrt_retest.svg b/gen/strategii/dhrt_retest.svg
new file mode 100644
index 0000000..cd17701
--- /dev/null
+++ b/gen/strategii/dhrt_retest.svg
@@ -0,0 +1,95 @@
+
\ No newline at end of file
diff --git a/gen/strategii/dhrt_setup.svg b/gen/strategii/dhrt_setup.svg
new file mode 100644
index 0000000..3996fd3
--- /dev/null
+++ b/gen/strategii/dhrt_setup.svg
@@ -0,0 +1,156 @@
+
\ No newline at end of file
diff --git a/gen/strategii/dhrt_stop.svg b/gen/strategii/dhrt_stop.svg
new file mode 100644
index 0000000..dd14fc2
--- /dev/null
+++ b/gen/strategii/dhrt_stop.svg
@@ -0,0 +1,70 @@
+
\ No newline at end of file
diff --git a/gen/strategii/dhrt_tinte.svg b/gen/strategii/dhrt_tinte.svg
new file mode 100644
index 0000000..d4d1ed9
--- /dev/null
+++ b/gen/strategii/dhrt_tinte.svg
@@ -0,0 +1,82 @@
+
\ No newline at end of file
diff --git a/gen/strategii/dhrt_trigger.svg b/gen/strategii/dhrt_trigger.svg
new file mode 100644
index 0000000..8e55c85
--- /dev/null
+++ b/gen/strategii/dhrt_trigger.svg
@@ -0,0 +1,149 @@
+
\ No newline at end of file
diff --git a/gen/strategii/substituie.py b/gen/strategii/substituie.py
index a35473e..4f6e707 100644
--- a/gen/strategii/substituie.py
+++ b/gen/strategii/substituie.py
@@ -19,7 +19,10 @@ import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
-CAPITOLE = sorted((ROOT / "summaries" / "17_STRATEGII").glob("[0A][1-8]_*.md"))
+_S = ROOT / "summaries" / "17_STRATEGII"
+# numerele au ajuns la doua cifre (09, 10): un glob pe [0A][1-8] le sarea, si
+# sarea tacut — capitolul parea doar sa n-aiba marcaje.
+CAPITOLE = sorted({*_S.glob("[0-9][0-9]_*.md"), *_S.glob("A[0-9]_*.md")})
# fie marcajul gol (prima punere), fie blocul deja ancorat (repunere)
MARCAJ = re.compile(
r"^[ \t]*[ \t]*$"