Al patrulea document de strategie, pe modelul aprobat al lui 01. Acoperite: volum "teava" = institutional, acumulare vs. distributie, divergenta de volum la al doilea picior, volumul care conduce pretul, breakout fara volum = esec, monthly vs. weekly, alegerea simbolului din sector dupa volum. Fiecare setup are perechea DA/NU desenata. Toate cele 11 diagrame sunt serii continue de 36-42 de bare cu panou de volum, generate din preturi cu svg_candles, cu assert pe fiecare nivel, pe raportul R si pe raportul de volum afirmat in text. Verificare in doua runde de screenshot citite efectiv: 6 probleme reparate, printre care o linie de suport la 12px de eticheta de SL si o cifra greșita in Gotchas (intrarea era 39.80, nu 38.40). Audit: 0. Marcat explicit ca adaugire proprie: "volumul conduce pretul" nu e o regula pe care o spune el, e construita din trei remarci (19.08, 17.08, 04.09); tabelul celor patru stari de acumulare/distributie e interpretare; diagramele arata volum brut, indicatorul lui nu se poate desena cu svg_candles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtSTyTmt6AbCL9j5ajEDm1
465 lines
27 KiB
Python
465 lines
27 KiB
Python
"""Diagramele pentru summaries/08_ANALIZA_ZILNICA/05_volum.md.
|
|
|
|
Toate prețurile și volumele sunt construite (fără ticker). Rulează:
|
|
.venv/bin/python gen/az_volum.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.
|
|
"""
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, "/workspace/atm-curs-dl")
|
|
from svg_candles import Chart as _Chart, Level, Mark, MA, from_closes # noqa: E402
|
|
|
|
|
|
def Chart(**kw):
|
|
"""lățimea crește cu numărul de bare și lasă o margine liberă în dreapta, ca etichetele
|
|
nivelurilor și SMA-20 să nu stea peste ultimele candele"""
|
|
kw["width"] = kw["left"] + (len(kw["candles"]) - 1) * kw["gap"] + 150
|
|
return _Chart(**kw)
|
|
|
|
MD = Path("/workspace/atm-curs-dl/summaries/08_ANALIZA_ZILNICA/05_volum.md")
|
|
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
|
|
STYLE = dict(gap=15, body_w=9, left=44, width=620, vol_h=70)
|
|
svgs = {}
|
|
|
|
|
|
def avg(xs):
|
|
xs = list(xs)
|
|
return sum(xs) / len(xs)
|
|
|
|
|
|
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 up_down_vol(cds, a, b):
|
|
"""volumul mediu pe barele de cumpărători vs. pe barele de vânzători, în [a, b)"""
|
|
up = [c.vol for c in cds[a:b] if c.c >= c.o]
|
|
dn = [c.vol for c in cds[a:b] if c.c < c.o]
|
|
return avg(up), avg(dn)
|
|
|
|
|
|
# ---------------------------------------------------------------- 1. breakout cu volum țeavă (DA)
|
|
base_cl = [44.0, 44.8, 44.5, 45.6, 46.4, 46.1, 47.3, 48.2, 47.9, 49.3, # 0-9 uptrend
|
|
49.0, 48.2, 48.6, 47.9, 48.8, 49.5, 49.1, 48.3, 48.0, 48.7, # 10-25 consolidare
|
|
49.4, 49.0, 48.5, 49.2, 49.7, 49.6]
|
|
base_w = [(0.3, 0.3)] * 26
|
|
base_v = [52, 55, 48, 58, 60, 50, 62, 64, 50, 60,
|
|
42, 46, 40, 44, 41, 40, 38, 42, 40, 39, 41, 38, 40, 42, 40, 39]
|
|
B = 26
|
|
da_cl = base_cl + [52.6, # 26 breakout marubozu
|
|
53.2, 52.8, 51.9, 51.4, # 27-30 pullback; 30 atinge 1/2 marubozu
|
|
52.3, 53.1, 53.8, 53.5, 54.3, 54.9, 54.6]
|
|
da_w = base_w + [(0.1, 0.1), (0.3, 0.3), (0.3, 0.3), (0.3, 0.3), (0.3, 0.3)] + [(0.3, 0.3)] * 7
|
|
da_v = base_v + [150, 72, 58, 36, 33, 52, 60, 58, 45, 62, 55, 48]
|
|
cds = from_closes(da_cl, wicks=da_w, vols=da_v, start=43.6)
|
|
RES = round(max(c.h for c in cds[7:B]), 2)
|
|
RLO = round(min(c.l for c in cds[10:B]), 2)
|
|
assert RES == 50.00 and RLO == 47.60, (RES, RLO)
|
|
assert all(c.c < RES for c in cds[:B]) and cds[B].c > RES
|
|
vavg = avg(da_v[B - 20:B])
|
|
assert da_v[B] >= 3 * vavg, "textul spune: volumul barei de breakout e peste 3x media ultimelor 20"
|
|
half = round((cds[B].o + cds[B].c) / 2, 2)
|
|
assert half == 51.10, half
|
|
PB = 30
|
|
assert abs(cds[PB].l - half) < 0.001 and all(c.l > half for c in cds[B + 1:PB])
|
|
assert avg(da_v[B + 3:PB + 1]) < vavg, "pullback-ul trebuie să aibă volum sub medie"
|
|
entry = half
|
|
sl = round(cds[B].o - 0.2, 2)
|
|
risk = entry - sl
|
|
tp = round(entry + 2 * risk, 2)
|
|
hit = next(i for i in range(PB + 1, len(cds)) if cds[i].h >= tp)
|
|
assert sl == 49.40 and tp == 54.50 and hit == 35, (sl, tp, hit)
|
|
print(f"D1 rez {RES} breakout close {cds[B].c} vol {da_v[B]} = {da_v[B] / vavg:.1f}x media; 1/2 {half}; "
|
|
f"SL {sl} risc {risk:.2f} TP {tp} atins la {hit}")
|
|
ch = Chart(candles=cds,
|
|
levels=[Level(RES, f"rezistență {RES:.2f}", ORANGE, label_at="left"),
|
|
Level(sl, f"SL {sl:.2f}", RED),
|
|
Level(entry, f"intrare {entry:.2f} (1/2 marubozu)", GREEN),
|
|
Level(tp, f"TP {tp:.2f} = 2R", BLUE)],
|
|
marks=[Mark(17, "consolidare, volum mic", GRAY, "below", dy=6),
|
|
Mark(B, "breakout", GREEN, "above", dy=-95),
|
|
Mark(PB, "retest", GREEN, "below", dy=4)],
|
|
mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
|
|
scale=40, title="Exemplu construit (fără ticker) — daily",
|
|
caption=f"Bara de breakout: volum {da_v[B] / vavg:.1f}x media ultimelor 20 de bare. Intri la retest, nu pe ea.",
|
|
**STYLE)
|
|
svg_ = ch.render()
|
|
assert abs(px_ratio(ch, sl, entry, tp) - 2.0) < 0.01
|
|
svgs["teava_da"] = svg_
|
|
|
|
# ---------------------------------------------------------------- 2. breakout fără volum (NU)
|
|
nu_cl = base_cl + [50.6, # 26 breakout fără volum
|
|
50.3, 49.3, 48.4, 47.8, 47.1, # 27-31 înapoi în range, apoi sub el
|
|
47.4, 46.6, 46.0, 46.5, 45.8, 45.3]
|
|
nu_w = base_w + [(0.2, 0.2), (0.3, 0.2)] + [(0.3, 0.3)] * 10
|
|
nu_v = base_v + [37, 34, 55, 60, 64, 72, 45, 66, 62, 44, 60, 58]
|
|
cds = from_closes(nu_cl, wicks=nu_w, vols=nu_v, start=43.6)
|
|
assert round(max(c.h for c in cds[7:B]), 2) == RES
|
|
assert cds[B].c > RES and nu_v[B] < avg(nu_v[B - 20:B]), "breakout-ul NU trebuie să aibă volum sub medie"
|
|
back = next(i for i in range(B + 1, len(cds)) if cds[i].c < RES)
|
|
assert back == B + 2, back
|
|
sl_nu = 47.40
|
|
stop = next(i for i in range(B + 1, len(cds)) if cds[i].l <= sl_nu)
|
|
assert sl_nu < RLO and cds[stop].c < RLO
|
|
assert avg(nu_v[back:stop + 1]) > avg(nu_v[B:back]) * 1.5, "la cădere volumul trebuie să crească"
|
|
print(f"D2 breakout close {cds[B].c} vol {nu_v[B]} ({nu_v[B] / avg(nu_v[B - 20:B]):.2f}x media); "
|
|
f"înapoi sub rez la {back}; SL {sl_nu} lovit la {stop}")
|
|
ch = Chart(candles=cds,
|
|
levels=[Level(RES, f"rezistență {RES:.2f}", ORANGE, label_at="left"),
|
|
Level(sl_nu, f"SL {sl_nu:.2f} lovit", RED)],
|
|
marks=[Mark(B, "breakout fără volum", RED, "above", dy=-4),
|
|
Mark(stop, "stop", RED, "below", dy=2)],
|
|
mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
|
|
scale=40, title="Exemplu construit (fără ticker) — daily",
|
|
caption=f"Aceeași bază. Bara de breakout are volum sub media ultimelor 20: nimeni mare nu cumpără.",
|
|
**STYLE)
|
|
svgs["teava_nu"] = ch.render()
|
|
|
|
# ---------------------------------------------------------------- 3. acumulare la bottom (weekly)
|
|
ac_cl = [38.0, 37.1, 37.5, 36.2, 35.3, 35.7, 34.4, 33.5, 33.9, 32.6, 31.8, 31.2, # 0-11 downtrend
|
|
31.9, 31.4, 32.3, 31.7, 31.2, 32.0, 32.6, 32.1, 31.6, 32.4, 32.9, # 12-27 bază
|
|
32.4, 32.0, 32.8, 33.1, 32.7,
|
|
34.6, 35.3, 35.0, 36.1, 36.8, 36.4, 37.5] # 28-34 breakout
|
|
ac_w = [(0.3, 0.3)] * len(ac_cl)
|
|
ac_v = [50, 62, 40, 64, 66, 42, 68, 70, 44, 72, 74, 76,
|
|
58, 30, 62, 28, 32, 64, 66, 30, 28, 68, 70, 29, 31, 72, 74, 30,
|
|
130, 80, 50, 82, 78, 48, 80]
|
|
cds = from_closes(ac_cl, wicks=ac_w, vols=ac_v, start=38.6)
|
|
BA = 28
|
|
AC_RES = round(max(c.h for c in cds[12:BA]), 2)
|
|
AC_LO = round(min(c.l for c in cds[12:BA]), 2)
|
|
u_dt, d_dt = up_down_vol(cds, 0, 12)
|
|
u_b, d_b = up_down_vol(cds, 12, BA)
|
|
assert d_dt > u_dt * 1.3, "în downtrend volumul vânzătorilor trebuie să domine"
|
|
assert u_b >= d_b * 2.0, "textul spune: în bază volumul cumpărătorilor e de peste 2x cel al vânzătorilor"
|
|
assert all(c.c < AC_RES for c in cds[12:BA]) and cds[BA].c > AC_RES
|
|
assert ac_v[BA] >= 2.5 * avg(ac_v[BA - 20:BA])
|
|
print(f"D3 bază {AC_LO}-{AC_RES}; în bază vol cumpărători {u_b:.0f} vs vânzători {d_b:.0f} = {u_b / d_b:.1f}x; "
|
|
f"breakout {cds[BA].c} vol {ac_v[BA] / avg(ac_v[BA - 20:BA]):.1f}x")
|
|
ch = Chart(candles=cds,
|
|
levels=[Level(AC_RES, f"top bază {AC_RES:.2f}", ORANGE, label_at="left"),
|
|
Level(AC_LO, f"low bază {AC_LO:.2f}", GRAY)],
|
|
marks=[Mark(0, "volum pe vânzători", RED, "below", dy=139),
|
|
Mark(20, "acumulare", GREEN, "below", dy=8),
|
|
Mark(BA, "breakout", GREEN, "above", dy=-4)],
|
|
mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
|
|
scale=26, title="Exemplu construit (fără ticker) — weekly",
|
|
caption=f"În bază: barele de cumpărători au volum mediu {u_b / d_b:.1f}x față de barele de vânzători.",
|
|
**STYLE)
|
|
svgs["acumulare"] = ch.render()
|
|
|
|
# ---------------------------------------------------------------- 4. distribuție la top (weekly, short)
|
|
di_cl = [62.0, 62.9, 62.5, 63.8, 64.7, 64.3, 65.6, 66.5, 66.1, 67.4, 68.2, 68.8, # 0-11 uptrend
|
|
68.1, 68.6, 67.7, 68.3, 68.8, 68.0, 67.4, 67.9, 68.4, 67.6, 67.1, # 12-27 top lateral
|
|
67.6, 68.0, 67.2, 66.9, 67.3,
|
|
65.3, # 28 breakdown
|
|
64.8, 65.9, 64.2, # 29-31 retest de jos
|
|
63.1, 62.4, 62.8, 61.5, 60.6, 60.0, 59.2, 58.8]
|
|
di_w = [(0.3, 0.3)] * 30 + [(0.7, 0.3), (0.3, 0.3), (0.0, 0.3)] + [(0.3, 0.3)] * 7
|
|
di_v = [62, 64, 40, 66, 68, 42, 70, 72, 44, 74, 76, 78,
|
|
66, 30, 68, 28, 30, 70, 72, 29, 28, 70, 74, 30, 28, 72, 70, 30,
|
|
135, 60, 34, 90, 80, 76, 40, 82, 78, 74, 80, 70]
|
|
cds = from_closes(di_cl, wicks=di_w, vols=di_v, start=61.4)
|
|
BD = 28
|
|
DI_HI = round(max(c.h for c in cds[12:BD]), 2)
|
|
DI_LO = round(min(c.l for c in cds[12:BD]), 2)
|
|
u_up, d_up = up_down_vol(cds, 0, 12)
|
|
u_t, d_t = up_down_vol(cds, 12, BD)
|
|
assert u_up > d_up * 1.3, "în uptrend volumul cumpărătorilor trebuie să domine"
|
|
assert d_t >= u_t * 2.0, "textul spune: la top volumul vânzătorilor e de peste 2x cel al cumpărătorilor"
|
|
assert all(c.c > DI_LO for c in cds[12:BD]) and cds[BD].c < DI_LO
|
|
assert di_v[BD] >= 2.5 * avg(di_v[BD - 20:BD])
|
|
RT = 30 # retest de jos al fostului suport
|
|
assert abs(cds[RT].h - DI_LO) <= 0.05 and cds[RT].c < DI_LO, "retestul nu atinge fostul suport"
|
|
assert di_v[RT] < avg(di_v[BD - 20:BD]), "retestul trebuie să aibă volum mic"
|
|
CF = 31
|
|
assert cds[CF].c < cds[RT].l
|
|
entry_s = cds[CF].c
|
|
sl_s = round(cds[RT].h + 0.2, 2)
|
|
risk_s = sl_s - entry_s
|
|
tp_s = round(entry_s - 2 * risk_s, 2)
|
|
hit_s = next(i for i in range(CF + 1, len(cds)) if cds[i].l <= tp_s)
|
|
print(f"D4 top {DI_LO}-{DI_HI}; la top vol vânzători {d_t:.0f} vs cumpărători {u_t:.0f} = {d_t / u_t:.1f}x; "
|
|
f"retest high {cds[RT].h:.2f}; short {entry_s} SL {sl_s} risc {risk_s:.2f} TP {tp_s} atins la {hit_s}")
|
|
ch = Chart(candles=cds,
|
|
levels=[Level(sl_s, f"SL {sl_s:.2f}", RED),
|
|
Level(entry_s, f"intrare short {entry_s:.2f}", GREEN),
|
|
Level(tp_s, f"TP {tp_s:.2f} = 2R", BLUE, label_at="left")],
|
|
marks=[Mark(0, "uptrend: volum pe cumpărători", GREEN, "below", dy=-6),
|
|
Mark(20, "distribuție", RED, "above", dy=-6),
|
|
Mark(BD, "breakdown", RED, "below", dy=30),
|
|
Mark(RT, "retest", ORANGE, "above", dy=-50),
|
|
Mark(hit_s, "TP", BLUE, "below")],
|
|
scale=22, title="Exemplu construit (fără ticker) — weekly",
|
|
caption=f"La top: barele de vânzători au volum mediu {d_t / u_t:.1f}x față de cele de cumpărători.",
|
|
**STYLE)
|
|
svg_ = ch.render()
|
|
assert abs(px_ratio(ch, sl_s, entry_s, tp_s) - 2.0) < 0.01
|
|
svgs["distributie"] = svg_
|
|
|
|
# ---------------------------------------------------------------- 5. divergență de volum la al doilea picior (DA)
|
|
dv_head = [47.0, 46.2, 46.6, 45.5, 44.6, 45.0, 43.9, 43.0, 43.4, 42.2, 41.3, # 0-10 downtrend
|
|
40.4, # 11 picior 1
|
|
41.2, 42.1, 41.8, 42.9, 43.3, # 12-16 revenire (neckline)
|
|
42.5, 41.6, 40.9, 40.1] # 17-20 coborâre spre picior 2
|
|
dv_head_w = [(0.3, 0.3)] * 11 + [(0.3, 0.6)] + [(0.3, 0.3)] * 9
|
|
dv_head_v = [50, 60, 44, 62, 64, 42, 66, 68, 44, 70, 76, 95, 60, 58, 40, 56, 52, 44, 50, 52, 56]
|
|
da2_cl = dv_head + [40.0, # 21 doji (picior 2, mai jos)
|
|
41.1, # 22 buy doji
|
|
41.6, 41.4, 42.3, 43.1, 43.6, 43.3, 44.2, 44.7, 45.3, 45.0, 45.9, 46.6]
|
|
da2_w = dv_head_w + [(0.5, 0.9), (0.2, 0.2)] + [(0.3, 0.3)] * 12
|
|
da2_v = dv_head_v + [48, 80, 66, 44, 72, 76, 70, 45, 74, 70, 72, 46, 74, 70]
|
|
cds = from_closes(da2_cl, wicks=da2_w, vols=da2_v, start=47.6)
|
|
L1, NK, L2, T = 11, 16, 21, 22
|
|
leg1, leg2 = cds[L1].l, cds[L2].l
|
|
neck = round(max(c.h for c in cds[L1:L2]), 2)
|
|
assert leg1 == min(c.l for c in cds[:L2]) and leg2 == min(c.l for c in cds), "picioarele nu sunt minimele"
|
|
assert leg2 < leg1, "al doilea picior trebuie să fie mai jos"
|
|
v1, v2 = max(da2_v[L1 - 2:L1 + 1]), max(da2_v[L2 - 3:L2 + 1])
|
|
assert v2 <= 0.6 * v1, "textul spune: la al doilea picior volumul maxim e sub 60% din primul"
|
|
assert abs(cds[L2].o - cds[L2].c) <= 0.15 and cds[T].c > cds[L2].h, "buy doji-ul nu închide peste high-ul doji-ului"
|
|
entry = cds[T].c
|
|
sl = round(leg2 - 0.1, 2)
|
|
risk = entry - sl
|
|
tp = round(entry + 2 * risk, 2)
|
|
hit = next(i for i in range(T + 1, len(cds)) if cds[i].h >= tp)
|
|
assert tp > neck, "ținta de 2R trebuie să fie peste neckline"
|
|
print(f"D5 picior1 {leg1:.2f} vol {v1}; picior2 {leg2:.2f} vol {v2} ({v2 / v1:.0%}); neck {neck}; "
|
|
f"intrare {entry} SL {sl} risc {risk:.2f} TP {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} = 2R", BLUE)],
|
|
marks=[Mark(L1, f"picior 1: {leg1:.2f}", RED, "below", dy=-2),
|
|
Mark(L2, f"picior 2: {leg2:.2f}", RED, "below", dy=12),
|
|
Mark(T, "buy doji", GREEN, "above", dy=-30)],
|
|
mas=[MA(20, GRAY, dash="4 3", text="SMA-20")],
|
|
scale=34, title="Exemplu construit (fără ticker) — daily",
|
|
caption=f"Prețul face minim nou, volumul vânzătorilor nu: la picior 2 e {v2 / v1:.0%} din picior 1.",
|
|
**STYLE)
|
|
svg_ = ch.render()
|
|
assert abs(px_ratio(ch, sl, entry, tp) - 2.0) < 0.01
|
|
svgs["divergenta_da"] = svg_
|
|
|
|
# ---------------------------------------------------------------- 6. fără divergență: bear flag (NU)
|
|
nu2_cl = dv_head + [40.0, # 21 doji (picior 2, mai jos)
|
|
41.1, # 22 buy doji fără volum
|
|
41.0, 41.4, 41.2, 41.5, # 23-26 bear flag
|
|
40.2, 39.1, 38.4, 38.8, 37.9, 37.2, 37.6, 36.8]
|
|
nu2_w = dv_head_w + [(0.5, 0.9), (0.2, 0.2)] + [(0.3, 0.3)] * 12
|
|
nu2_v = dv_head_v[:17] + [60, 78, 90, 104] + [118, 40, 30, 36, 32, 34, 112, 120, 98, 50, 104, 96, 48, 90]
|
|
cds = from_closes(nu2_cl, wicks=nu2_w, vols=nu2_v, start=47.6)
|
|
leg2n = cds[L2].l
|
|
assert leg2n < leg1 and cds[T].c > cds[L2].h
|
|
v1n, v2n = max(nu2_v[L1 - 2:L1 + 1]), max(nu2_v[L2 - 3:L2 + 1])
|
|
assert v2n >= 1.2 * v1n, "pe NU volumul la picior 2 trebuie să fie mai mare decât la picior 1"
|
|
assert nu2_v[T] < avg(nu2_v[T - 20:T]), "buy doji-ul NU are volum sub medie"
|
|
sl_n = round(leg2n - 0.1, 2)
|
|
stop_n = next(i for i in range(T + 1, len(cds)) if cds[i].l <= sl_n)
|
|
assert all(c.h < neck for c in cds[T:stop_n]) and nu2_v[stop_n] >= 3 * avg(nu2_v[T:T + 5])
|
|
print(f"D6 picior2 vol {v2n} = {v2n / v1n:.1f}x picior1; buy doji vol {nu2_v[T]}; SL {sl_n} lovit la {stop_n}")
|
|
ch = Chart(candles=cds,
|
|
levels=[Level(neck, f"neckline {neck:.2f}", ORANGE, label_at="left"),
|
|
Level(sl_n, f"SL {sl_n:.2f} lovit", RED)],
|
|
marks=[Mark(L1, "picior 1", RED, "below", dy=-2),
|
|
Mark(L2, "picior 2: vânzare mai mare", RED, "below", dy=12),
|
|
Mark(24, "bear flag", GRAY, "above", dy=-30),
|
|
Mark(stop_n, "stop", RED, "below", dy=2)],
|
|
mas=[MA(20, GRAY, dash="4 3", text="SMA-20")],
|
|
scale=34, title="Exemplu construit (fără ticker) — daily",
|
|
caption=f"Același minim nou, dar volumul vânzătorilor crește ({v2n / v1n:.1f}x). Buy doji-ul fără volum cade.",
|
|
**STYLE)
|
|
svgs["divergenta_nu"] = ch.render()
|
|
|
|
# ---------------------------------------------------------------- 7. volumul iese primul (weekly)
|
|
vc_cl = [25.0, 24.3, 24.6, 23.7, 23.0, 23.3, 22.4, 21.6, # 0-7 downtrend
|
|
21.0, 21.6, 21.2, 20.6, 21.3, 21.8, 21.1, 20.7, 21.4, 21.0, # 8-17 bază, volum mort
|
|
21.5, 21.2, 21.7, 21.4, 21.8, 21.5, 21.9, 21.6, 21.7, 21.5, 21.9, # 18-28 bază, volumul crește
|
|
23.1, # 29 breakout de preț
|
|
23.6, 23.4, 24.2, 24.8, 24.5, 25.3, 25.1, 25.9, 26.6, 26.3, 27.1, 27.6]
|
|
vc_w = [(0.3, 0.3)] * len(vc_cl)
|
|
vc_v = [40, 46, 30, 48, 50, 30, 52, 54,
|
|
30, 28, 26, 30, 27, 26, 28, 29, 26, 27,
|
|
44, 26, 52, 28, 58, 30, 64, 30, 66, 29, 72,
|
|
110, 80, 50, 84, 82, 52, 86, 50, 84, 82, 48, 86, 84]
|
|
cds = from_closes(vc_cl, wicks=vc_w, vols=vc_v, start=25.6)
|
|
BP = 29
|
|
VC_RES = round(max(c.h for c in cds[8:BP]), 2)
|
|
dead_max = max(vc_v[8:18])
|
|
vfirst = next(i for i in range(18, BP) if vc_v[i] > dead_max * 1.5)
|
|
assert VC_RES == 22.20 and all(c.c < VC_RES for c in cds[8:BP]) and cds[BP].c > VC_RES, VC_RES
|
|
assert BP - vfirst >= 8, "volumul trebuie să iasă cu cel puțin 8 bare înaintea prețului"
|
|
ups = [vc_v[i] for i in range(18, BP) if cds[i].c >= cds[i].o]
|
|
assert ups == sorted(ups) and ups[-1] >= 2.4 * dead_max, "volumul cumpărătorilor în bază trebuie să urce treaptă cu treaptă"
|
|
hl = round(min(c.l for c in cds[22:BP]), 2)
|
|
assert hl > round(min(c.l for c in cds[8:18]), 2), "low-urile din a doua parte a bazei trebuie să fie mai sus"
|
|
entry = cds[BP].c
|
|
sl = round(hl - 0.1, 2)
|
|
risk = entry - sl
|
|
tp = round(entry + 2 * risk, 2)
|
|
hit = next(i for i in range(BP + 1, len(cds)) if cds[i].h >= tp)
|
|
print(f"D7 rez {VC_RES}; volum mort max {dead_max}; volumul iese la {vfirst} (vol {vc_v[vfirst]}), prețul la {BP}; "
|
|
f"ultimul vol cumpărători în bază {ups[-1]} = {ups[-1] / dead_max:.1f}x; intrare {entry} SL {sl} "
|
|
f"risc {risk:.2f} TP {tp} atins la {hit}")
|
|
ch = Chart(candles=cds,
|
|
levels=[Level(VC_RES, f"rezistență {VC_RES:.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} = 2R", BLUE)],
|
|
marks=[Mark(12, "volum mort", GRAY, "below", dy=40),
|
|
Mark(vfirst + 4, "volumul urcă", GREEN, "above", dy=-50),
|
|
Mark(BP, "prețul iese", GREEN, "above", dy=-4)],
|
|
mas=[MA(20, GRAY, dash="4 3", text="SMA-20")],
|
|
scale=34, title="Exemplu construit (fără ticker) — weekly",
|
|
caption=f"Volumul cumpărătorilor urcă {BP - vfirst} săptămâni sub rezistență, înainte ca prețul să iasă.",
|
|
**STYLE)
|
|
svg_ = ch.render()
|
|
assert abs(px_ratio(ch, sl, entry, tp) - 2.0) < 0.01
|
|
svgs["volum_conduce"] = svg_
|
|
|
|
# ---------------------------------------------------------------- 8. monthly: contextul (volum pozitiv)
|
|
mo_cl = [30.0, 31.2, 30.6, 32.1, 33.4, 32.8, 34.5, 35.6, 35.0, 36.8, 38.0, # 0-10 uptrend 1
|
|
37.2, 36.5, 37.0, 36.1, 36.8, # 11-15 consolidare
|
|
38.4, 39.6, 40.9, 40.2, 41.8, 43.0, # 16-21 breakout, vârf
|
|
42.1, 41.3, 41.8, 40.9, 41.5, 42.4] # 22-27 pullback
|
|
mo_w = [(0.4, 0.4)] * len(mo_cl)
|
|
mo_v = [60, 70, 40, 72, 76, 42, 78, 80, 44, 82, 84,
|
|
40, 38, 50, 36, 48,
|
|
96, 88, 90, 44, 92, 94,
|
|
36, 34, 58, 32, 60, 64]
|
|
cds = from_closes(mo_cl, wicks=mo_w, vols=mo_v, start=29.4)
|
|
CRIME = round(max(c.h for c in cds[8:16]), 2)
|
|
PEAK = round(max(c.h for c in cds), 2)
|
|
assert CRIME == 38.40 and PEAK == 43.40, (CRIME, PEAK)
|
|
u_m, d_m = up_down_vol(cds, 0, len(cds))
|
|
u_pb, d_pb = up_down_vol(cds, 22, len(cds))
|
|
assert u_m >= 1.6 * d_m, "pe monthly volumul cumpărătorilor trebuie să domine"
|
|
assert d_pb < avg(mo_v[16:22]) * 0.5, "pullback-ul monthly are volum de vânzare mic"
|
|
print(f"D8 monthly: vol cumpărători {u_m:.0f} vs vânzători {d_m:.0f} = {u_m / d_m:.1f}x; locul crimei {CRIME}; vârf {PEAK}")
|
|
ch = Chart(candles=cds,
|
|
levels=[Level(CRIME, f"fost vârf {CRIME:.2f}", ORANGE, label_at="left")],
|
|
marks=[Mark(12, "consolidare", GRAY, "below", dy=4),
|
|
Mark(13, "breakout cu volum", GREEN, "above", dy=-35),
|
|
Mark(24, "pullback, volum mic", GRAY, "above", dy=-20)],
|
|
mas=[MA(20, GRAY, dash="4 3", text="SMA-20")],
|
|
scale=14, title="Exemplu construit (fără ticker) — monthly",
|
|
caption=f"Monthly: barele de cumpărători au volum mediu {u_m / d_m:.1f}x față de barele de vânzători.",
|
|
**STYLE)
|
|
svgs["monthly"] = ch.render()
|
|
|
|
# ---------------------------------------------------------------- 9. weekly: volumul deteriorat, retestul locului crimei
|
|
wk_cl = [36.2, 36.8, 36.4, 37.1, 36.7, 37.3, 37.9, 38.1, # 0-7 sub fostul vârf
|
|
39.5, 40.4, 40.0, 41.2, 41.9, 41.5, 42.4, 43.0, 42.6, 43.2, # 8-17 raliu
|
|
42.4, 41.7, 41.0, 40.4, 39.8, 39.4, 39.3, # 18-24 pullback
|
|
39.5, # 25 hammer în locul crimei
|
|
39.8, # 26 confirmare
|
|
40.5, 41.2, 40.9, 41.8, 42.5, 42.2, 43.1, 43.6]
|
|
wk_w = [(0.3, 0.3)] * 25 + [(0.1, 0.9), (0.1, 0.2)] + [(0.3, 0.3)] * 8
|
|
wk_v = [40, 44, 34, 46, 32, 44, 48, 50,
|
|
90, 84, 50, 88, 86, 48, 82, 80, 46, 78,
|
|
44, 42, 40, 38, 36, 34, 32,
|
|
58, 70, 72, 76, 44, 74, 78, 46, 76, 80]
|
|
cds = from_closes(wk_cl, wicks=wk_w, vols=wk_v, start=36.6)
|
|
HM, CFW = 25, 26
|
|
assert abs(cds[HM].l - CRIME) < 0.001, "codița hammer-ului trebuie să atingă exact locul crimei"
|
|
assert cds[HM].l == min(c.l for c in cds[18:]) and round(max(c.h for c in cds[:8]), 2) == CRIME, "hammer-ul e low-ul pullback-ului"
|
|
rally_v, pb_v = avg(wk_v[8:18]), avg(wk_v[18:25])
|
|
assert pb_v <= 0.6 * rally_v, "textul spune: volumul pe pullback e sub 60% din cel de pe raliu"
|
|
assert pb_v > 0, "volumul rămâne (pozitiv)"
|
|
assert cds[CFW].c > cds[HM].h
|
|
top = round(max(c.h for c in cds[8:18]), 2)
|
|
entry = cds[CFW].c
|
|
sl = round(cds[HM].l - 0.3, 2)
|
|
risk = entry - sl
|
|
R = (top - entry) / risk
|
|
hit = next(i for i in range(CFW + 1, len(cds)) if cds[i].h >= top)
|
|
assert R >= 2.0, R
|
|
print(f"D9 weekly: raliu vol {rally_v:.0f}, pullback vol {pb_v:.0f} ({pb_v / rally_v:.0%}); hammer low {cds[HM].l:.2f}; "
|
|
f"intrare {entry} SL {sl} risc {risk:.2f} TP {top} = {R:.2f}R atins la {hit}")
|
|
ch = Chart(candles=cds,
|
|
levels=[Level(CRIME, f"locul crimei {CRIME:.2f}", ORANGE, label_at="left"),
|
|
Level(sl, f"SL {sl:.2f}", RED),
|
|
Level(entry, f"intrare {entry:.2f}", GREEN),
|
|
Level(top, f"TP {top:.2f} = {R:.1f}R", BLUE)],
|
|
marks=[Mark(24, "volum în scădere", GRAY, "above", dy=-100),
|
|
Mark(HM, "hammer", GREEN, "below", dy=12)],
|
|
mas=[MA(20, GRAY, dash="4 3", text="SMA-20")],
|
|
scale=60, title="Exemplu construit (fără ticker) — weekly",
|
|
caption="Weekly-ul arată vânzare, dar pe volum mic: e retestul, nu ieșirea instituționalilor.",
|
|
**STYLE)
|
|
svg_ = ch.render()
|
|
assert abs(px_ratio(ch, sl, entry, top) - R) < 0.01
|
|
svgs["weekly_retest"] = svg_
|
|
|
|
# ---------------------------------------------------------------- 10-11. doi candidați din același sector
|
|
sec_cl = [58.0, 57.1, 57.6, 56.2, 55.3, 55.8, 54.4, 53.5, 54.0, 52.6, 51.6, # 0-10 downtrend
|
|
50.6, # 11 picior 1
|
|
51.5, 52.4, 52.0, 53.1, 52.6, # 12-16 revenire
|
|
51.8, 51.0, 50.3, # 17-19 coborâre
|
|
50.2, # 20 picior 2 (mai jos)
|
|
51.2, 51.0, 51.9, 51.6, 52.3] # 21-25 până la decizie
|
|
DEC = len(sec_cl) - 1 # 25 bara de decizie
|
|
SUP = 50.00
|
|
sec_w = [(0.3, 0.3)] * 11 + [(0.3, 0.6)] + [(0.3, 0.3)] * 8 + [(0.3, 0.5)] + [(0.3, 0.3)] * 5
|
|
a_cl = sec_cl + [53.4, 54.1, 53.8, 54.9, 55.6, 55.2, 56.4, 57.1, 57.8, 58.6]
|
|
b_cl = sec_cl + [51.8, 51.1, 51.6, 50.7, 49.9, 50.3, 49.4, 48.8, 49.2, 48.5]
|
|
tail_w = [(0.3, 0.3)] * 10
|
|
a_v = [50, 60, 42, 62, 64, 42, 66, 68, 44, 70, 74, 90, 58, 60, 32, 56, 30, 30, 34, 36, 38,
|
|
66, 30, 70, 28, 74] + [86, 80, 44, 84, 82, 46, 84, 80, 78, 80]
|
|
b_v = [50, 60, 42, 62, 64, 42, 66, 68, 44, 70, 74, 90, 40, 38, 58, 36, 60, 64, 70, 80, 98,
|
|
40, 62, 38, 60, 36] + [72, 76, 40, 80, 84, 42, 86, 88, 40, 90]
|
|
ca = from_closes(a_cl, wicks=sec_w + tail_w, vols=a_v, start=58.6)
|
|
cb = from_closes(b_cl, wicks=sec_w + tail_w, vols=b_v, start=58.6)
|
|
assert [(c.o, c.h, c.l, c.c) for c in ca[:DEC + 1]] == [(c.o, c.h, c.l, c.c) for c in cb[:DEC + 1]], \
|
|
"până la decizie prețul trebuie să fie identic"
|
|
p1, p2 = ca[11].l, ca[20].l
|
|
assert p2 < p1 and abs(p2 - SUP) < 0.35 and abs(p1 - SUP) < 0.35, (p1, p2)
|
|
ua, da_ = up_down_vol(ca, 12, DEC + 1)
|
|
ub, db = up_down_vol(cb, 12, DEC + 1)
|
|
assert ua >= 1.8 * da_ and db >= 1.8 * ub, "A = acumulare, B = distribuție, după picior 1"
|
|
assert a_v[20] <= 0.6 * a_v[11] and b_v[20] > b_v[11], "A are divergență la picior 2, B nu"
|
|
entry = ca[DEC].c
|
|
sl = round(p2 - 0.4, 2)
|
|
risk = entry - sl
|
|
tp = round(entry + 2 * risk, 2)
|
|
hit = next(i for i in range(DEC + 1, len(ca)) if ca[i].h >= tp)
|
|
stop_b = next(i for i in range(DEC + 1, len(cb)) if cb[i].l <= sl)
|
|
print(f"D10/11 picior1 {p1:.2f} picior2 {p2:.2f}; A: cumpărători {ua:.0f} vs vânzători {da_:.0f} ({ua / da_:.1f}x), "
|
|
f"picior2 vol {a_v[20] / a_v[11]:.0%}; B: vânzători {db:.0f} vs cumpărători {ub:.0f} ({db / ub:.1f}x); "
|
|
f"intrare A {entry} SL {sl} risc {risk:.2f} TP {tp} atins la {hit}; B ar fi lovit SL la {stop_b}")
|
|
for key, cds, vtxt, extra_marks, cap in (
|
|
("sector_a", ca, "acumulare", [],
|
|
f"Candidatul A: după picior 1, volumul cumpărătorilor e {ua / da_:.1f}x cel al vânzătorilor."),
|
|
("sector_b", cb, "distribuție", [Mark(stop_b, "ar fi lovit SL", RED, "above", dy=-30)],
|
|
f"Candidatul B, același preț: volumul vânzătorilor e {db / ub:.1f}x cel al cumpărătorilor.")):
|
|
ch = Chart(candles=cds,
|
|
levels=[Level(SUP, f"suport de an {SUP:.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} = 2R", BLUE)],
|
|
marks=[Mark(11, "picior 1", RED, "below", dy=14),
|
|
Mark(20, "picior 2", RED, "below", dy=14),
|
|
Mark(DEC, "decizia", GRAY, "above", dy=-30),
|
|
Mark(16, vtxt, GREEN if key == "sector_a" else RED, "above", dy=-24)] + extra_marks,
|
|
scale=34, title=f"Exemplu construit (fără ticker) — weekly, candidat {key[-1].upper()}",
|
|
caption=cap, **STYLE)
|
|
svg_ = ch.render()
|
|
assert abs(px_ratio(ch, sl, entry, tp) - 2.0) < 0.01
|
|
svgs[key] = svg_
|
|
|
|
|
|
# ---------------------------------------------------------------- substituție în .md
|
|
if MD.exists():
|
|
md = MD.read_text(encoding="utf-8")
|
|
for name, svg in svgs.items():
|
|
assert re.search(rf"<!--SVG:{name}-->", md), f"marcatorul {name} lipsește din .md"
|
|
md = re.sub(rf"<!--SVG:{name}-->(?:\n<svg.*?</svg>\n<!--/SVG-->)?",
|
|
lambda _m, name=name, svg=svg: f"<!--SVG:{name}-->\n{svg}\n<!--/SVG-->", md, flags=re.S)
|
|
MD.write_text(md, encoding="utf-8")
|
|
print("scris", MD)
|