Files
atm-curs-dl/gen/strategii/dhrt.py
Claude Agent 1e35993bc2 17_STRATEGII/10_dhrt: eticheta zonei, lipita de candela de intrare
Prima suprapunere prinsa de eticheta_libera dupa ce a invatat sa citeasca
<g transform>: era intr-un capitol deja comis, la 3 px de corpul candelei.
Reparata prin gap si latime, fara sa se schimbe vreo cifra; dreptunghiul
zonei acopera in continuare candelele 23-32.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-10-02 06:04:48 +00:00

577 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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ă <svg>, 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("</svg>")], 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'<line x1="{x0}" y1="{y:.1f}" x2="{w - 14}" y2="{y:.1f}" stroke="{color}" '
f'stroke-width="1.5" stroke-dasharray="4 3"/>']
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'<text x="{tx}" y="{ty:.1f}" fill="{color}" text-anchor="{anchor}" '
f'style="font-weight:bold">{s}</text>')
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'<rect x="{x0:.1f}" y="{y0:.1f}" width="{x1 - x0:.1f}" height="{y1 - y0:.1f}" '
f'fill="{color}" fill-opacity="0.08" stroke="{color}" stroke-width="1"/>',
f'<text x="{x0:.1f}" y="{y0 - 7:.1f}" fill="{color}" '
f'style="font-weight:bold">{label}</text>']
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'<g transform="translate(0,{y:.0f})">{"".join(extra)}{body}</g>')
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'<svg viewBox="0 0 {w} {height:.0f}" xmlns="http://www.w3.org/2000/svg" '
f'style="width:100%;height:auto;font:12px sans-serif">')
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'<text x="24" y="{cap_top + 17 * k:.0f}" fill="{GREY}">{c}</text>')
svg = "\n ".join([head, *parts, *caps, "</svg>"]).replace("\n </svg>", "\n</svg>")
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'<text x="([-\d.]+)" y="([-\d.]+)"([^>]*)>(.*?)</text>', 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'<polyline points="{x + 5:.1f},{y1:.1f} {x:.1f},{y1:.1f} {x:.1f},{y2:.1f} '
f'{x + 5:.1f},{y2:.1f}" fill="none" stroke="{color}" stroke-width="1.5"/>']
y0 = (y1 + y2) / 2 + 4 if mijloc else y1 + 12
for k, t in enumerate(texte):
out.append(f'<text x="{x + dx:.1f}" y="{y0 + 14 * k - 7 * (len(texte) - 1):.1f}" '
f'fill="{color}" style="font-weight:bold">{t}</text>')
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)")