#!/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)")