#!/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ă , plus înălțimea lui — pentru stivuire. `_render()`, nu `render()`: al doilea restaurează `top`, iar liniile desenate de mână după el ar ieși deplasate față de candele. """ svg = ch._render() m = re.search(r'viewBox="0 0 ([\d.]+) ([\d.]+)"', svg) assert m and float(m.group(1)) <= w, f"panoul iese din {w}px: {m.group(1) if m else '?'}" return svg[svg.index(">") + 1:svg.rindex("")], 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'') caps = [] for k, c in enumerate(captions): assert "<" not in c and 24 + len(c) * 6.3 <= w, f"{nume}: caption iese din pânză: {c!r}" caps.append(f'{c}') svg = "\n ".join([head, *parts, *caps, ""]).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() # 740 -> 770: cu gap-ul mărit, candela de intrare ajungea sub etichetele de # nivel din marginea dreaptă („linia gâtului 63.00") w = 770 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), ], # gap 15->16: eticheta zonei („umărul drept = consolidarea") se termina # la 3 px de corpul candelei de intrare — se citea lipită de ea. Zona # începe la candela 23, candela care deranjează e a 34-a, deci fiecare # pixel de gap le desparte cu 11. scale=15.0, gap=16, 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)")