"""Diagrama de ansamblu MDSRT long (strategii, marcajul `ansamblu`).
„SPY — 5 min: al doilea picior mai jos la 589.80, sub suportul 590.00 →
divergență bull (M) preț-vs-volum → candela-trigger Buy Hammer/Doji →
intrare 590.20 → SL 589.40 (risc 0.80) → TP1 591.00 (1:1) → poziția rămasă
pe trail, ieșire la close 592.40 (≈ 2.75R)."
Toate close-urile, wick-urile speciale și nivelurile vin din
`gen/strategii/mdsrt_ansamblu.spec.txt`. Structura M→A→P, suportul 590.00,
minimul 589.80 și riscul 0.80 sunt din exemplul cursului
(`05_MODUL_4/02_strategia_5_mdsrt.md:159-177, 246, 308-311`). Intrare/SL/TP
sunt exemplul construit al autorului sumarului, marcat explicit ca NU citat
din curs (`...:166-177, 439-445`); transcrierea se întrerupe înainte de
planul numeric. Volumele sunt relative, alese de generator (materialul nu dă
volum pentru acest exemplu).
Rulează:
python3 gen/strategii/mdsrt_ansamblu.py
Scrie lângă el `mdsrt_ansamblu.svg`.
"""
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
OUT = Path(__file__).resolve().parent / "mdsrt_ansamblu.svg"
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
# ---------------------------------------------------------------- seria (30 bare)
CLOSES = [
596.00, 595.20, 595.60, 594.80, 594.10, 593.40, 593.70, 592.90, # 0-7
592.20, 591.60, 591.90, 591.30, 590.80, 590.30, 590.55, 590.10, # 8-15
589.85, 590.05, # 16-17
589.80, # 18 = al doilea picior mai jos + candela-trigger (hammer)
590.20, # 19 = intrare (close = entry; sparge high-ul trigger-ului)
590.60, 590.90, 591.00, # 20-22 (22 = TP1 1:1)
591.30, 591.10, 591.45, 591.70, 591.95, 592.15, # 23-28
592.40, # 29 = ieșire pe trail (close)
]
assert len(CLOSES) == 30, len(CLOSES)
# fitile (sus, jos): (0.25, 0.30) peste tot, cu excepțiile din spec.
WICKS = [(0.25, 0.30)] * 30
WICKS[18] = (0.10, 0.40) # hammer: high 590.15, low 589.40 (= SL)
WICKS[19] = (0.10, 0.05) # intrare: high 590.30, low 589.75
# bara 21 coborâtă sub TP1 (high 590.95 < 591.00), ca TP1 să fie atins exact
# prima dată la bara 22 (altfel assert-ul 5 ar pica pe „primul high").
WICKS[21] = (0.05, 0.30)
# volum relativ (ales de generator): 16-19 crescător, 18-19 peste medie,
# vol[18] > vol[10] (higher low pe volum la lower low pe preț).
VOLS = [
48, 46, 44, 45, 43, 41, 40, 42, 44, 46, # 0-9
52, 38, 40, 42, 39, 44, # 10-15
50, 58, 72, 85, # 16-19 absorbție la minim
70, 64, 58, 60, 55, 52, 50, 48, 46, 44, # 20-29
]
assert len(VOLS) == 30, len(VOLS)
cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=596.60)
# ---------------------------------------------------------------- assert-urile din spec
entry = cds[19].c
sl = cds[18].l
tp1 = cds[22].c
# 1. minimul și SL-ul pe bara 18
assert cds[18].c == 589.80, cds[18].c
assert cds[18].l == 589.40, cds[18].l
# 2. al doilea picior rupe suportul/pivotul 590.00
assert cds[18].l < 590.00, cds[18].l
# 3. intrarea = close-ul barei 19 și sparge high-ul trigger-ului
assert cds[19].c == 590.20, cds[19].c
assert cds[19].c == entry
assert cds[19].c > cds[18].h, (cds[19].c, cds[18].h)
# 4. TP1 exact 1:1
assert abs((entry - sl) - (tp1 - entry)) < 1e-9, (entry - sl, tp1 - entry)
assert abs((entry - sl) - 0.80) < 1e-9
# 5. TP1 atins la bara 22 (și nu mai devreme)
assert cds[22].c == 591.00, cds[22].c
assert cds[22].c == tp1
assert max(c.h for c in cds[20:22]) < tp1, [c.h for c in cds[20:22]]
# 6. ieșirea pe trail e pe profit
assert cds[29].c == 592.40, cds[29].c
assert cds[29].c > entry, (cds[29].c, entry)
# 7. hammer: corp mic, coadă lungă în jos
body = abs(cds[18].o - cds[18].c)
rng = cds[18].h - cds[18].l
tail = min(cds[18].o, cds[18].c) - cds[18].l
assert body == 0.25, body
assert body <= 0.5 * rng, (body, rng)
assert tail > body, (tail, body)
# 8. downtrend vizibil
assert cds[13].c < cds[0].c - 5, (cds[13].c, cds[0].c)
# 9. divergență pe volum: lower low pe preț, higher low pe volum
assert cds[18].l < cds[10].l, (cds[18].l, cds[10].l)
assert VOLS[18] > VOLS[10], (VOLS[18], VOLS[10])
# barele 16-19 au volum crescător; 18-19 peste media seriei
assert VOLS[16] < VOLS[17] < VOLS[18] < VOLS[19], VOLS[16:20]
avg_vol = sum(VOLS) / len(VOLS)
assert VOLS[18] > avg_vol and VOLS[19] > avg_vol, (VOLS[18], VOLS[19], avg_vol)
# 10. fiecare open == close-ul barei anterioare
for a, b in zip(cds, cds[1:]):
assert a.c == b.o, (a.c, b.o)
r_mult = (cds[29].c - entry) / (entry - sl)
assert abs(r_mult - 2.75) < 1e-9, r_mult
# ---------------------------------------------------------------- niveluri, marks
levels = [
Level(590.00, "suport / pivot 590.00", BLUE, label_at="left"),
Level(sl, f"SL {sl:.2f} — risc 0.80", RED, label_at="left"),
# stânga: altfel eticheta din dreapta se calcă pe textul „SMA-20" al mediei
Level(tp1, f"TP1 {tp1:.2f} = 1:1", GREEN, label_at="left"),
]
marks = [
Mark(18, f"minim 589.80 — divergență (M)", RED, "below"),
# ridicat deasupra vârfurilor candelelor de urcare (altfel textul cade peste
# fitilele barelor 20-22) — bara 19 e totuși exact sub el
Mark(19, f"intrare {entry:.2f}", GREEN, "above", dy=-46),
Mark(29, f"ieșire trail {cds[29].c:.2f}", GREEN, "above"),
]
# titlul e injectat pe două linii: textul din spec (82 caractere) nu încape
# pe un singur rând în 640 px fără să fie tăiat la margine
TITLE = ["SPY — 5 min (exemplu construit pe scara lecției;",
"cifrele planului nu vin din curs)"]
ch = Chart(
candles=cds, levels=levels, marks=marks,
mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
scale=48.0, gap=15, body_w=9, left=44, width=640, top=64, vol_h=70,
vol_label="volum",
title="", caption="",
)
svg = ch.render()
title_svg = "\n ".join(
f'{t}'
for i, t in enumerate(TITLE))
i = svg.index(">") + 1
svg = svg[:i] + "\n " + title_svg + svg[i:]
# ---------------------------------------------------------------- legenda (onestitate)
m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg)
W, H0 = int(m.group(1)), float(m.group(2))
legend = [
"Legendă: entry/SL/TP/trail sunt exemplul construit al autorului sumarului,",
"nu citate din curs (transcrierea se întrerupe înainte de planul numeric).",
]
legend_svg = [f'{t}'
for i, t in enumerate(legend)]
new_h = H0 + 16 + 17 * (len(legend) - 1) + 14
svg = re.sub(r'(viewBox="0 0 \d+ )[0-9.]+(")',
lambda mm: mm.group(1) + f"{new_h:.0f}" + mm.group(2), svg, count=1)
svg = svg.replace("\n", "\n " + "\n ".join(legend_svg) + "\n")
assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
assert f"TP1 {tp1:.2f}" in svg and f"intrare {entry:.2f}" in svg
OUT.write_text(svg)
print(f"scris {OUT.name}: {len(cds)} candele, suport 590.00, minim {cds[18].l:.2f}, "
f"intrare {entry:.2f}, SL {sl:.2f} (risc 0.80), TP1 {tp1:.2f} (1:1), "
f"ieșire {cds[29].c:.2f} = {r_mult:.2f}R, vol[18]={VOLS[18]} > vol[10]={VOLS[10]}")