Files
atm-curs-dl/gen/az_structura.py
Claude Agent b805d66c23 06 structura: exemplul de triple doji contrazicea regula lui; plus 6 defecte
Verificare independenta fata de harvest/ si transcrieri. Niciun punct
contestat.

Conținut:
- citat inventat, scos: "un wedge in compresie cere volum pentru breakout
  masiv" nu exista in transcriere (verificat cu grep, 0 apariții acum). El
  spune ca vrea volum mai mare ("as vad sa vad si volumul mai tare"), iar
  breakout-ul masiv il leaga de reintrarea in MBT-10. Nu formuleaza nicio
  regula. Acelasi citat fals era si in 04, corectat acolo.
- exemplul de triple doji contrazicea regula lui: avea volum de vanzare in
  crestere, cand el cere acumulare si divergenta bull de volum (19.08).
  Contrazicea si propriul tabel DA/NU de dedesubt, si documentul 05.
  Reparat DESENUL, nu textul: volumul scade sub low-ul anterior (divergenta
  bull), scade pe cele trei doji (epuizare), trigger-ul are volumul maxim
  din serie, iar pe urcare barele verzi au mai mult volum decat cele rosii.
  Trei assert-uri noi pe relatiile astea. Tabelul DA/NU a rămas neschimbat
  si acum se potriveste.
- trei atribuiri "din curs" erau de fapt din analiza zilnica, verificate
  fisier cu fisier in module: conditia McClellan pentru double top (19.08),
  inverted H&S pe 60 min ca trigger (25.08, 18.08), si RSP + McClellan +
  stochastic (17.08, 19.08). In module McClellan apare doar ca nume. A
  patra trimitere, cea catre modulele 2, 3 si 4, e corecta si a rămas.
- doua caption-uri contraziceau desenul ("volumul e cel mai mic din serie",
  "cel mai mic volum de la fundul cestii" - de fapt egalitate); corectate
  dupa ce arata desenul, unde assert-ul exista deja.
- 44.50 era numit marginea de sus a gap-ului 44.50-44.90, deci de jos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtSTyTmt6AbCL9j5ajEDm1
2026-09-14 06:33:12 +00:00

897 lines
51 KiB
Python

