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
580 lines
32 KiB
Python
580 lines
32 KiB
Python
"""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}")
|