Files
atm-curs-dl/gen/patternuri_complexe_3.py
Claude Agent 973410823e etichete: cinci cifre scoase de sub candele in modulul 5; si randarea lui, reparata
Cele cinci erau toate in patternuri_complexe_3: "SL 29.40" se citea cu 9.4
lipsa, "target 62.00" fara 62., "intrare short 89.00" fara 8. Reparate cu
Level.i0, care muta si linia si eticheta - label_dy doar le-ar fi lipit de
linia vecina. Nicio cifra schimbata.

Si o regresie a mea, din 29dc93d: marcajul <!--SVG0--> pe care il pune
stash_svgs semana cu ancorele <!--SVG:tb--> ale generatoarelor din modulul 5,
deci assert-ul se aprindea degeaba si **niciun capitol din modulul 5 nu se mai
putea randa**. Marcajul e acum <!--SVG-STASH-n-->, iar assert-ul cauta exact
forma lui. Cele 7 capitole se randeaza din nou.

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

580 lines
32 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.

"""Diagramele pentru summaries/06_MODUL_5/02_patternuri_complexe_partea_a_treia.md.
Construiește toate graficele cu svg_candles (o singură scară px/unitate pe
desen), verifică prin assert fiecare cifră pe care o afirmă textul, apoi
substituie marcatorii <!--SVG:nume--> din fișierul .md.
Rulează: .venv/bin/python gen/patternuri_complexe_3.py
"""
import re
import sys
sys.path.insert(0, "/workspace/atm-curs-dl")
sys.path.insert(0, "/workspace/atm-curs-dl/gen")
from svg_candles import Chart, Level, Mark, MA, Zone, from_closes # noqa: E402
from eticheta_libera import eticheta_libera # noqa: E402
MD = "/workspace/atm-curs-dl/summaries/06_MODUL_5/02_patternuri_complexe_partea_a_treia.md"
SVG = {}
def lo(cds, a, b):
return round(min(c.l for c in cds[a:b + 1]), 2)
def hi(cds, a, b):
return round(max(c.h for c in cds[a:b + 1]), 2)
def check(ch, name, distante=(), clear=(), clear_rect=()):
"""Nicio etichetă nu poate numi un preț care nu e un nivel desenat."""
svg = ch.render()
assert "\n\n" not in svg, f"{name}: linie goală în svg"
assert len(ch.caption) <= 100, f"{name}: caption de {len(ch.caption)} caractere"
desenate = ({f"{lv.price:.2f}" for lv in ch.levels}
| {f"{d:.2f}" for d in distante})
for t in re.findall(r"<text\b[^>]*>(.*?)</text>", svg, re.S):
for m in re.finditer(r"\d+\.\d\d", re.sub(r"<[^>]+>", "", t)):
assert m.group(0) in desenate, \
f"{name}: eticheta numește {m.group(0)}, care nu e un nivel desenat"
# liniile de nivel nu cad pe marginea de sus a cadrului
sus = min(ch._y(c.h) for c in ch.candles)
jos = max(ch._y(c.l) for c in ch.candles)
for lv in ch.levels:
assert ch._y(lv.price) > sus - 0.06 * (jos - sus), \
f"{name}: nivelul {lv.price} e lipit de marginea de sus"
if clear:
eticheta_libera(svg, name, set(clear))
if clear_rect: # etichete de nivel: doar peste candele e o problemă
eticheta_libera(svg, name, set(clear_rect), rects=True, segmente=False)
SVG[name] = svg
# ---------------------------------------------------------------- triple bottom
closes = ([51.2, 50.3, 49.6, 48.5, 47.9, 46.8, 45.9, 44.8] # 0-7 downtrend
+ [43.6, 42.6, 42.9] # 8-10 picior 1
+ [43.8, 44.6, 44.95] # 11-13 revenire
+ [44.0, 43.0, 42.5, 42.9] # 14-17 picior 2
+ [43.9, 44.7, 44.9] # 18-20 revenire
+ [44.1, 43.1, 42.4, 43.0] # 21-24 picior 3
+ [44.0, 45.3, 45.6] # 25-27 breakout
+ [44.6, 45.4] # 28-29 retest + intrare
+ [46.2, 47.0, 47.6, 48.1, 47.8]) # 30-34 spre target
wicks = ([(0.25, 0.3)] * 8
+ [(0.05, 0.4), (0.3, 0.60), (0.4, 0.3)]
+ [(0.3, 0.3), (0.3, 0.3), (0.05, 0.3)]
+ [(0.0, 0.3), (0.3, 0.3), (0.3, 0.45), (0.3, 0.3)]
+ [(0.3, 0.3), (0.2, 0.3), (0.10, 0.3)]
+ [(0.0, 0.3), (0.3, 0.3), (0.3, 0.40), (0.3, 0.3)]
+ [(0.2, 0.3), (0.2, 0.3), (0.3, 0.2)]
+ [(0.2, 0.35), (0.2, 0.2)]
+ [(0.2, 0.2), (0.2, 0.2), (0.2, 0.2), (0.3, 0.2), (0.2, 0.3)])
vols = ([52, 49, 46, 44, 41, 39, 44, 48]
+ [56, 64, 42] + [38, 36, 34]
+ [40, 46, 55, 38] + [33, 31, 30]
+ [36, 42, 48, 35] + [52, 96, 78] + [44, 58]
+ [66, 60, 54, 62, 48])
tb = from_closes(closes, wicks=wicks, vols=vols, start=52.0)
REZ, PICIOR = 45.00, 42.00
assert lo(tb, 8, 10) == PICIOR, lo(tb, 8, 10)
assert lo(tb, 14, 17) == 42.05, lo(tb, 14, 17)
assert lo(tb, 21, 24) == PICIOR, lo(tb, 21, 24)
assert hi(tb, 11, 13) == REZ and hi(tb, 18, 20) == REZ
assert hi(tb, 8, 24) == REZ, "rezistența nu e maximul bazei"
ADANCIME = REZ - PICIOR
TARGET = REZ + ADANCIME
RETEST_LOW = lo(tb, 28, 28)
INTRARE, SL = tb[29].c, 44.10
assert abs(ADANCIME - 3.00) < 1e-9 and abs(TARGET - 48.00) < 1e-9
assert RETEST_LOW == 44.25, RETEST_LOW
assert SL < RETEST_LOW < REZ < INTRARE, "ordinea SL / retest / rezistență / intrare"
assert SL > PICIOR, "stopul de retest trebuie să fie peste picioarele bazei"
assert tb[26].c > REZ >= tb[25].c, "breakout-ul nu e la bara 26"
assert hi(tb, 30, 34) >= TARGET, "target-ul nu e atins în serie"
VOL_BAZA = sum(vols[8:25]) / len(vols[8:25])
assert vols[26] >= 2 * VOL_BAZA, f"volumul de breakout e doar "\
f"{vols[26] / VOL_BAZA:.1f}x media bazei"
R_TB = (TARGET - INTRARE) / (INTRARE - SL)
R_TB_LARG = (TARGET - INTRARE) / (INTRARE - 41.90)
print(f"triple bottom: intrare {INTRARE:.2f} SL {SL:.2f} target {TARGET:.2f} "
f"-> {R_TB:.2f}R (stop sub bază: {R_TB_LARG:.2f}R)")
assert abs(R_TB - 2.00) < 0.01, R_TB
assert 0.7 < R_TB_LARG < 0.8, R_TB_LARG
check(Chart(
candles=tb,
levels=[Level(TARGET, f"target {TARGET:.2f} = 45.00 + 3.00", "#1a5fb4"),
Level(INTRARE, f"intrare {INTRARE:.2f}", "#188a3e", label_at="left"),
Level(REZ, f"rezistență {REZ:.2f}", "#c00"),
Level(SL, f"SL {SL:.2f}", "#c00", label_at="left"),
Level(PICIOR, f"picioarele {PICIOR:.2f}", "#b06000")],
marks=[Mark(9, "picior 1", "#b06000", "below"),
Mark(16, "picior 2", "#b06000", "below"),
Mark(23, "picior 3", "#b06000", "below"),
Mark(26, "breakout cu volum dublu", "#188a3e", dy=-24),
Mark(28, "retest", "#b06000", "below"),],
zones=[Zone(8, 24, "baza triplă", "#1a5fb4")],
mas=[MA(20, "#777", dash="4 3")],
scale=42, gap=15, body_w=9, left=46, width=620, vol_h=70, top=100,
vol_label="volum", title="Triple bottom — exemplu construit (fără ticker), daily",
caption="Baza triplă ține 17 bare. Linia gri punctată e SMA-20, pe toate graficele."),
"tb", [ADANCIME])
# ------------------------------------------------ triple bottom: fals breakout
closes_n = ([49.8, 48.9, 47.8, 46.6, 45.6, 44.7] # 0-5 downtrend
+ [43.5, 42.6, 43.0] # 6-8 picior 1
+ [43.9, 44.6, 44.9] # 9-11 revenire
+ [44.1, 43.1, 42.5, 43.0] # 12-15 picior 2
+ [43.9, 44.7, 44.9] # 16-18 revenire
+ [44.2, 43.2, 42.5, 43.1] # 19-22 picior 3
+ [44.1, 44.85] # 23-24 fals breakout
+ [44.0, 43.2, 42.4, 41.6, 41.0]) # 25-29 cade prin bază
wicks_n = ([(0.25, 0.3)] * 6
+ [(0.3, 0.4), (0.3, 0.60), (0.3, 0.3)]
+ [(0.3, 0.3), (0.3, 0.3), (0.10, 0.3)]
+ [(0.0, 0.3), (0.3, 0.3), (0.3, 0.50), (0.3, 0.3)]
+ [(0.3, 0.3), (0.3, 0.3), (0.10, 0.3)]
+ [(0.0, 0.3), (0.3, 0.3), (0.3, 0.50), (0.3, 0.3)]
+ [(0.2, 0.3), (0.23, 0.3)]
+ [(0.2, 0.3), (0.2, 0.3), (0.2, 0.3), (0.2, 0.3), (0.2, 0.4)])
vols_n = ([50, 47, 44, 42, 46, 50] + [55, 62, 40] + [36, 34, 32]
+ [38, 44, 52, 36] + [31, 30, 29] + [36, 43, 50, 34]
+ [33, 31] + [46, 54, 61, 70, 66])
tbn = from_closes(closes_n, wicks=wicks_n, vols=vols_n, start=50.4)
assert hi(tbn, 23, 24) == 45.08, hi(tbn, 23, 24)
assert tbn[24].c < REZ, "falsul breakout nu are voie să închidă peste linie"
assert lo(tbn, 6, 8) == PICIOR and lo(tbn, 19, 22) == PICIOR
assert hi(tbn, 6, 22) <= REZ, "nicio bară din bază nu trece peste rezistență"
assert lo(tbn, 25, 29) < PICIOR, "invalidarea cere un minim sub picioare"
assert max(vols_n[23:25]) < min(vols_n[6:8]), "falsul breakout vine cu volum mic"
INVALID = lo(tbn, 25, 29)
print(f"fals breakout: varf {hi(tbn, 23, 24):.2f} inchidere {tbn[24].c:.2f} "
f"invalidare {INVALID:.2f}")
check(Chart(
candles=tbn,
levels=[Level(REZ, f"rezistență {REZ:.2f}", "#c00"),
Level(45.08, "vârf 45.08", "#b06000", label_at="left"),
Level(PICIOR, f"picioarele {PICIOR:.2f}", "#b06000"),
Level(INVALID, f"{INVALID:.2f} — invalidat", "#c00", label_at="left")],
marks=[Mark(7, "picior 1", "#b06000", "below"),
Mark(14, "picior 2", "#b06000", "below"),
Mark(21, "picior 3", "#b06000", "below"),
Mark(24, "închide sub linie, volum mic", "#c00"),
Mark(28, "sub picioare: nu mai e bază triplă", "#c00", "below")],
mas=[MA(20, "#777", dash="4 3")],
scale=42, gap=15, body_w=9, left=46, width=620, vol_h=70, top=100,
vol_label="volum",
title="NU — fals breakout pe aceeași bază triplă (construit), daily",
caption="Atinge 45.08, închide sub 45.00 cu volum mic, apoi cade sub picioare."),
"tbn", [])
# -------------------------------------------------------------- rounding bottom
closes_r = ([29.8, 29.1, 28.3, 27.4] # 0-3 coboară de la rim
+ [26.6, 26.9, 26.4, 26.8] # 4-7 picior 1
+ [26.5, 26.2, 26.7, 26.9] # 8-11 picior 2
+ [26.6, 26.4, 27.0, 27.4] # 12-15 picior 3
+ [27.1, 26.9, 27.6, 28.1] # 16-19 picior 4
+ [27.8, 27.6, 28.4, 29.0] # 20-23 picior 5
+ [29.5, 29.95, 30.4] # 24-26 breakout
+ [29.7, 30.3] # 27-28 retest + intrare
+ [31.0, 31.8, 32.5, 33.2, 33.9, 34.1]) # 29-34 spre target
wicks_r = ([(0.00, 0.2), (0.2, 0.2), (0.2, 0.2), (0.2, 0.2)]
+ [(0.2, 0.2), (0.2, 0.2), (0.2, 0.30), (0.2, 0.2)]
+ [(0.2, 0.2), (0.2, 0.20), (0.2, 0.2), (0.2, 0.2)]
+ [(0.2, 0.2), (0.2, 0.25), (0.2, 0.2), (0.2, 0.2)]
+ [(0.2, 0.2), (0.2, 0.30), (0.2, 0.2), (0.2, 0.2)]
+ [(0.2, 0.2), (0.2, 0.30), (0.2, 0.2), (0.2, 0.2)]
+ [(0.2, 0.2), (0.05, 0.2), (0.2, 0.2)]
+ [(0.2, 0.15), (0.2, 0.2)]
+ [(0.2, 0.2)] * 5 + [(0.2, 0.3)])
vols_r = ([62, 54, 47, 43] + [40, 36, 38, 33] + [31, 34, 30, 28]
+ [27, 29, 28, 30] + [31, 33, 35, 37] + [39, 41, 44, 48]
+ [54, 60, 102] + [52, 68] + [74, 70, 66, 62, 70, 56])
rb = from_closes(closes_r, wicks=wicks_r, vols=vols_r, start=30.0)
RIM, FUND = 30.00, 26.00
picioare_rb = [lo(rb, 4, 7), lo(rb, 8, 11), lo(rb, 12, 15), lo(rb, 16, 19), lo(rb, 20, 23)]
assert picioare_rb == [26.10, 26.00, 26.15, 26.60, 27.30], picioare_rb
assert len(picioare_rb) == 5 and min(picioare_rb) == FUND
assert hi(rb, 0, 0) == RIM and hi(rb, 25, 25) == RIM
assert hi(rb, 0, 25) == RIM, "rim-ul nu e maximul formațiunii"
assert picioare_rb[2:] == sorted(picioare_rb[2:]), \
"picioarele castronului urcă după cel mai adânc"
AD_RB = RIM - FUND
TARGET_RB = RIM + AD_RB
RETEST_RB = lo(rb, 27, 27)
INTRARE_RB, SL_RB = rb[28].c, 29.40
assert abs(AD_RB - 4.00) < 1e-9 and abs(TARGET_RB - 34.00) < 1e-9
assert RETEST_RB == 29.55, RETEST_RB
assert SL_RB < RETEST_RB < RIM < INTRARE_RB
assert rb[26].c > RIM >= rb[25].c, "breakout-ul nu e la bara 26"
assert hi(rb, 29, 34) >= TARGET_RB
assert min(vols_r[10:16]) < min(vols_r[0:2]) and min(vols_r[10:16]) < vols_r[26], \
"volumul trebuie să fie mic în mijlocul castronului și mare la capete"
assert len(rb[4:24]) == 20, "castronul are 20 de bare"
R_RB = (TARGET_RB - INTRARE_RB) / (INTRARE_RB - SL_RB)
print(f"rounding bottom: 5 picioare {picioare_rb}, intrare {INTRARE_RB:.2f} "
f"SL {SL_RB:.2f} target {TARGET_RB:.2f} -> {R_RB:.2f}R")
assert abs(R_RB - 4.11) < 0.01, R_RB
check(Chart(
candles=rb,
levels=[Level(TARGET_RB, f"target {TARGET_RB:.2f} = 30.00 + 4.00", "#1a5fb4"),
Level(INTRARE_RB, f"intrare {INTRARE_RB:.2f}", "#188a3e", label_at="left"),
Level(RIM, f"rezistență {RIM:.2f}", "#c00"),
# i0: la marginea stângă eticheta cădea peste corpul roșu al
# barei 1 și se citea „SL 29 40" (02.10.2026); linia pornește
# linia pornește din castron, după eticheta lui: la marginea stângă
# eticheta sta pe corpul rosu al barei 1, iar mai devreme de bara 8
# intra peste caseta „castronul"
Level(SL_RB, f"SL {SL_RB:.2f}", "#c00", label_at="left", i0=8),
Level(FUND, f"fundul castronului {FUND:.2f}", "#b06000")],
marks=[Mark(6, "1", "#b06000", "below"), Mark(9, "2", "#b06000", "below"),
Mark(13, "3", "#b06000", "below"), Mark(17, "4", "#b06000", "below"),
Mark(21, "5", "#b06000", "below"),
Mark(26, "breakout", "#188a3e"),
Mark(27, "retest", "#b06000", "below")],
zones=[Zone(4, 23, "castronul", "#1a5fb4")],
mas=[MA(20, "#777", dash="4 3")],
scale=60, gap=15, body_w=9, left=46, width=620, vol_h=70, top=100,
vol_label="volum", title="Rounding bottom — exemplu construit (fără ticker), 60 min",
caption="Castronul ține 20 de bare și 5 swing low-uri. Volum mare la capete, mic la mijloc."),
"rb", [AD_RB] + picioare_rb)
# ------------------------------------------------------------------- triple top
closes_t = ([60.8, 61.9, 62.7, 63.8, 64.6, 65.4, 66.3, 67.2] # 0-7 uptrend
+ [67.9, 67.0, 66.0] # 8-10 vârf 1
+ [65.4, 66.2, 67.0, 67.85] # 11-14 vârf 2
+ [67.1, 66.2, 65.3, 66.0] # 15-18
+ [66.9, 67.8, 66.8, 65.6] # 19-22 vârf 3
+ [65.1, 64.6, 64.3] # 23-25 breakdown
+ [65.0, 64.7] # 26-27 retest + intrare
+ [64.0, 63.3, 62.6, 62.0, 61.7, 62.2]) # 28-33 spre target
wicks_t = ([(0.25, 0.25)] * 8
+ [(0.10, 0.25), (0.00, 0.25), (0.25, 0.25)]
+ [(0.25, 0.40), (0.25, 0.25), (0.25, 0.25), (0.15, 0.25)]
+ [(0.00, 0.25), (0.25, 0.25), (0.25, 0.30), (0.25, 0.25)]
+ [(0.25, 0.25), (0.20, 0.25), (0.00, 0.25), (0.25, 0.25)]
+ [(0.25, 0.25), (0.25, 0.25), (0.25, 0.25)]
+ [(0.30, 0.25), (0.25, 0.25)]
+ [(0.25, 0.25)] * 5 + [(0.25, 0.3)])
vols_t = ([40, 42, 44, 46, 45, 43, 41, 44] + [62, 48, 44] + [40, 36, 34, 52]
+ [42, 38, 44, 33] + [30, 46, 40, 44] + [58, 94, 76] + [48, 62]
+ [68, 64, 60, 66, 58, 50])
tt = from_closes(closes_t, wicks=wicks_t, vols=vols_t, start=60.0)
VARF, SUPORT = 68.00, 65.00
varfuri = [hi(tt, 8, 8), hi(tt, 14, 14), hi(tt, 20, 20)]
assert varfuri == [VARF, VARF, VARF], varfuri
assert hi(tt, 8, 22) == VARF, "vârfurile sunt maximul formațiunii"
assert lo(tt, 11, 11) == SUPORT and lo(tt, 15, 18) == SUPORT
assert lo(tt, 8, 22) == SUPORT, "suportul nu e minimul formațiunii"
INALTIME = VARF - SUPORT
TARGET_TT = SUPORT - INALTIME
RETEST_TT = hi(tt, 26, 26)
INTRARE_TT, SL_TT = tt[27].c, 65.40
assert abs(INALTIME - 3.00) < 1e-9 and abs(TARGET_TT - 62.00) < 1e-9
assert RETEST_TT == 65.30, RETEST_TT
assert INTRARE_TT < SUPORT < RETEST_TT < SL_TT, "ordinea intrare / suport / retest / SL"
assert SL_TT < VARF, "stopul de retest stă sub vârfuri"
assert tt[24].c < SUPORT <= tt[23].c, "breakdown-ul nu e la bara 24"
assert lo(tt, 28, 33) <= TARGET_TT
assert len(tt[8:23]) == 15, "vârful triplu are 15 bare"
R_TT = (INTRARE_TT - TARGET_TT) / (SL_TT - INTRARE_TT)
R_TT_LARG = (INTRARE_TT - TARGET_TT) / (VARF + 0.10 - INTRARE_TT)
print(f"triple top: intrare {INTRARE_TT:.2f} SL {SL_TT:.2f} target {TARGET_TT:.2f} "
f"-> {R_TT:.2f}R (stop peste vârfuri: {R_TT_LARG:.2f}R)")
assert abs(R_TT - 3.86) < 0.01, R_TT
assert 0.7 < R_TT_LARG < 0.8, R_TT_LARG
check(Chart(
candles=tt,
levels=[Level(VARF, f"vârfurile {VARF:.2f}", "#b06000"),
Level(SL_TT, f"SL {SL_TT:.2f}", "#c00"),
Level(SUPORT, f"suport {SUPORT:.2f}", "#c00", label_at="left"),
Level(INTRARE_TT, f"intrare short {INTRARE_TT:.2f}", "#188a3e"),
# i0: peste prima treaptă verde dispărea „2" din 62.00
Level(TARGET_TT, f"target {TARGET_TT:.2f} = 65.00 − 3.00", "#1a5fb4",
label_at="left", i0=13)],
marks=[Mark(8, "vârf 1", "#b06000", dy=-20),
Mark(14, "vârf 2", "#b06000", dy=-20),
Mark(20, "vârf 3", "#b06000", dy=-20),
Mark(24, "breakdown", "#c00", "below"),
Mark(26, "retest de dedesubt", "#b06000", dy=-34)],
mas=[MA(20, "#777", dash="4 3")],
scale=42, gap=15, body_w=9, left=46, width=620, vol_h=70, top=100,
vol_label="volum", title="Triple top — exemplu construit (fără ticker), weekly",
caption="Vârful triplu ține 15 bare, de la vârful 1 la ultima bară de sub el."),
"tt", [INALTIME])
# ------------------------------------ triple top cu vârful din mijloc mai sus
closes_h = ([61.0, 62.2, 63.3, 64.4, 65.5, 66.4, 67.0] # 0-6 uptrend
+ [67.4, 66.6, 65.6] # 7-9 vârf 1
+ [65.2, 66.4, 67.6] # 10-12
+ [68.6, 69.4, 68.0] # 13-15 vârf 2, mai sus
+ [66.8, 65.6, 65.2] # 16-18
+ [66.3, 67.5, 66.4] # 19-21 vârf 3
+ [65.6, 65.1, 64.5] # 22-24 breakdown
+ [64.0, 63.2, 62.4, 61.6, 60.9, 60.4, 60.8]) # 25-31 spre target
wicks_h = ([(0.25, 0.25)] * 7
+ [(0.10, 0.25), (0.00, 0.25), (0.25, 0.25)]
+ [(0.25, 0.20), (0.25, 0.20), (0.25, 0.25)]
+ [(0.25, 0.25), (0.10, 0.25), (0.00, 0.25)]
+ [(0.25, 0.25), (0.25, 0.25), (0.25, 0.20)]
+ [(0.25, 0.20), (0.10, 0.25), (0.00, 0.25)]
+ [(0.25, 0.25)] * 3
+ [(0.25, 0.25)] * 6 + [(0.25, 0.3)])
vols_h = ([40, 42, 44, 46, 48, 46, 44] + [60, 46, 42] + [38, 36, 44]
+ [52, 86, 58] + [44, 40, 36] + [34, 48, 42] + [50, 62, 90]
+ [74, 70, 66, 62, 58, 64, 52])
th = from_closes(closes_h, wicks=wicks_h, vols=vols_h, start=60.0)
V1, V2, V3 = hi(th, 7, 7), hi(th, 13, 14), hi(th, 19, 20)
assert (V1, V2, V3) == (67.50, 69.50, 67.60), (V1, V2, V3)
assert V2 > V1 and V2 > V3, "vârful din mijloc trebuie să fie cel mai sus"
assert hi(th, 7, 21) == V2, "capul trebuie să fie maximul formațiunii"
assert lo(th, 10, 10) == SUPORT and lo(th, 18, 18) == SUPORT
assert lo(th, 7, 21) == SUPORT, "suportul nu e minimul formațiunii"
AD_H = V2 - SUPORT
TARGET_H = SUPORT - AD_H
assert abs(AD_H - 4.50) < 1e-9 and abs(TARGET_H - 60.50) < 1e-9
assert lo(th, 25, 31) <= TARGET_H, lo(th, 25, 31)
assert th[24].c < SUPORT <= th[23].c, "breakdown-ul nu e la bara 24"
print(f"triple top varianta H&S: varfuri {V1:.2f}/{V2:.2f}/{V3:.2f}, "
f"masurat din {V2:.2f} -> target {TARGET_H:.2f}")
check(Chart(
candles=th,
levels=[Level(V2, f"vârful 2 (capul) {V2:.2f}", "#b06000"),
Level(V1, f"umerii ≈ {V1:.2f}", "#b06000", label_at="left"),
Level(SUPORT, f"suport (neckline) {SUPORT:.2f}", "#c00", label_at="left"),
# în dreapta, eticheta cădea peste ultimele corpuri roșii și se
# citea „target 6 0 = …"; trece la stânga, după prima bară
Level(TARGET_H, f"target {TARGET_H:.2f} = 65.00 − 4.50", "#1a5fb4",
label_at="left", i0=1)],
marks=[Mark(7, "vârf 1", "#b06000"), Mark(14, "vârf 2 — cel mai sus", "#b06000", dy=-20),
Mark(20, "vârf 3", "#b06000"),
Mark(24, "breakdown", "#c00", "below")],
mas=[MA(20, "#777", dash="4 3")],
scale=42, gap=15, body_w=9, left=46, width=620, vol_h=70, top=100,
vol_label="volum",
title="Triple top cu vârful din mijloc mai sus — construit (fără ticker), weekly",
caption="Înălțimea se măsoară de la vârful 2, nu de la cele laterale."),
"th", [AD_H])
# --------------------------------------------------------------- cup with handle
closes_c = ([97.0, 101.0, 106.0, 111.0, 116.0, 119.5] # 0-5 uptrend + rim 1
+ [116.0, 110.0, 103.0, 96.0, 89.0, 83.0] # 6-11 coboară ceașca
+ [80.5, 82.0, 81.0, 85.0] # 12-15 fundul ceștii
+ [89.0, 94.0, 99.0, 104.0, 109.0, 113.0, 117.0, 119.6] # 16-23 urcă + rim 2
+ [116.0, 112.0, 108.5, 112.0] # 24-27 mânerul
+ [118.0, 121.0] # 28-29 breakout, intrare
+ [128.0, 136.0, 143.0, 150.0, 157.0, 160.5]) # 30-35 spre target
wicks_c = ([(1.0, 1.0)] * 5 + [(0.50, 1.0)]
+ [(0.00, 1.0)] + [(1.0, 1.0)] * 5
+ [(1.0, 0.50), (1.0, 0.50), (1.0, 0.80), (1.0, 1.0)]
+ [(1.0, 1.0)] * 7 + [(0.40, 1.0)]
+ [(0.00, 1.0), (1.0, 1.0), (1.0, 0.50), (1.0, 0.50)]
+ [(1.0, 1.0), (1.0, 1.0)]
+ [(1.0, 1.0)] * 5 + [(1.0, 1.5)])
vols_c = ([44, 48, 52, 50, 54, 58] + [62, 68, 74, 70, 64, 56]
+ [50, 44, 42, 46] + [48, 52, 54, 50, 52, 56, 60, 64]
+ [40, 34, 30, 36] + [58, 104]
+ [92, 84, 78, 72, 70, 60])
cu = from_closes(closes_c, wicks=wicks_c, vols=vols_c, start=95.0)
RIM_C, FUND_C = 120.00, 80.00
assert hi(cu, 5, 5) == RIM_C and hi(cu, 23, 23) == RIM_C, (hi(cu, 5, 5), hi(cu, 23, 23))
assert hi(cu, 0, 23) == RIM_C, "cele două rim-uri sunt maximul ceștii"
assert lo(cu, 12, 15) == FUND_C, lo(cu, 12, 15)
AD_C = RIM_C - FUND_C
TARGET_C = RIM_C + AD_C
MANER = lo(cu, 24, 27)
PROC_MANER = (RIM_C - MANER) / AD_C
LIMITA50 = RIM_C - 0.5 * AD_C
INTRARE_C, SL_C = cu[29].c, 107.00
assert abs(AD_C - 40.00) < 1e-9 and abs(TARGET_C - 160.00) < 1e-9
assert MANER == 108.00, MANER
assert abs(LIMITA50 - 100.00) < 1e-9
assert MANER > LIMITA50, "mânerul valid nu coboară sub jumătatea ceștii"
assert abs(PROC_MANER - 0.30) < 1e-9, PROC_MANER
assert SL_C < MANER < RIM_C < INTRARE_C
assert cu[29].c > RIM_C >= cu[28].c, "breakout-ul nu e la bara 29"
assert hi(cu, 30, 35) >= TARGET_C
assert len(cu[24:28]) == 4, "mânerul are 4 bare"
R_C = (TARGET_C - INTRARE_C) / (INTRARE_C - SL_C)
R_C_LIMITA = (TARGET_C - INTRARE_C) / (INTRARE_C - (LIMITA50 - 1.00))
print(f"cup&handle: maner {MANER:.2f} ({PROC_MANER:.0%}), intrare {INTRARE_C:.2f} "
f"SL {SL_C:.2f} target {TARGET_C:.2f} -> {R_C:.2f}R "
f"(maner la limita de 50%: {R_C_LIMITA:.2f}R)")
assert abs(R_C - 2.79) < 0.01, R_C
assert abs(R_C_LIMITA - 1.77) < 0.01, R_C_LIMITA
check(Chart(
candles=cu,
levels=[Level(TARGET_C, f"target {TARGET_C:.2f} = 120.00 + 40.00", "#1a5fb4"),
Level(INTRARE_C, f"intrare {INTRARE_C:.2f}", "#188a3e", label_at="left"),
Level(RIM_C, f"rezistență {RIM_C:.2f}", "#c00"),
Level(MANER, f"minimul mânerului {MANER:.2f}", "#b06000"),
Level(SL_C, f"SL {SL_C:.2f}", "#c00", label_at="left"),
Level(LIMITA50, f"limita de 50% {LIMITA50:.2f}", "#8b5cf6"),
Level(FUND_C, f"fundul ceștii {FUND_C:.2f}", "#b06000", label_at="left")],
marks=[Mark(5, "top 1", "#c00"), Mark(23, "top 2", "#c00", dy=-18),
Mark(13, "fundul ceștii", "#b06000", "below"),
Mark(29, "breakout", "#188a3e", dy=-68)],
zones=[Zone(24, 27, "mânerul", "#b06000")],
mas=[MA(20, "#777", dash="4 3")],
scale=4.2, gap=15, body_w=9, left=46, width=700, vol_h=70, top=100,
vol_label="volum", title="Cup with handle — exemplu construit (fără ticker), monthly",
caption="Mânerul: 4 bare, −30% din adâncime. Două top-uri la 120.00 = double top. Gri punctat: SMA-20."),
"cu", [AD_C, RIM_C - MANER],
clear=("breakout",), clear_rect=(f"minimul mânerului {MANER:.2f}", "breakout"))
# ----------------------------------------- cup with handle: mâner prea adânc
closes_cn = ([99.0, 105.0, 111.0, 116.0, 119.5] # 0-4 uptrend + rim 1
+ [115.0, 108.0, 100.0, 92.0, 84.0] # 5-9 coboară
+ [80.6, 83.0, 86.0] # 10-12 fundul
+ [91.0, 97.0, 103.0, 109.0, 114.0, 118.0, 119.6] # 13-19 urcă + rim 2
+ [114.0, 108.0, 102.0, 96.5, 99.0] # 20-24 mâner prea adânc
+ [97.0, 94.0, 90.0, 93.0, 91.0]) # 25-29 recade în ceașcă
wicks_cn = ([(1.0, 1.0)] * 4 + [(0.50, 1.0)]
+ [(0.00, 1.0)] + [(1.0, 1.0)] * 4
+ [(1.0, 0.60), (1.0, 0.60), (1.0, 1.0)]
+ [(1.0, 1.0)] * 6 + [(0.40, 1.0)]
+ [(0.00, 1.0), (1.0, 1.0), (1.0, 1.0), (1.0, 0.50), (1.0, 0.50)]
+ [(1.0, 1.0)] * 4 + [(1.0, 1.5)])
vols_cn = ([46, 50, 54, 52, 56] + [60, 66, 72, 68, 62]
+ [52, 46, 48] + [50, 52, 54, 52, 54, 58, 62]
+ [56, 62, 70, 76, 48] + [54, 62, 70, 58, 64])
cn = from_closes(closes_cn, wicks=wicks_cn, vols=vols_cn, start=95.0)
assert hi(cn, 4, 4) == RIM_C and hi(cn, 19, 19) == RIM_C
assert lo(cn, 10, 12) == FUND_C, lo(cn, 10, 12)
MANER_N = lo(cn, 20, 24)
PROC_N = (RIM_C - MANER_N) / AD_C
assert MANER_N == 96.00, MANER_N
assert abs(PROC_N - 0.60) < 1e-9, PROC_N
assert MANER_N < LIMITA50, "mânerul invalid trebuie să fie sub limita de 50%"
assert hi(cn, 20, 29) < RIM_C, "în varianta NU nu există breakout"
print(f"cup&handle NU: maner {MANER_N:.2f} = {PROC_N:.0%} din adancime")
check(Chart(
candles=cn,
levels=[Level(RIM_C, f"rezistență {RIM_C:.2f}", "#c00"),
Level(LIMITA50, f"limita de 50% {LIMITA50:.2f}", "#8b5cf6", label_at="left"),
Level(MANER_N, f"minimul mânerului {MANER_N:.2f}", "#c00"),
Level(FUND_C, f"fundul ceștii {FUND_C:.2f}", "#b06000", label_at="left")],
marks=[Mark(4, "top 1", "#c00", dy=-20), Mark(19, "top 2", "#c00", dy=-20),
Mark(11, "fundul ceștii", "#b06000", "below"),
Mark(23, "rupe 100.00: −60%", "#c00", "below"),
Mark(27, "fără breakout, prețul stă în ceașcă", "#c00", "below")],
mas=[MA(20, "#777", dash="4 3")],
scale=4.2, gap=15, body_w=9, left=46, width=620, vol_h=70, top=100,
vol_label="volum",
title="NU — mâner mai adânc de 50% (construit, fără ticker), monthly",
caption="Mânerul coboară la 96.00, sub limita de 50% desenată la 100.00."),
"cn", [AD_C, RIM_C - MANER_N])
# ------------------------------------- varianta bear: inverted cup with handle
closes_b = ([101.0, 98.0, 95.0, 92.0, 90.4] # 0-4 downtrend + rim 1
+ [94.0, 100.0, 106.0, 112.0, 117.0, 119.5] # 5-10 urcă cupola
+ [119.8, 117.0, 113.0] # 11-13 vârful cupolei
+ [108.0, 103.0, 98.0, 95.0, 93.0, 91.5, 90.6] # 14-20 coboară + rim 2
+ [93.0, 96.0, 97.6, 94.0] # 21-24 mâner inversat
+ [92.0, 89.0] # 25-26 breakdown, intrare
+ [83.0, 77.0, 72.0, 67.0, 63.0, 60.0, 61.0]) # 27-33 spre target
wicks_b = ([(0.8, 0.8)] * 4 + [(0.8, 0.40)]
+ [(0.8, 0.00)] + [(0.8, 0.8)] * 4 + [(0.50, 0.8)]
+ [(0.20, 0.8), (0.00, 0.8), (0.8, 0.8)]
+ [(0.8, 0.8)] * 6 + [(0.8, 0.60)]
+ [(0.8, 0.00), (0.8, 0.8), (0.40, 0.8), (0.40, 0.8)]
+ [(0.8, 0.8), (0.8, 0.8)]
+ [(0.8, 0.8)] * 6 + [(0.8, 0.8)])
vols_b = ([48, 52, 56, 54, 58] + [52, 50, 48, 46, 44, 42]
+ [56, 50, 48] + [52, 56, 60, 58, 54, 50, 56]
+ [36, 32, 30, 38] + [62, 108]
+ [96, 88, 82, 76, 72, 70, 58])
cb = from_closes(closes_b, wicks=wicks_b, vols=vols_b, start=103.0)
RIM_B, CUPOLA = 90.00, 120.00
assert lo(cb, 4, 4) == RIM_B and lo(cb, 20, 20) == RIM_B, (lo(cb, 4, 4), lo(cb, 20, 20))
assert lo(cb, 0, 20) == RIM_B, "cele două rim-uri sunt minimul cupolei"
assert hi(cb, 10, 13) == CUPOLA, hi(cb, 10, 13)
AD_B = CUPOLA - RIM_B
TARGET_B = RIM_B - AD_B
MANER_B = hi(cb, 21, 24)
PROC_B = (MANER_B - RIM_B) / AD_B
LIMITA50_B = RIM_B + 0.5 * AD_B
INTRARE_B, SL_B = cb[26].c, 99.00
assert abs(AD_B - 30.00) < 1e-9 and abs(TARGET_B - 60.00) < 1e-9
assert MANER_B == 98.00, MANER_B
assert abs(LIMITA50_B - 105.00) < 1e-9
assert MANER_B < LIMITA50_B, "mânerul inversat valid nu urcă peste jumătatea cupolei"
assert INTRARE_B < RIM_B < MANER_B < SL_B
assert cb[26].c < RIM_B <= cb[25].c, "breakdown-ul nu e la bara 26"
assert lo(cb, 27, 33) <= TARGET_B
assert len(cb[21:25]) == 4, "mânerul inversat are 4 bare"
R_B = (INTRARE_B - TARGET_B) / (SL_B - INTRARE_B)
print(f"cup&handle bear: cupola {CUPOLA:.2f} rim {RIM_B:.2f} maner {MANER_B:.2f} "
f"({PROC_B:.0%}), intrare {INTRARE_B:.2f} SL {SL_B:.2f} target {TARGET_B:.2f} "
f"-> {R_B:.2f}R")
assert abs(R_B - 2.90) < 0.01, R_B
assert abs(PROC_B - 0.2667) < 0.001, PROC_B
check(Chart(
candles=cb,
levels=[Level(CUPOLA, f"vârful cupolei {CUPOLA:.2f}", "#b06000"),
Level(SL_B, f"SL {SL_B:.2f}", "#c00"),
Level(LIMITA50_B, f"limita de 50% {LIMITA50_B:.2f}", "#777", label_at="left"),
Level(RIM_B, f"suport {RIM_B:.2f}", "#c00"),
# i0: peste corpurile din steag se pierdea „8" din 89.00
Level(INTRARE_B, f"intrare short {INTRARE_B:.2f}", "#188a3e",
label_at="left", i0=6),
# idem, în dreapta dispărea „90" sub ultimele corpuri
Level(TARGET_B, f"target {TARGET_B:.2f} = 90.00 − 30.00", "#1a5fb4",
label_at="left")],
marks=[Mark(4, "fund 1", "#c00", "below"), Mark(20, "fund 2", "#c00", "below"),
Mark(11, "vârful cupolei", "#b06000", dy=-20),
Mark(26, "breakdown", "#c00", "below")],
zones=[Zone(21, 24, "mânerul inversat", "#b06000")],
mas=[MA(20, "#777", dash="4 3")],
scale=4.2, gap=15, body_w=9, left=46, width=620, vol_h=70, top=100,
vol_label="volum",
title="Inverted cup with handle / rounding top — construit (fără ticker), monthly",
caption="Mânerul inversat: 4 bare, +27%. Cele două funduri la 90.00 = double bottom."),
"cb", [AD_B])
# --------------------------------------------- aceleași reguli între diagrame
# stopul stă dincolo de bara de retest (triple bottom / rounding bottom / triple
# top) sau dincolo de mâner (cup with handle, în ambele direcții)
assert SL < RETEST_LOW and SL_RB < RETEST_RB and SL_TT > RETEST_TT, \
"regula de stop pe retest nu e aceeași în toate diagramele"
assert SL_C < MANER and SL_B > MANER_B, "regula de stop pe mâner nu e simetrică"
# target = adâncimea/înălțimea proiectată de la linia spartă, în toate diagramele
for linie, adanc, target, semn in ((REZ, ADANCIME, TARGET, 1), (RIM, AD_RB, TARGET_RB, 1),
(SUPORT, INALTIME, TARGET_TT, -1),
(SUPORT, AD_H, TARGET_H, -1),
(RIM_C, AD_C, TARGET_C, 1),
(RIM_B, AD_B, TARGET_B, -1)):
assert abs(target - (linie + semn * adanc)) < 1e-9, (linie, adanc, target)
# limita de 50% a mânerului, la fel pe long și pe short
assert abs((RIM_C - LIMITA50) / AD_C - 0.5) < 1e-9
assert abs((LIMITA50_B - RIM_B) / AD_B - 0.5) < 1e-9
# ---------------------------------------------------------------- substituire
with open(MD, encoding="utf-8") as f:
md = f.read()
lipsa = [n for n in SVG if f"<!--SVG:{n}-->" not in md] # marcatori obligatorii
assert not lipsa, f"marcatori lipsă în .md: {lipsa}"
for n, svg in SVG.items():
# marcatorul rămâne în fișier, ca să se poată rula generatorul din nou
md = re.sub(rf"<!--SVG:{n}-->(\s*<svg\b[\s\S]*?</svg>)?",
lambda _m: f"<!--SVG:{n}-->\n{svg}", md, count=1)
with open(MD, "w", encoding="utf-8") as f:
f.write(md)
print(f"\n{len(SVG)} diagrame substituite in {MD}")