"""Diagramele pentru summaries/08_ANALIZA_ZILNICA/06_structura_trend.md.
Toate prețurile sunt construite (fără ticker). Rulează:
.venv/bin/python gen/az_structura.py
Înlocuiește în .md fiecare bloc <!--SVG:nume--> ... <!--/SVG--> cu output-ul
generatorului. Ca să schimbi o cifră, o schimbi aici și rulezi din nou.
Liniile de trend (wedge, triunghi) se calculează din high-urile / low-urile
candelelor și se inserează în SVG cu aceleași funcții de coordonate ale
graficului (ch._cx, ch._y) — nu sunt desenate de mână.
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402
MD = Path("/workspace/atm-curs-dl/summaries/08_ANALIZA_ZILNICA/06_structura_trend.md")
GRAY, RED, GREEN, BLUE, ORANGE, INK = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000", "#333"
STYLE = dict(gap=15, body_w=9, left=44, width=620)
svgs = {}
def wk(n, over=None, base=(0.3, 0.3)):
"""wick-uri: `base` peste tot, cu excepțiile din `over` {index: (sus, jos)}"""
w = [base] * n
for i, v in (over or {}).items():
w[i] = v
return w
def px_ratio(ch, a, b, c):
"""raportul (c-b)/(b-a) măsurat în pixeli, după render"""
return (ch._y(b) - ch._y(c)) / (ch._y(a) - ch._y(b))
def is_sl(cds, i):
return cds[i].l < cds[i - 1].l and cds[i].l < cds[i + 1].l
def is_sh(cds, i):
return cds[i].h > cds[i - 1].h and cds[i].h > cds[i + 1].h
def doji(c):
return abs(c.o - c.c) <= 0.15 and (c.h - c.l) >= 3 * abs(c.o - c.c)
def line_at(i1, p1, i2, p2, i):
return p1 + (p2 - p1) * (i - i1) / (i2 - i1)
def with_lines(ch, svg, lines):
"""lines: (i1, p1, i2, p2, i_end, culoare) — linie de trend prin două puncte, prelungită până la i_end"""
out = []
for i1, p1, i2, p2, ie, col in lines:
pe = line_at(i1, p1, i2, p2, ie)
out.append(f'<line x1="{ch._cx(i1):.1f}" y1="{ch._y(p1):.1f}" x2="{ch._cx(ie):.1f}" '
f'y2="{ch._y(pe):.1f}" stroke="{col}" stroke-width="1.6"/>')
return svg.replace("</text>", "</text>\n " + "\n ".join(out), 1)
def add(svg, elems):
"""elemente SVG în plus, desenate peste tot restul"""
return svg.replace("\n</svg>", "\n " + "\n ".join(elems) + "\n</svg>")
def lbl_below(ch, price, text, color, at="right"):
"""eticheta unui Level, SUB linie (când deasupra ar tăia o linie apropiată); nivelul se dă cu text gol"""
x, anc = (ch.width - 28, "end") if at == "right" else (30, "start")
return f'<text x="{x}" y="{ch._y(price) + 14:.1f}" fill="{color}" text-anchor="{anc}">{text}</text>'
def callout(ch, i, price, text, color, anchor="middle", below=False):
"""text la prețul `price` deasupra (sau sub) barei i, cu o linie subțire până la bară"""
c = ch.candles[i]
x, ty = ch._cx(i), ch._y(price)
tx = x - 4 if anchor == "start" else (x + 4 if anchor == "end" else x)
y1, y2 = (ty - 13, ch._y(c.l) + 3) if below else (ty + 3, ch._y(c.h) - 3)
return (f'<line x1="{x:.1f}" y1="{y1:.1f}" x2="{x:.1f}" y2="{y2:.1f}" stroke="{color}" stroke-width="1"/>\n '
f'<text x="{tx:.1f}" y="{ty:.1f}" fill="{color}" text-anchor="{anchor}" '
f'style="font-weight:bold">{text}</text>')
def dy_line(c, scale, p_line, off, where):
"""dy pentru un Mark pe candela c, ca baseline-ul textului să stea la `off` px de prețul p_line
(off<0 = deasupra). Diferențele de y depind doar de scară, deci merge înainte de render."""
if where == "above":
return (c.h - p_line) * scale + off + 10
return (c.l - p_line) * scale + off - 20
def first_hit_up(cds, start, price):
return next(i for i in range(start, len(cds)) if cds[i].h >= price - 1e-9)
def first_hit_down(cds, start, price):
return next(i for i in range(start, len(cds)) if cds[i].l <= price + 1e-9)
# =============================================================== 1. scărița + confirmarea swing low-ului
stair = [40.0, 40.9, 41.6, 42.4, # 0-3 urcare, SH la 3
41.9, 41.3, 41.0, # 4-6 pullback, SL la 6
41.8, 42.7, 43.5, 44.1, # 7-10 HH la 10
43.6, 43.0, 42.6, # 11-13 HL la 13
43.4, 44.3, 45.2, 45.8, # 14-17 HH la 17
45.3, 44.7, 44.3, # 18-20 HL la 20
45.1, 46.0, 46.9, 47.5, # 21-24 HH la 24
47.0, 46.4, 46.0] # 25-27 bara 27 = low „în formare"
SH_I, SL_I = [3, 10, 17, 24], [6, 13, 20, 27]
stair_w = {i: (0.7, 0.3) for i in SH_I} | {i: (0.3, 0.7) for i in SL_I}
cl = stair + [47.0, # 28 candela din dreapta: confirmă
47.6, 48.3, 48.0, 48.9, 49.6, 49.3, 50.1, 50.6]
cds = from_closes(cl, wicks=wk(len(cl), stair_w | {28: (0.2, 0.3)}), start=39.6)
for i in SH_I:
assert is_sh(cds, i), f"bara {i} nu e swing high"
for i in SL_I:
assert is_sl(cds, i), f"bara {i} nu e swing low"
highs = [cds[i].h for i in SH_I]
lows = [cds[i].l for i in SL_I]
assert all(b > a for a, b in zip(highs, highs[1:])), "vârfurile nu sunt higher highs"
assert all(b > a for a, b in zip(lows, lows[1:])), "minimele nu sunt higher lows"
HL = 27
assert cds[HL + 1].c > cds[HL].h and cds[HL + 1].l > cds[HL].l, "candela din dreapta nu confirmă"
entry = cds[HL + 1].c
sl = round(cds[HL].l - 0.1, 2)
step = cds[24].h - cds[20].l
tp = round(cds[HL].l + step, 2)
risk = entry - sl
R = (tp - entry) / risk
hit = first_hit_up(cds, HL + 2, tp)
assert R >= 1.5, R
print(f"S1 HH {[round(x, 2) for x in highs]} HL {[round(x, 2) for x in lows]} entry {entry:.2f} SL {sl:.2f} "
f"treaptă {step:.2f} TP {tp:.2f} = {R:.2f}R atins la {hit} ({hit - HL - 1} candele)")
ch = Chart(candles=cds,
levels=[Level(sl, f"SL {sl:.2f}", RED),
Level(entry, f"intrare {entry:.2f}", GREEN, label_at="left"),
Level(tp, f"TP {tp:.2f} = {R:.1f}R", BLUE, label_at="left")],
marks=[Mark(3, "SH", INK, "above"), Mark(6, "SL", INK, "below"),
Mark(10, "HH", GREEN, "above"), Mark(13, "HL", GREEN, "below"),
Mark(17, "HH", GREEN, "above", dy=-14), Mark(20, "HL", GREEN, "below", dy=12),
Mark(24, "HH", GREEN, "above"), Mark(HL, "HL", GREEN, "below", dy=14)],
mas=[MA(20, ORANGE, dash="4 3")],
scale=24, title="Exemplu construit (fără ticker) — daily",
caption="Linia punctată = SMA-20. Bara 27 devine higher low abia când bara 28 închide peste high-ul ei.",
**{**STYLE, "width": 700})
svg_ = add(ch.render(), [callout(ch, HL + 1, 49.0, "confirmare", GREEN)])
assert abs(px_ratio(ch, sl, entry, tp) - R) < 0.01
svgs["scarita_da"] = svg_
S1 = dict(entry=entry, sl=sl, tp=tp, hl=cds[HL].l, hl20=cds[20].l)
# =============================================================== 2. scărița NU: candela din dreapta sparge
cl = stair + [45.2, # 28 low sub low-ul barei 27
44.6, 45.0, 44.1, 43.4, 43.8, 42.9, 42.5]
cds = from_closes(cl, wicks=wk(len(cl), stair_w), start=39.6)
last_hl = cds[20].l
assert cds[28].l < cds[27].l, "bara 28 nu sparge low-ul barei 27"
assert not is_sl(cds, 27), "bara 27 n-ar trebui să mai fie swing low"
brk = next(i for i in range(28, len(cds)) if cds[i].c < last_hl)
print(f"S2 low 27 {cds[27].l:.2f}, bara 28 low {cds[28].l:.2f} close {cds[28].c:.2f}; "
f"ultimul HL confirmat {last_hl:.2f} spart la close {cds[brk].c:.2f} (bara {brk})")
ch = Chart(candles=cds,
levels=[Level(cds[27].l, f"HL? {cds[27].l:.2f} (în formare)", GRAY),
Level(last_hl, f"ultimul HL confirmat {last_hl:.2f}", RED, label_at="left")],
marks=[Mark(24, "HH", GREEN, "above"), Mark(20, "HL", GREEN, "below"),
Mark(brk, "lower low", RED, "below")],
scale=24, title="Exemplu construit (fără ticker) — daily",
caption="Candela din dreapta închide sub low: nu există higher low. Sub 43.60 scărița e ruptă.",
**{**STYLE, "width": 700})
svgs["scarita_nu"] = add(ch.render(), [callout(ch, 27, 47.2, "HL în formare?", GRAY, anchor="start")])
S2 = dict(low27=cds[27].l, c28=cds[28].c, last_hl=last_hl, brk_c=cds[brk].c)
# =============================================================== 3. double bottom, al doilea picior mai sus
down = [38.0, 37.2, 37.6, 36.5, 35.6, 35.9, 34.8, 33.9, 34.2, 33.0, 32.1, 31.4] # 0-11
bounce = [31.4, 32.1, 31.8, 32.5, 32.9, 32.3, 31.7] # 13-19
cl = down + [30.6] + bounce + [31.2, # 20 picior 2
32.1, # 21 confirmare
32.6, 32.3, 33.8, 34.3, 34.0, 34.9, 35.6, 35.3, 36.2, 36.8]
cds = from_closes(cl, wicks=wk(len(cl), {12: (0.3, 0.6), 17: (0.4, 0.3), 20: (0.3, 0.4), 21: (0.2, 0.3)}),
start=38.6)
L1, L2, CF = 12, 20, 21
neck = max(c.h for c in cds[L1 + 1:L2])
assert neck == cds[17].h
assert is_sl(cds, L1) and is_sl(cds, L2)
assert cds[L2].l > cds[L1].l, "al doilea picior trebuie să fie mai sus"
assert cds[CF].c > cds[L2].h and cds[CF].l > cds[L2].l
entry = cds[CF].c
sl = round(cds[L2].l - 0.1, 2)
risk = entry - sl
tp = round(neck + (neck - cds[L1].l), 2)
R = (tp - entry) / risk
bo = next(i for i in range(CF, len(cds)) if cds[i].c > neck)
hit = first_hit_up(cds, CF + 1, tp)
assert hit == len(cds) - 1 and R >= 2.5
print(f"S3 picior1 {cds[L1].l:.2f} picior2 {cds[L2].l:.2f} neck {neck:.2f} entry {entry:.2f} SL {sl:.2f} "
f"TP {tp:.2f} = {R:.2f}R; breakout la {bo} close {cds[bo].c:.2f}; TP atins la {hit}")
ch = Chart(candles=cds,
levels=[Level(neck, f"neckline {neck:.2f}", ORANGE, label_at="left"),
Level(sl, f"SL {sl:.2f}", RED),
Level(entry, f"intrare {entry:.2f}", GREEN),
Level(tp, f"TP {tp:.2f} = {R:.1f}R", BLUE)],
marks=[Mark(L1, f"picior 1: {cds[L1].l:.2f}", RED, "below"),
Mark(L2, f"picior 2: {cds[L2].l:.2f} (mai sus)", GREEN, "below", dy=12),
Mark(bo, "breakout", GREEN, "above", dy=-4)],
scale=26, title="Exemplu construit (fără ticker) — daily",
caption="Intrare la confirmarea piciorului 2; ținta = neckline + înălțimea pattern-ului.",
**{**STYLE, "width": 700})
svg_ = ch.render()
assert abs(px_ratio(ch, sl, entry, tp) - R) < 0.01
svgs["db_mai_sus"] = svg_
S3 = dict(l1=cds[L1].l, l2=cds[L2].l, neck=neck, entry=entry, sl=sl, tp=tp, R=R, bo_c=cds[bo].c)
# =============================================================== 4. double bottom, al doilea picior mai jos (stop hunt)
cl = down + [30.6] + bounce + [31.4 - 0.8, # 20 coborâre
30.3, # 21 picior 2: hammer sub picior 1
31.5, # 22 trigger
32.2, 31.9, 33.0, 33.9, 34.4, 34.1, 35.2, 36.0, 35.6, 36.9]
vols = [52, 50, 44, 55, 58, 46, 60, 62, 48, 64, 66, 68, 74,
46, 42, 38, 40, 44, 40, 46, 50, 52, 70, 60, 48, 62, 72, 58, 46, 60, 58, 44, 56]
cds = from_closes(cl, wicks=wk(len(cl), {12: (0.3, 0.6), 17: (0.4, 0.3), 21: (0.3, 0.9), 22: (0.2, 0.3)}),
vols=vols, start=38.6)
L1, L2, TR = 12, 21, 22
neck = max(c.h for c in cds[L1 + 1:L2])
leg1, leg2 = cds[L1].l, cds[L2].l
assert is_sl(cds, L1) and is_sl(cds, L2)
assert leg2 < leg1, "al doilea picior trebuie să fie mai jos"
body = abs(cds[L2].c - cds[L2].o)
assert min(cds[L2].o, cds[L2].c) - cds[L2].l >= 2.5 * body, "piciorul 2 nu e hammer"
assert vols[L2] < vols[L1] * 0.75, "volumul pe piciorul 2 trebuie să fie clar mai mic"
assert cds[TR].c > cds[L2].h and cds[TR].c > leg1, "trigger-ul nu închide peste high-ul hammer-ului"
entry = cds[TR].c
sl = round(leg2 - 0.1, 2)
risk = entry - sl
tp = round(neck + (neck - leg2), 2)
R = (tp - entry) / risk
hit = first_hit_up(cds, TR + 1, tp)
assert hit == len(cds) - 1 and R >= 2.2
print(f"S4 picior1 {leg1:.2f} (vol {vols[L1]}) picior2 {leg2:.2f} (vol {vols[L2]}) neck {neck:.2f} "
f"entry {entry:.2f} SL {sl:.2f} TP {tp:.2f} = {R:.2f}R atins la {hit}")
ch = Chart(candles=cds,
levels=[Level(leg1, f"picior 1 {leg1:.2f}", GRAY, label_at="left"),
Level(sl, f"SL {sl:.2f}", RED),
Level(entry, f"intrare {entry:.2f}", GREEN),
Level(tp, f"TP {tp:.2f} = {R:.1f}R", BLUE)],
marks=[Mark(L1, "picior 1", RED, "below", dy=8),
Mark(L2, "picior 2: stop-uri luate", RED, "below", dy=2),
Mark(TR, "trigger", GREEN, "above", dy=-30),
Mark(17, f"neckline {neck:.2f}", ORANGE, "above")],
scale=24, vol_h=70, title="Exemplu construit (fără ticker) — daily",
caption="Piciorul 2 sparge 30.00 cu volum mai mic decât piciorul 1, apoi închide înapoi peste.",
**{**STYLE, "width": 700})
svg_ = ch.render()
assert abs(px_ratio(ch, sl, entry, tp) - R) < 0.01
svgs["db_mai_jos"] = svg_
S4 = dict(leg1=leg1, leg2=leg2, neck=neck, entry=entry, sl=sl, tp=tp, R=R, v1=vols[L1], v2=vols[L2])
# =============================================================== 5. double bottom NU: e bear flag
cl = down + [30.6, # 12 picior 1
31.0, 31.4, 31.1, 31.7, 31.9, 31.6, 32.0, # 13-19 bear flag: urcare lentă, volum mic
31.2, 30.5, # 20-21
30.2, # 22 „picior 2", volum mare
30.9, # 23 cumpărătorul grăbit intră
30.5, 29.6, 28.8, 29.1, 28.0, 27.3, 27.6, 26.8]
vols = [52, 50, 44, 55, 58, 46, 60, 62, 48, 64, 66, 68, 74,
40, 38, 36, 38, 34, 36, 38, 62, 66, 78, 42, 60, 84, 76, 50, 80, 72, 48, 70]
cds = from_closes(cl, wicks=wk(len(cl), {12: (0.3, 0.6), 22: (0.3, 0.5)}), vols=vols, start=38.6)
L1, L2, EN = 12, 22, 23
leg1, leg2 = cds[L1].l, cds[L2].l
flag_up = sum(vols[13:20]) / 7
assert leg2 < leg1 and cds[EN].c > cds[L2].h
assert vols[L2] > vols[L1], "pe NU vânzătorii vin cu volum mai mare la piciorul 2 (fără divergență)"
assert flag_up < sum(vols[0:12]) / 12 * 0.75, "urcarea din bear flag trebuie să aibă volum mic"
assert max(c.h for c in cds[13:20]) < 32.6, "bear flag-ul nu ajunge nici la jumătatea downtrend-ului"
entry = cds[EN].c
sl = round(leg2 - 0.1, 2)
stop_bar = first_hit_down(cds, EN + 1, sl)
assert cds[stop_bar].c < leg2
print(f"S5 picior1 {leg1:.2f} vol {vols[L1]}, picior2 {leg2:.2f} vol {vols[L2]}, flag high "
f"{max(c.h for c in cds[13:20]):.2f}; intrare grăbită {entry:.2f} SL {sl:.2f} atins la {stop_bar} "
f"close {cds[stop_bar].c:.2f}; final {cds[-1].c:.2f}")
ch = Chart(candles=cds,
levels=[Level(entry, f"intrare grăbită {entry:.2f}", GRAY, label_at="left"),
Level(leg1, f"picior 1 {leg1:.2f}", ORANGE),
Level(sl, "", RED)],
marks=[Mark(16, "bear flag? volum mic", GRAY, "above", dy=-6),
Mark(L2, "volum mai mare", RED, "below", dy=2),
Mark(stop_bar, "SL atins", RED, "below", dy=36)],
scale=24, vol_h=70, title="Exemplu construit (fără ticker) — daily",
caption="Fără divergență de volum, „double bottom-ul în formare\" e doar un bear flag.", **STYLE)
svgs["db_nu"] = add(ch.render(), [lbl_below(ch, sl, f"SL {sl:.2f}", RED, at="left")])
S5 = dict(leg1=leg1, leg2=leg2, entry=entry, sl=sl, v1=vols[L1], v2=vols[L2], stop_c=cds[stop_bar].c)
# =============================================================== 6. double top, al doilea vârf mai sus + evening star (short)
cl = [52.0, 52.8, 52.5, 53.6, 54.4, 54.1, 55.3, 56.2, 55.9, 57.2, 58.4, # 0-10 uptrend
59.6, # 11 vârf 1
59.0, 58.2, 57.5, 57.3, # 12-15 pullback, neckline
57.9, 58.6, # 16-17
59.8, # 18 evening star (a): cumpărători
59.7, # 19 evening star (b): shooting star, vârf 2
58.9, # 20 evening star (c): vânzători, intrare short
58.3, 58.6, 57.4, 56.5, 56.9, 55.8, 54.9, 55.3, 54.0, 53.4]
vols = [48, 50, 42, 52, 54, 44, 56, 58, 46, 60, 62, 64, 50, 52, 54, 48, 44, 46, 50, 58, 70,
62, 44, 66, 80, 50, 70, 66, 48, 64, 60]
cds = from_closes(cl, wicks=wk(len(cl), {11: (0.4, 0.3), 15: (0.3, 0.4), 18: (0.2, 0.2), 19: (0.9, 0.2),
20: (0.2, 0.3), 30: (0.3, 0.3)}), vols=vols, start=51.6)
T1, T2, EV = 11, 19, 20
top1, top2 = cds[T1].h, cds[T2].h
neck = min(c.l for c in cds[T1 + 1:T2])
assert is_sh(cds, T1) and is_sh(cds, T2)
assert top2 > top1, "al doilea vârf trebuie să fie mai sus"
a, b, c_ = cds[18], cds[19], cds[20]
assert a.c > a.o and (a.c - a.o) >= 1.0, "(a) nu e lumânare mare a cumpărătorilor"
assert abs(b.c - b.o) <= 0.15 and b.h - max(b.o, b.c) >= 5 * abs(b.c - b.o), "(b) nu e shooting star"
assert c_.c < c_.o and c_.c < (a.o + a.c) / 2, "(c) nu închide sub jumătatea lui (a)"
entry = c_.c
sl = round(top2 + 0.1, 2)
risk = sl - entry
tp = round(neck - (top2 - neck), 2)
R = (entry - tp) / risk
brk = next(i for i in range(EV + 1, len(cds)) if cds[i].c < neck)
hit = first_hit_down(cds, EV + 1, tp)
assert hit == len(cds) - 1 and R >= 2.5
print(f"S6 vârf1 {top1:.2f} vârf2 {top2:.2f} neck {neck:.2f} short {entry:.2f} SL {sl:.2f} TP {tp:.2f} = {R:.2f}R; "
f"neckline spart la {brk} close {cds[brk].c:.2f}; TP atins la {hit}")
ch = Chart(candles=cds,
levels=[Level(sl, f"SL {sl:.2f}", RED),
Level(entry, f"intrare short {entry:.2f}", GREEN, label_at="left"),
Level(neck, f"neckline {neck:.2f}", ORANGE),
Level(tp, f"TP {tp:.2f} = {R:.1f}R", BLUE),
Level(63.2, "", "none")], # invizibil: loc deasupra pentru etichetele vârfurilor
marks=[Mark(T1, f"vârf 1: {top1:.2f}", INK, "above", dy=-22),
Mark(T2, "evening star", RED, "above", dy=-16),
Mark(T2, f"vârf 2: {top2:.2f}", RED, "above", dy=-30),
],
scale=22, vol_h=70, title="Exemplu construit (fără ticker) — daily",
caption="Oglinda double bottom-ului cu piciorul 2 mai jos: vârful 2 ia stop-urile short-urilor.",
**{**STYLE, "width": 700})
svg_ = add(ch.render(), [callout(ch, brk, 55.0, "neckline spart", RED, anchor="end", below=True)])
assert abs(px_ratio(ch, tp, entry, sl) - 1 / R) < 0.01
svgs["double_top"] = svg_
S6 = dict(top1=top1, top2=top2, neck=neck, entry=entry, sl=sl, tp=tp, R=R)
# =============================================================== 7. cup & handle weekly: DA
PIV_M = 47.50
cup = [45.0, 45.9, 46.8, 47.9, 48.9, 49.8, # 0-5 urcare până la marginea stângă
49.0, 47.8, 46.5, 45.2, 44.2, 43.4, 42.9, # 6-12 coborâre rotunjită
42.8, # 13 doji în fundul ceștii
43.6, # 14 buy doji
44.1, 44.9, 45.8, 46.9, 47.9, 48.8, 49.5, # 15-21 partea dreaptă
49.9, # 22 marginea dreaptă
49.5, 49.0, 48.6, 48.9, 49.4] # 23-27 mânerul
cup_w = {5: (0.5, 0.3), 12: (0.3, 0.3), 13: (0.5, 0.6), 22: (0.4, 0.3), 25: (0.3, 0.4)}
cup_v = [40, 44, 42, 46, 48, 50, 52, 50, 48, 46, 44, 40, 36, 42, 58, 46, 48, 50, 52, 54, 52, 50, 48,
34, 32, 30, 32, 36]
cl = cup + [51.0, # 28 breakout din mâner, volum țeavă
51.8, 51.4, 52.6, 53.5, 53.1, 54.4, 55.3, 55.0, 56.4, 57.6, 58.2]
cds = from_closes(cl, wicks=wk(len(cl), cup_w | {28: (0.2, 0.3)}),
vols=cup_v + [96, 66, 50, 64, 68, 52, 66, 70, 50, 66, 64, 62], start=44.4)
RL, RR, DJ, BD, BO = 5, 22, 13, 14, 28
rimL, rimR = cds[RL].h, cds[RR].h
cup_low = min(c.l for c in cds[RL:RR])
handle_top = max(c.h for c in cds[RR:BO])
handle_low = min(c.l for c in cds[RR + 1:BO])
depth = rimR - cup_low
assert abs(rimL - rimR) <= 0.1, "marginile ceștii nu sunt la același nivel"
assert handle_top == rimR
assert doji(cds[DJ]) and cds[BD].c > cds[DJ].h and cup_low == cds[DJ].l, "buy doji-ul nu e din fundul ceștii"
assert (rimR - handle_low) / depth <= 1 / 3, "mânerul coboară mai mult de 1/3 din ceașcă"
assert handle_low > PIV_M, "mânerul trebuie să stea deasupra pivotului monthly"
assert cds[BO].c > handle_top and cds[BO].vol == max(c.vol for c in cds), "breakout fără cel mai mare volum"
entry = cds[BO].c
sl = round(handle_low - 0.1, 2)
risk = entry - sl
tp = round(rimR + depth, 2)
R = (tp - entry) / risk
hit = first_hit_up(cds, BO + 1, tp)
assert hit == len(cds) - 1 and R >= 2.4
print(f"S7 margini {rimL:.2f}/{rimR:.2f} fund {cup_low:.2f} adâncime {depth:.2f} mâner low {handle_low:.2f} "
f"({(rimR - handle_low) / depth:.2f} din adâncime) entry {entry:.2f} SL {sl:.2f} TP {tp:.2f} = {R:.2f}R "
f"atins la {hit} ({hit - BO} săptămâni)")
ch = Chart(candles=cds,
levels=[Level(PIV_M, "", ORANGE),
Level(sl, f"SL {sl:.2f}", RED),
Level(entry, f"intrare {entry:.2f}", GREEN),
Level(tp, f"TP {tp:.2f} = {R:.1f}R", BLUE)],
marks=[Mark(RL, "margine", INK, "above", dy=-12), Mark(RR, "margine", INK, "above", dy=-12),
Mark(DJ, "ceașca", GRAY, "below", dy=4), Mark(BD, "buy doji", GREEN, "above", dy=-20),
Mark(25, "mâner", ORANGE, "above", dy=-2), Mark(BO, "breakout", GREEN, "above", dy=-4)],
scale=16, vol_h=70, title="Exemplu construit (fără ticker) — weekly",
caption="Trigger = închiderea peste mâner, cu cel mai mare volum din serie; ținta = adâncimea ceștii.",
**{**STYLE, "width": 760})
svg_ = add(ch.render(), [lbl_below(ch, PIV_M, f"pivot monthly {PIV_M:.2f}", ORANGE)])
assert abs(px_ratio(ch, sl, entry, tp) - R) < 0.01
svgs["cup_da"] = svg_
S7 = dict(rim=rimR, low=cup_low, depth=depth, hlow=handle_low, entry=entry, sl=sl, tp=tp, R=R, weeks=hit - BO)
CUP = dict(entry=entry, sl=sl, tp=tp, handle_top=handle_top, handle_low=handle_low, closes=cl[RR:BO + 1])
# =============================================================== 8. cup & handle NU: breakout fără volum
cl = cup + [50.6, # 28 breakout, volum sub media mânerului
50.1, 49.4, 48.7, 47.9, 47.3, 47.8, 46.9, 46.2, 46.6]
vv = cup_v + [30, 34, 40, 46, 58, 60, 44, 56, 58, 42]
cds = from_closes(cl, wicks=wk(len(cl), cup_w), vols=vv, start=44.4)
assert cds[BO].c > handle_top
assert vv[BO] < sum(vv[RR:BO]) / (BO - RR), "breakout-ul NU trebuie să aibă volum peste media mânerului"
entry = cds[BO].c
sl = round(handle_low - 0.1, 2)
stop_bar = first_hit_down(cds, BO + 1, sl)
back_in = next(i for i in range(BO + 1, len(cds)) if cds[i].c < handle_top)
assert back_in == BO + 1
print(f"S8 breakout {entry:.2f} vol {vv[BO]} (media mânerului {sum(vv[RR:BO]) / (BO - RR):.0f}), înapoi sub "
f"{handle_top:.2f} la {back_in}, SL {sl:.2f} atins la {stop_bar} ({stop_bar - BO} săpt.), final {cds[-1].c:.2f}")
ch = Chart(candles=cds,
levels=[Level(PIV_M, "", ORANGE),
Level(sl, f"SL {sl:.2f}", RED),
Level(entry, f"intrare {entry:.2f}", GREEN)],
marks=[Mark(BD, "buy doji", GREEN, "above", dy=-20), Mark(25, "mâner", ORANGE, "above", dy=-2),
Mark(BO, "breakout fără volum", RED, "above", dy=-4),
Mark(stop_bar, "SL atins", RED, "below", dy=4)],
scale=16, vol_h=70, title="Exemplu construit (fără ticker) — weekly",
caption="Aceeași ceașcă. Breakout-ul are volum sub media mânerului: cade înapoi.",
**{**STYLE, "width": 760})
svgs["cup_nu"] = add(ch.render(), [lbl_below(ch, PIV_M, f"pivot monthly {PIV_M:.2f}", ORANGE)])
S8 = dict(entry=entry, sl=sl, vol=vv[BO], vol_avg=sum(vv[RR:BO]) / (BO - RR), weeks=stop_bar - BO)
# =============================================================== 9. fractalul: mânerul weekly e o ceașcă pe daily
daily = [49.8, 49.6, 49.8, 49.6, 49.5, # săpt. 23
49.3, 49.4, 49.1, 49.2, 49.0, # săpt. 24
48.8, 48.9, 48.6, 48.7, 48.6, # săpt. 25 — fundul ceștii mici
48.8, 48.7, 48.9, 49.0, 48.9, # săpt. 26
49.1, 49.3, 49.5, 49.6, 49.4, # săpt. 27 — marginea dreaptă (23), mânerul mic începe
49.2, 49.5, 50.1, 50.4, 51.0] # săpt. 28 — mâner mic (25), breakout daily (27)
cds = from_closes(daily, wicks=wk(len(daily), {12: (0.3, 0.4)}), start=CUP["closes"][0])
weekly_closes = CUP["closes"][1:]
assert [round(daily[4 + 5 * k], 2) for k in range(6)] == weekly_closes, "close-urile de vineri ≠ close-urile weekly"
assert abs(min(c.l for c in cds[:25]) - CUP["handle_low"]) < 1e-9, "fundul ceștii mici ≠ low-ul mânerului weekly"
assert max(c.h for c in cds[:25]) <= CUP["handle_top"] + 1e-9
MR = 23
mini_rim = max(c.h for c in cds[18:25])
assert mini_rim == cds[MR].h
mini_handle_low = min(c.l for c in cds[MR + 1:27])
trig = next(i for i in range(MR + 1, len(cds)) if cds[i].c > mini_rim)
entry = cds[trig].c
sl = round(mini_handle_low - 0.1, 2)
assert trig == 27 and entry < CUP["handle_top"], "trigger-ul daily trebuie să vină sub nivelul de breakout weekly"
assert sl > CUP["sl"] and entry < CUP["entry"]
R_d = (CUP["tp"] - entry) / (entry - sl)
R_w = (CUP["tp"] - CUP["entry"]) / (CUP["entry"] - CUP["sl"])
print(f"S9 margine mică {mini_rim:.2f}, mâner mic low {mini_handle_low:.2f}, trigger daily la {trig} close {entry:.2f} "
f"SL {sl:.2f} risc {entry - sl:.2f}; aceeași țintă {CUP['tp']:.2f} = {R_d:.1f}R daily vs {R_w:.1f}R weekly")
ch = Chart(candles=cds,
levels=[Level(CUP["handle_top"], f"breakout weekly {CUP['handle_top']:.2f}", ORANGE, label_at="left"),
Level(entry, "", GREEN),
Level(sl, f"SL daily {sl:.2f}", RED)],
marks=[Mark(12, "ceașca mică", GRAY, "below", dy=4),
Mark(MR, "margine", INK, "above", dy=-26),
Mark(25, "mâner mic", ORANGE, "below", dy=0)],
scale=70, title="Exemplu construit (fără ticker) — daily, în mânerul weekly",
caption="Cele 6 săptămâni ale mânerului, pe daily: trigger-ul vine cu 2 zile înainte de cel weekly.",
**{**STYLE, "width": 760, "gap": 19})
svg_ = add(ch.render(), [callout(ch, trig, 48.3, "trigger daily", GREEN, below=True),
lbl_below(ch, entry, f"intrare daily {entry:.2f}", GREEN)])
svgs["cup_fractal"] = svg_
S9 = dict(entry=entry, sl=sl, R_d=R_d, R_w=R_w, rim=mini_rim, hlow=mini_handle_low)
# =============================================================== 10. descending wedge: DA
pre = [55.0, 54.2, 54.5, 53.4, 52.6] # 0-4 downtrend
wedge = [52.0, # 5
52.5, # 6 SH1
51.6, 50.6, 49.8, 49.1, # 7-10 (10 = SL1)
49.8, 50.4, 51.0, # 11-13 (13 = SH2)
50.2, 49.2, 48.4, # 14-16 (16 = SL2)
49.1, 49.9, # 17-18 (18 = SH3)
49.0, 48.3, 47.9, # 19-21 (21 = SL3)
48.3, 48.8, # 22-23 (23 = SH4)
48.0, 47.6, 47.3, # 24-26 (26 = SL4)
47.6] # 27
W_SH, W_SL = [6, 13, 18, 23], [10, 16, 21, 26]
w_over = {i: (0.5, 0.3) for i in W_SH} | {i: (0.3, 0.5) for i in W_SL} | {27: (0.2, 0.3)}
w_vol = [60, 58, 54, 62, 64, 56, 52, 58, 56, 54, 52, 44, 42, 40, 46, 44, 42, 36, 34, 38, 36, 34, 30, 30,
32, 30, 28, 30]
BO_W = 28
def wedge_series(after, after_vols, mirror=False):
cl_ = pre + wedge + after
if mirror:
cl_ = [round(100 - x, 2) for x in cl_]
ov = w_over if not mirror else {i: (d, u) for i, (u, d) in w_over.items()}
return from_closes(cl_, wicks=wk(len(cl_), ov), vols=w_vol + after_vols,
start=(55.8 if not mirror else 100 - 55.8))
def wedge_lines(cds_, falling=True):
"""falling: vârfurile la W_SH, minimele la W_SL; la oglindă (rising) rolurile se inversează"""
hs, ls = (W_SH, W_SL) if falling else (W_SL, W_SH)
hi = (hs[0], cds_[hs[0]].h, hs[-1], cds_[hs[-1]].h)
lo = (ls[0], cds_[ls[0]].l, ls[-1], cds_[ls[-1]].l)
return hi, lo, hs, ls
def check_wedge(cds_, falling=True):
hi, lo, hs, ls = wedge_lines(cds_, falling)
for i in range(W_SH[0], BO_W):
assert cds_[i].h <= line_at(*hi, i) + 0.12, f"bara {i} străpunge linia de sus"
assert cds_[i].l >= line_at(*lo, i) - 0.12, f"bara {i} străpunge linia de jos"
for i in hs:
assert is_sh(cds_, i), f"bara {i} nu e swing high"
for i in ls:
assert is_sl(cds_, i), f"bara {i} nu e swing low"
s_hi = (hi[3] - hi[1]) / (hi[2] - hi[0])
s_lo = (lo[3] - lo[1]) / (lo[2] - lo[0])
if falling:
assert s_hi < s_lo < 0, "nu e descending wedge (linii care coboară și converg)"
else:
assert 0 < s_hi < s_lo, "nu e ascending wedge (linii care urcă și converg)"
for a_, b_ in zip(hs, hs[1:]):
assert (cds_[b_].h < cds_[a_].h) == falling
for a_, b_ in zip(ls, ls[1:]):
assert (cds_[b_].l < cds_[a_].l) == falling
return hi, lo
after = [48.6, # 28 breakout peste linia de sus, volum țeavă
49.3, 49.0, 50.1, 50.8, 50.5, 51.6, 52.3, 52.1, 52.8]
cds = wedge_series(after, [92, 70, 52, 66, 70, 50, 64, 62, 48, 60])
hi, lo = check_wedge(cds)
upper_at_bo = line_at(*hi, BO_W)
assert cds[BO_W].c > upper_at_bo and cds[BO_W - 1].c < line_at(*hi, BO_W - 1)
assert cds[BO_W].vol == max(c.vol for c in cds[W_SH[0]:])
entry = cds[BO_W].c
sl = round(cds[W_SL[-1]].l - 0.1, 2)
tp = round(cds[W_SH[0]].h, 2)
risk = entry - sl
R = (tp - entry) / risk
hit = first_hit_up(cds, BO_W + 1, tp)
assert hit == len(cds) - 1 and R >= 1.8
print(f"S10 SH {[round(cds[i].h, 2) for i in W_SH]} SL {[round(cds[i].l, 2) for i in W_SL]} linia sus la "
f"breakout {upper_at_bo:.2f}; entry {entry:.2f} SL {sl:.2f} TP {tp:.2f} = {R:.2f}R atins la {hit}")
ch = Chart(candles=cds,
levels=[Level(sl, f"SL {sl:.2f}", RED, label_at="left"),
Level(entry, f"intrare {entry:.2f}", GREEN),
Level(tp, f"TP {tp:.2f} = {R:.1f}R", BLUE)],
marks=[Mark(14, "descending wedge", ORANGE, "below", dy=dy_line(cds[14], 30, line_at(*lo, 14), 22, "below"))],
scale=30, vol_h=70, title="Exemplu construit (fără ticker) — daily",
caption="Minime și maxime tot mai jos, dar tot mai apropiate: vânzătorii obosesc.",
**{**STYLE, "width": 700})
svg_ = add(with_lines(ch, ch.render(), [(*hi, BO_W, ORANGE), (*lo, BO_W, ORANGE)]),
[callout(ch, BO_W, 50.0, "breakout + volum", GREEN, anchor="end")])
assert abs(px_ratio(ch, sl, entry, tp) - R) < 0.01
svgs["wedge_desc_da"] = svg_
S10 = dict(entry=entry, sl=sl, tp=tp, R=R, line=upper_at_bo, vol=cds[BO_W].vol)
# =============================================================== 11. descending wedge NU: breakout fals
after = [48.3, # 28 închide peste linie, dar cu volum mic
47.7, 47.1, 46.5, 45.8, 46.2, 45.4]
cds = wedge_series(after, [31, 40, 50, 62, 66, 48, 60])
hi, lo = check_wedge(cds)
assert cds[BO_W].c > line_at(*hi, BO_W)
assert cds[BO_W].vol < sum(w_vol[W_SH[0]:]) / len(w_vol[W_SH[0]:]), "breakout-ul fals are volum sub medie"
entry = cds[BO_W].c
sl = round(cds[W_SL[-1]].l - 0.1, 2)
stop_bar = first_hit_down(cds, BO_W + 1, sl)
print(f"S11 breakout fals {entry:.2f} vol {cds[BO_W].vol} (media wedge {sum(w_vol[W_SH[0]:]) / len(w_vol[W_SH[0]:]):.0f}); "
f"SL {sl:.2f} atins la {stop_bar} ({stop_bar - BO_W} candele)")
ch = Chart(candles=cds,
levels=[Level(sl, f"SL {sl:.2f}", RED, label_at="left"),
Level(entry, f"intrare {entry:.2f}", GREEN)],
marks=[Mark(14, "descending wedge", ORANGE, "below", dy=dy_line(cds[14], 30, line_at(*lo, 14), 22, "below")),
Mark(BO_W, "breakout fără volum", RED, "above", dy=-18),
Mark(stop_bar, "SL atins", RED, "below", dy=20)],
scale=30, vol_h=70, title="Exemplu construit (fără ticker) — daily",
caption="Aceeași compresie. Close peste linie, dar cu volum sub media wedge-ului: fals.",
**{**STYLE, "width": 700})
svgs["wedge_desc_nu"] = with_lines(ch, ch.render(), [(*hi, BO_W, ORANGE), (*lo, BO_W, ORANGE)])
S11 = dict(entry=entry, sl=sl, vol=cds[BO_W].vol, n=stop_bar - BO_W)
# =============================================================== 12. ascending wedge la rezistența monthly (short)
after = [48.6, 49.3, 49.0, 50.1, 50.8, 50.5, 51.6, 52.3, 52.1, 52.8] # oglinda breakout-ului din S10
cds = wedge_series(after, [92, 70, 52, 66, 70, 50, 64, 62, 48, 60], mirror=True)
hi_, lo_ = check_wedge(cds, falling=False)
lower_at_bo = line_at(*lo_, BO_W)
assert cds[BO_W].c < lower_at_bo and cds[BO_W - 1].c > line_at(*lo_, BO_W - 1)
RES_M = round(cds[W_SL[-1]].h, 2)
assert all(c.h <= RES_M + 1e-9 for c in cds[:BO_W]), "rezistența monthly trebuie să fie maximul seriei"
entry = cds[BO_W].c
sl = round(cds[W_SL[-1]].h + 0.1, 2)
tp = round(cds[W_SH[0]].l, 2)
risk = sl - entry
R = (entry - tp) / risk
hit = first_hit_down(cds, BO_W + 1, tp)
assert hit == len(cds) - 1 and R >= 1.8
print(f"S12 rezistență monthly {RES_M:.2f}; linia jos la breakdown {lower_at_bo:.2f}; short {entry:.2f} SL {sl:.2f} "
f"TP {tp:.2f} = {R:.2f}R atins la {hit}")
ch = Chart(candles=cds,
levels=[Level(sl, f"SL {sl:.2f}", RED),
Level(entry, f"intrare short {entry:.2f}", GREEN, label_at="left"),
Level(tp, f"TP {tp:.2f} = {R:.1f}R", BLUE)],
marks=[Mark(2, "uptrend", GRAY, "below", dy=8),
Mark(14, "ascending wedge", ORANGE, "above", dy=dy_line(cds[14], 30, line_at(*hi_, 14), -8, "above")),
Mark(W_SL[-1], "rezistență monthly", RED, "above", dy=-4)],
scale=30, vol_h=70, title="Exemplu construit (fără ticker) — daily",
caption="Oglinda: vârfuri tot mai sus, dar tot mai mici, lipite de rezistența monthly.",
**{**STYLE, "width": 760})
svg_ = add(with_lines(ch, ch.render(), [(*hi_, BO_W, ORANGE), (*lo_, BO_W, ORANGE)]),
[callout(ch, BO_W, 49.7, "breakdown + volum", RED, anchor="end", below=True)])
assert abs(px_ratio(ch, tp, entry, sl) - 1 / R) < 0.01
svgs["wedge_asc"] = svg_
S12 = dict(entry=entry, sl=sl, tp=tp, R=R, res=RES_M)
# =============================================================== 13. ascending triangle, breakout, retest + bull harami
TOP = 50.00
cl = [44.0, 44.9, 45.5, 46.4, 47.3, 48.4, 49.1, # 0-6
49.7, # 7 atinge 50.00
49.0, 48.1, 47.1, # 8-10 (10 = low 1)
47.8, 48.6, 49.3, 49.7, # 11-14 (14 atinge 50.00)
49.1, 48.4, 48.0, # 15-17 (17 = low 2)
48.7, 49.3, 49.7, # 18-20 (20 atinge 50.00)
49.2, 48.8, # 21-22 (22 = low 3)
49.3, 49.6, # 23-24
50.9, # 25 breakout
51.4, 50.9, # 26-27
50.3, # 28 retest: low exact pe 50.00
50.5, # 29 bull harami
51.1, # 30 trigger
51.7, 51.4, 52.3, 52.9, 52.6, 53.2]
vols = [40, 44, 42, 46, 48, 52, 50, 54, 44, 42, 46, 40, 38, 42, 44, 36, 34, 38, 36, 34, 38, 30, 32, 30, 34,
88, 60, 44, 40, 38, 62, 56, 42, 58, 60, 44, 56]
cds = from_closes(cl, wicks=wk(len(cl), {10: (0.3, 0.5), 17: (0.3, 0.4), 22: (0.3, 0.3)}), vols=vols, start=43.6)
T_LO = [10, 17, 22]
BO_T, RT, HAR, TR = 25, 28, 29, 30
assert all(c.h <= TOP + 1e-9 for c in cds[5:BO_T]), "o bară din triunghi trece peste 50.00"
touch = [i for i in range(5, BO_T) if abs(cds[i].h - TOP) < 1e-9]
assert sum(1 for i in touch if i - 1 not in touch) >= 3, "rezistența nu e atinsă de 3 ori separat"
t_lows = [cds[i].l for i in T_LO]
assert all(b > a for a, b in zip(t_lows, t_lows[1:])), "minimele triunghiului nu urcă"
lo_t = (T_LO[0], t_lows[0], T_LO[1], t_lows[1])
for i in range(T_LO[0], BO_T):
assert cds[i].l >= line_at(*lo_t, i) - 0.12, f"bara {i} străpunge linia ascendentă"
assert cds[BO_T].c > TOP and cds[BO_T].vol == max(vols)
assert abs(cds[RT].l - TOP) < 1e-9 and cds[RT].c > TOP, "retestul nu atinge 50.00 cu codița"
har, mom = cds[HAR], cds[RT]
assert mom.c < mom.o and har.c > har.o and min(har.o, har.c) >= mom.c and max(har.o, har.c) <= mom.o, "nu e bull harami"
assert cds[TR].c > har.h
entry = cds[TR].c
sl = round(TOP - 0.2, 2)
height = TOP - t_lows[0]
tp = round(TOP + height, 2)
risk = entry - sl
R = (tp - entry) / risk
hit = first_hit_up(cds, TR + 1, tp)
assert hit == len(cds) - 1 and R >= 1.75
print(f"S13 low-uri {[round(x, 2) for x in t_lows]} breakout close {cds[BO_T].c:.2f} retest low {cds[RT].l:.2f} "
f"harami {har.o:.2f}->{har.c:.2f} entry {entry:.2f} SL {sl:.2f} înălțime {height:.2f} TP {tp:.2f} = {R:.2f}R")
ch = Chart(candles=cds,
levels=[Level(TOP, f"rezistență = pivot quarterly {TOP:.2f}", ORANGE),
Level(sl, "", RED),
Level(entry, f"intrare {entry:.2f}", GREEN),
Level(tp, f"TP {tp:.2f} = {R:.1f}R", BLUE)],
marks=[Mark(17, "ascending triangle", ORANGE, "below", dy=dy_line(cds[17], 30, line_at(*lo_t, 17), 16, "below")),
Mark(BO_T, "breakout", GREEN, "above", dy=-4),
Mark(RT, "retest", ORANGE, "below", dy=4),
Mark(HAR, "harami", GREEN, "below", dy=22)],
scale=30, vol_h=70, title="Exemplu construit (fără ticker) — daily",
caption="Breakout, cădere înapoi pe fosta rezistență, bull harami acolo: intrare la trigger.",
**{**STYLE, "width": 760})
svg_ = add(with_lines(ch, ch.render(), [(*lo_t, BO_T, ORANGE)]), [lbl_below(ch, sl, f"SL {sl:.2f}", RED)])
assert abs(px_ratio(ch, sl, entry, tp) - R) < 0.01
svgs["triunghi_asc"] = svg_
S13 = dict(lows=t_lows, bo=cds[BO_T].c, entry=entry, sl=sl, tp=tp, R=R, height=height)
# =============================================================== 14. island top, apoi cumpărarea în gap-ul închis
PIV_Q = 44.50
segA = from_closes([40.0, 40.8, 41.5, 41.2, 42.1, 42.9, 42.6, 43.5, 44.0, 44.2], wick=0.3, start=39.6)
segB = from_closes([45.6, 46.4, 46.1, 47.0, 47.8, 47.5, 48.4, 49.2, 49.0, 49.6], wick=0.3, start=45.2)
segC = from_closes([51.6, 51.9, 51.5, 51.2], wick=0.3, start=51.0)
segD = from_closes([49.4, 49.7, 48.8, 48.1, 48.4, 47.5, 46.8, 47.1, 46.2, 45.5, 45.1,
45.0, # hammer: închide gap-ul de jos
45.9, # trigger
46.6, 46.3, 47.4, 48.3, 48.0, 49.2, 50.0],
wicks=wk(20, {11: (0.2, 0.6), 12: (0.2, 0.3)}), start=50.0)
cds = segA + segB + segC + segD
vols = [40, 44, 38, 46, 48, 42, 50, 52, 50, 60, 72, 56, 44, 58, 60, 46, 62, 64, 50, 66,
88, 70, 60, 64, 90, 56, 70, 64, 48, 66, 68, 50, 64, 62, 50, 70, 80, 58, 44, 60, 64, 46, 60, 58]
assert len(vols) == len(cds)
for c, v in zip(cds, vols):
c.vol = v
iA, iB, iC, iD = 0, len(segA), len(segA) + len(segB), len(segA) + len(segB) + len(segC)
gap1 = (cds[iB - 1].h, cds[iB].l)
gap2 = (cds[iC - 1].h, cds[iC].l)
isl_low = min(c.l for c in cds[iC:iD])
gap3 = (cds[iD].h, isl_low)
assert gap1[1] > gap1[0] and gap2[1] > gap2[0] and gap3[1] > gap3[0], "lipsesc gap-urile"
assert all(c.l > gap2[0] for c in cds[iC:iD]) and all(c.h < isl_low for c in cds[iD:]), "insula nu e izolată"
assert max(c.h for c in cds[iC:iD]) == max(c.h for c in cds), "insula nu e vârful"
HAM, TRI = iD + 11, iD + 12
fill = next(i for i in range(iD, len(cds)) if cds[i].l <= gap1[0])
assert fill == HAM, "gap-ul de jos trebuie închis chiar de hammer"
hm = cds[HAM]
assert min(hm.o, hm.c) - hm.l >= 4 * abs(hm.o - hm.c), "bara de închidere nu e hammer"
assert abs(gap1[0] - PIV_Q) < 1e-9, "marginea de jos a gap-ului vechi = pivotul quarterly"
assert cds[TRI].c > hm.h
entry = cds[TRI].c
sl = round(hm.l - 0.2, 2)
tp = round(gap3[0], 2)
risk = entry - sl
R = (tp - entry) / risk
hit = first_hit_up(cds, TRI + 1, tp)
assert hit == len(cds) - 1 and R >= 2.5
print(f"S14 gap jos {gap1[0]:.2f}-{gap1[1]:.2f}; gap sus {gap2[0]:.2f}-{gap2[1]:.2f}; insula {isl_low:.2f}-"
f"{max(c.h for c in cds[iC:iD]):.2f}; gap down {gap3[0]:.2f}-{gap3[1]:.2f}; hammer low {hm.l:.2f} la {HAM}; "
f"entry {entry:.2f} SL {sl:.2f} TP {tp:.2f} = {R:.2f}R atins la {hit}")
ch = Chart(candles=cds,
levels=[Level(tp, f"TP {tp:.2f} = {R:.1f}R (gap-ul insulei)", BLUE, label_at="left"),
Level(entry, f"intrare {entry:.2f}", GREEN, label_at="left"),
Level(PIV_Q, f"pivot quarterly {PIV_Q:.2f}", ORANGE, label_at="left"),
Level(sl, "", RED)],
marks=[Mark(iC + 1, "island top", RED, "above", dy=-4),
Mark(iD, "gap down", RED, "below", dy=4),
Mark(HAM, "hammer: gap închis", GREEN, "below", dy=4)],
scale=22, vol_h=70, title="Exemplu construit (fără ticker) — daily",
caption="Insula e bearish. Cumperi abia jos, unde gap-ul vechi s-a închis în pivotul quarterly.",
**{**STYLE, "width": 720})
svg_ = add(ch.render(), [lbl_below(ch, sl, f"SL {sl:.2f}", RED),
callout(ch, iB, 47.5, "gap up", GRAY, anchor="end")])
assert abs(px_ratio(ch, sl, entry, tp) - R) < 0.01
svgs["island_top"] = svg_
S14 = dict(gap1=gap1, gap2=gap2, gap3=gap3, isl_low=isl_low, isl_high=max(c.h for c in cds[iC:iD]),
ham_l=hm.l, entry=entry, sl=sl, tp=tp, R=R)
# =============================================================== 15. triple doji la bottom
cl = [44.0, 43.1, 43.5, 42.2, 41.0, 41.4, 39.9, 38.6,
36.9, # 8 low anterior
37.8, 38.6, 39.0, # 9-11 (11 = lower high)
38.3, 37.4, 37.8, 36.8, 35.9, 35.2, 34.6, 34.3, # 12-19 sparge low-ul anterior
34.2, 34.3, 34.2, # 20-22 triple doji
35.3, # 23 trigger
35.9, 35.6, 36.7, 37.5, 37.2, 38.4, 39.2]
vols = [50, 54, 42, 58, 60, 44, 62, 64, 70, 46, 42, 40, 52, 56, 44, 56, 52, 48, 46, 42,
36, 34, 32, 84, 66, 40, 70, 72, 42, 76, 80]
cds = from_closes(cl, wicks=wk(len(cl), {8: (0.3, 0.6), 11: (0.4, 0.3), 20: (0.5, 0.5), 21: (0.5, 0.5),
22: (0.5, 0.5), 23: (0.2, 0.3)}), vols=vols, start=44.6)
D3 = [20, 21, 22]
prior_low = cds[8].l
assert is_sl(cds, 8) and is_sh(cds, 11)
assert all(doji(cds[i]) for i in D3), "cele trei bare nu sunt doji"
assert min(c.l for c in cds[12:20]) < prior_low, "low-ul anterior nu e spart înainte de doji"
assert max(vols[15:20]) < vols[8], "sub low-ul anterior volumul trebuie să fie mai mic (divergență bull de volum)"
assert vols[20] > vols[21] > vols[22] and vols[20] < min(vols[15:20]), "pe doji vânzarea trebuie să se epuizeze"
up = [vols[i] for i in range(23, len(cl)) if cl[i] > cl[i - 1]]
dn = [vols[i] for i in range(23, len(cl)) if cl[i] < cl[i - 1]]
assert vols[23] == max(vols) and min(up) > max(dn), "din oversold, volumul trebuie să fie pe barele verzi (acumulare)"
assert max(abs(cds[i].c - cds[20].c) for i in D3) <= 0.15, "doji-urile nu stau la același nivel"
d_hi, d_lo = max(cds[i].h for i in D3), min(cds[i].l for i in D3)
TR = 23
assert cds[TR].c > d_hi and all(cds[i].c <= d_hi for i in D3)
entry = cds[TR].c
sl = round(d_lo - 0.1, 2)
tp = round(cds[11].h, 2)
risk = entry - sl
R = (tp - entry) / risk
hit = first_hit_up(cds, TR + 1, tp)
assert hit == len(cds) - 1 and R >= 2
print(f"S15 low anterior {prior_low:.2f}; doji high {d_hi:.2f} low {d_lo:.2f}; entry {entry:.2f} SL {sl:.2f} "
f"TP (lower high) {tp:.2f} = {R:.2f}R atins la {hit} ({hit - TR} săpt.)")
ch = Chart(candles=cds,
levels=[Level(prior_low, f"low anterior {prior_low:.2f}", GRAY, label_at="left"),
Level(sl, f"SL {sl:.2f}", RED),
Level(entry, f"intrare {entry:.2f}", GREEN),
Level(tp, f"TP {tp:.2f} = {R:.1f}R", BLUE)],
marks=[Mark(11, "lower high", INK, "above", dy=-2),
Mark(21, "triple doji", GREEN, "below", dy=6),
Mark(TR, "trigger", GREEN, "above", dy=-28)],
scale=26, vol_h=70, title="Exemplu construit (fără ticker) — weekly",
caption="Sub low volumul scade, pe doji se stinge; trigger-ul și urcarea vin cu volum: acumulare.",
**{**STYLE, "width": 700})
svg_ = ch.render()
assert abs(px_ratio(ch, sl, entry, tp) - R) < 0.01
svgs["triple_doji"] = svg_
S15 = dict(prior=prior_low, hi=d_hi, lo=d_lo, entry=entry, sl=sl, tp=tp, R=R, weeks=hit - TR)
# =============================================================== 16. inverted head & shoulders
cl = [47.0, 46.2, 46.6, 45.4, 44.5, 44.8, 43.6, # 0-6 downtrend
42.6, # 7 umăr stâng
43.1, 43.6, 43.9, # 8-10 neckline 1
43.4, 42.2, 41.0, # 11-13
39.8, # 14 cap
40.8, 41.9, 43.0, 43.9, # 15-18 neckline 2
43.4, 43.0, 42.9, # 19-21 umăr drept
43.6, 43.9, # 22-23
44.8, # 24 breakout
45.4, 45.1, 46.2, 47.0, 46.6, 47.8, 48.5, 48.2, 49.0]
vols = [52, 56, 44, 58, 60, 46, 62, 66, 48, 44, 46, 50, 58, 64, 72, 54, 52, 50, 48, 38, 36, 40, 44, 46,
90, 66, 48, 64, 66, 50, 62, 64, 50, 60]
cds = from_closes(cl, wicks=wk(len(cl), {7: (0.3, 0.5), 14: (0.3, 0.6), 21: (0.3, 0.4), 24: (0.2, 0.3)}),
vols=vols, start=47.6)
LS, HD, RS, BO_I = 7, 14, 21, 24
neck = max(c.h for c in cds[LS:RS])
assert is_sl(cds, LS) and is_sl(cds, HD) and is_sl(cds, RS)
assert cds[HD].l < cds[LS].l and cds[HD].l < cds[RS].l, "capul nu e cel mai jos"
assert cds[RS].l > cds[LS].l, "textul spune: umărul drept mai sus"
assert all(c.h <= neck + 1e-9 for c in cds[LS:BO_I]) and cds[BO_I].c > neck
assert vols[RS] < vols[LS] and vols[BO_I] == max(vols), "volum: umăr drept mic, breakout maxim"
entry = cds[BO_I].c
sl = round(cds[RS].l - 0.1, 2)
height = neck - cds[HD].l
tp = round(neck + height, 2)
risk = entry - sl
R = (tp - entry) / risk
hit = first_hit_up(cds, BO_I + 1, tp)
assert hit == len(cds) - 1 and R >= 1.7
print(f"S16 umăr stâng {cds[LS].l:.2f} cap {cds[HD].l:.2f} umăr drept {cds[RS].l:.2f} neck {neck:.2f} "
f"entry {entry:.2f} SL {sl:.2f} înălțime {height:.2f} TP {tp:.2f} = {R:.2f}R atins la {hit}")
ch = Chart(candles=cds,
levels=[Level(neck, "", ORANGE),
Level(sl, f"SL {sl:.2f}", RED),
Level(entry, f"intrare {entry:.2f}", GREEN),
Level(tp, f"TP {tp:.2f} = {R:.1f}R", BLUE)],
marks=[Mark(LS, "umăr stâng", INK, "below", dy=4), Mark(HD, "cap", INK, "below", dy=4),
Mark(RS, "umăr drept (mai sus)", GREEN, "below", dy=4),
Mark(BO_I, "breakout", GREEN, "above", dy=-4)],
scale=24, vol_h=70, title="Exemplu construit (fără ticker) — daily",
caption="Umărul drept mai sus decât cel stâng: aceeași formă ca o ceașcă cu mâner.",
**{**STYLE, "width": 680})
svg_ = add(ch.render(), [lbl_below(ch, neck, f"neckline {neck:.2f}", ORANGE)])
assert abs(px_ratio(ch, sl, entry, tp) - R) < 0.01
svgs["ihs"] = svg_
S16 = dict(ls=cds[LS].l, hd=cds[HD].l, rs=cds[RS].l, neck=neck, entry=entry, sl=sl, tp=tp, R=R)
# =============================================================== 17. bull flag înapoi în locul crimei
cl = [45.0, 46.1, 46.9, 47.8, 48.6, 49.2,
49.7, # 6 vârful anterior (locul crimei)
49.0, 48.3, 48.8, 48.2, 49.1, # 7-11 consolidare
50.6, 51.8, 52.9, 53.6, 54.5, # 12-16 impuls
55.0, # 17 vârful impulsului
54.4, 53.6, 53.9, 52.8, 52.0, 52.4, 51.4, # 18-24 steagul coboară
50.8, # 25 picior 1
51.5, 51.2, # 26-27
50.4, # 28 picior 2: mai jos, pe locul crimei
51.7, # 29 trigger
52.4, 52.1, 53.3, 54.2, 53.9, 55.0]
vols = [44, 50, 48, 54, 56, 52, 60, 46, 42, 40, 38, 48, 84, 76, 70, 66, 62, 60, 46, 42, 38, 40, 36, 34, 36,
40, 30, 32, 42, 72, 60, 44, 62, 66, 48, 62]
cds = from_closes(cl, wicks=wk(len(cl), {6: (0.3, 0.3), 17: (0.3, 0.3), 25: (0.3, 0.4), 28: (0.3, 0.4),
29: (0.2, 0.3)}), vols=vols, start=44.6)
PH, TOPF, L1, L2, TR = 6, 17, 25, 28, 29
crime = cds[PH].h
assert crime == max(c.h for c in cds[:12]), "vârful anterior nu e maximul dinainte de impuls"
assert cds[12].c > crime, "impulsul nu sparge vârful anterior"
assert cds[TOPF].h == max(c.h for c in cds[:TR])
flag_vol = sum(vols[18:25]) / 7
assert flag_vol < sum(vols[12:18]) / 6 * 0.65, "volumul pe steag trebuie să scadă clar față de impuls"
assert is_sl(cds, L1) and is_sl(cds, L2) and cds[L2].l < cds[L1].l, "piciorul 2 trebuie să fie mai jos"
assert abs(cds[L2].l - crime) < 1e-9, "piciorul 2 trebuie să atingă exact locul crimei cu codița"
assert cds[TR].c > cds[L2].h
entry = cds[TR].c
sl = round(cds[L2].l - 0.2, 2)
tp = round(cds[TOPF].h, 2)
risk = entry - sl
R = (tp - entry) / risk
hit = first_hit_up(cds, TR + 1, tp)
assert hit == len(cds) - 1 and R >= 1.8
print(f"S17 locul crimei {crime:.2f}; vârf impuls {cds[TOPF].h:.2f}; picior1 {cds[L1].l:.2f} picior2 {cds[L2].l:.2f}; "
f"vol impuls {sum(vols[12:18]) / 6:.0f} vs steag {flag_vol:.0f}; entry {entry:.2f} SL {sl:.2f} TP {tp:.2f} = {R:.2f}R")
ch = Chart(candles=cds,
levels=[Level(crime, f"locul crimei {crime:.2f}", ORANGE, label_at="left"),
Level(sl, "", RED),
Level(entry, f"intrare {entry:.2f}", GREEN),
Level(tp, f"TP {tp:.2f} = {R:.1f}R", BLUE)],
marks=[Mark(PH, "vârf anterior", INK, "above", dy=-16),
Mark(L1, "picior 1", RED, "below", dy=8),
Mark(L2, "picior 2", RED, "below", dy=12)],
mas=[MA(20, GRAY, dash="4 3")],
scale=26, vol_h=70, title="Exemplu construit (fără ticker) — daily",
caption="Linia punctată = SMA-20. Double bottom DUPĂ un impuls, nu după downtrend: e steag.",
**{**STYLE, "width": 700})
svg_ = ch.render()
svg_ = add(svg_, [lbl_below(ch, sl, f"SL {sl:.2f}", RED),
f'<text x="{ch._cx(21) - 4:.1f}" y="{ch._y(54.7):.1f}" fill="{ORANGE}" '
f'style="font-weight:bold">bull flag</text>'])
assert abs(px_ratio(ch, sl, entry, tp) - R) < 0.01
svgs["bull_flag"] = svg_
S17 = dict(crime=crime, top=cds[TOPF].h, l1=cds[L1].l, l2=cds[L2].l, entry=entry, sl=sl, tp=tp, R=R)
print(f"{len(svgs)} diagrame")
# ---------------------------------------------------------------- substituție în .md
if MD.exists():
md = MD.read_text(encoding="utf-8")
for name, svg in svgs.items():
pat = re.compile(rf"<!--SVG:{name}-->.*?<!--/SVG-->", re.S)
assert pat.search(md), f"marcatorul {name} lipsește din .md"
md = pat.sub(lambda _m, s=svg, n=name: f"<!--SVG:{n}-->\n{s}\n<!--/SVG-->", md)
MD.write_text(md, encoding="utf-8")
print("scris", MD)