"""MDSRT — diagrama de ansamblu (Nivelul 1) și seria canonică a capitolului.
Aici trăiește singurul exemplu construit al capitolului: SPY pe 5 minute,
suport/pivot daily 590.00, piciorul 1 la 590.10 (ține nivelul), piciorul 2 la
589.50 (îl rupe, cu divergență pe volum), candela-trigger = Buy Hammer,
intrare 590.20, SL 589.40, risc 0.80, TP1 591.00 (1:1), ieșire pe trail la
592.40 (2.75R).
Celelalte generatoare de preț ale capitolului (picior, trigger, stop, iesire)
IMPORTĂ seria de aici. Așa o regulă numerică — marja stopului, distanța până
la țintă — nu poate să difere de la o diagramă la alta.
Regulile numerice ale capitolului, definite o singură dată, aici:
MARJA = 0.10 — stopul stă cu atât sub low-ul piciorului 2
intrarea = close-ul candelei care sparge high-ul candelei-trigger
TP1 = intrare + (intrare − SL)
Ce e din curs și ce nu: structura M→A→P e a cursului; suportul 590.00 și
minimul piciorului 2 vin din exemplul construit al autorului sumarului
(`05_MODUL_4/02_strategia_5_mdsrt.md:166-177, 439-445`), nu din transcriere —
transcrierea se întrerupe înainte de planul numeric.
Rulează: python3 gen/strategii/mdsrt_ansamblu.py
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Chart, Level, Mark, MA, Zone, Arrow, from_closes # noqa: E402
OUT = Path(__file__).with_suffix(".svg")
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
# ------------------------------------------------- regulile numerice ale capitolului
MARJA = 0.10 # stopul, sub low-ul piciorului 2
SUPORT = 590.00 # pivotul daily (nivelul de pe TF-ul mare)
LEG1, TRIG, CONF = 10, 18, 19 # indicii: piciorul 1, candela-trigger, candela de intrare
STOP_BARS = 4 # low-ul se caută pe ultimele 4 candele, ca la Buy Doji
CLOSES = [
596.00, 595.20, 595.60, 594.80, 594.10, 593.40, 593.70, 592.90, # 0-7
592.20, 591.60,
590.40, # 10 = piciorul 1
590.90, 591.40, 591.10, 591.30, 590.90, 590.50, 590.05, # 11-17
589.80, # 18 = piciorul 2 (Buy Hammer)
590.20, # 19 = intrarea
590.60, 590.90, 591.00, # 20-22 (22 = TP1)
591.30, 591.10, 591.45, 591.70, 591.95, 592.15, # 23-28
592.80, 593.10, 592.90, # 29-31 = ultimul impuls
592.40, # 32 = ieșire pe trail
]
WICKS = [(0.25, 0.30)] * 33
WICKS[LEG1] = (0.25, 0.30) # piciorul 1: low 590.10, deasupra suportului
WICKS[16] = (0.20, 0.15)
WICKS[17] = (0.15, 0.10) # low 589.95 — prețul intră în zonă, fără să facă minim nou
WICKS[TRIG] = (0.10, 0.30) # hammer: high 590.15, low 589.50
WICKS[CONF] = (0.10, 0.05)
WICKS[20] = (0.25, 0.15) # retestul nivelului ține: low 590.05, peste pivot
WICKS[21] = (0.05, 0.15) # high 590.95 < TP1: ținta e atinsă prima dată la bara 22
VOLS = [
48, 46, 44, 45, 43, 41, 40, 42, 44, 46, # 0-9
52, # 10 = volumul piciorului 1
38, 40, 42, 39, 44, 50, 58, # 11-17
88, 80, # 18-19 absorbție la minim
70, 64, 58, 60, 55, 52, 50, 48, 46, # 20-28
52, 48, 42, 46, # 29-32
]
assert len(CLOSES) == len(WICKS) == len(VOLS) == 33
EXIT_I = 32 # bara pe care se închide poziția, sub linia de trail
def bull_trail(cds, start):
"""Bull Trail, definiția cursului: low-ul fiecărei candele care se închide
peste high-ul candelei dinainte; linia urcă, nu coboară niciodată.
Întoarce lista [(index, nivel)] de la bara `start` încolo."""
out, cur = [], None
for i in range(start, len(cds)):
if cds[i].c > cds[i - 1].h:
cur = round(cds[i].l, 2) if cur is None else max(cur, round(cds[i].l, 2))
if cur is not None:
out.append((i, cur))
return out
def serie():
"""Seria canonică a capitolului, 33 de candele de 5 minute."""
return from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=596.60)
cds = serie()
leg1_l = cds[LEG1].l
leg2_l = cds[TRIG].l
ENTRY = cds[CONF].c
SL = round(leg2_l - MARJA, 2)
R = round(ENTRY - SL, 2)
TP1 = round(ENTRY + R, 2)
# ------------------------------------------------------------------ assert-urile
for i, c in enumerate(cds):
assert c.h >= max(c.o, c.c) and c.l <= min(c.o, c.c), f"bara {i}: h/l incoerent"
if i:
assert abs(c.o - cds[i - 1].c) < 1e-9, f"open[{i}] != close[{i-1}]"
# 1. piciorul 1 ține nivelul, piciorul 2 îl rupe — asta e povestea capitolului
assert leg1_l == 590.10 and leg1_l > SUPORT, leg1_l
assert leg2_l == 589.50 and leg2_l < SUPORT, leg2_l
assert leg2_l < leg1_l, (leg2_l, leg1_l)
# 2. cele două picioare sunt minimele locale, nu doar două bare oarecare
assert leg1_l == min(c.l for c in cds[:LEG1 + 6]), "piciorul 1 nu e minimul zonei lui"
assert leg2_l == min(c.l for c in cds), "piciorul 2 nu e minimul seriei"
# 3. distanța dintre picioare depășește minimul de 5 candele cerut de curs
assert TRIG - LEG1 == 8, TRIG - LEG1
# 4. hammer: corp mic sus, coadă lungă în jos
body = abs(cds[TRIG].o - cds[TRIG].c)
rng = cds[TRIG].h - cds[TRIG].l
tail = min(cds[TRIG].o, cds[TRIG].c) - cds[TRIG].l
assert body <= 0.5 * rng and tail > body, (body, rng, tail)
# 5. intrarea sparge high-ul candelei-trigger (regula de intrare a capitolului)
assert ENTRY == 590.20 and ENTRY > cds[TRIG].h, (ENTRY, cds[TRIG].h)
# 6. stopul: marja sub cel mai jos low din ultimele 4 candele
low4 = min(c.l for c in cds[CONF - STOP_BARS + 1:CONF + 1])
assert low4 == leg2_l, (low4, leg2_l)
assert SL == 589.40 and R == 0.80, (SL, R)
# 7. TP1 e 1:1 exact și e atins prima dată la bara 22
assert abs((ENTRY - SL) - (TP1 - ENTRY)) < 1e-9
assert TP1 == 591.00
assert next(i for i in range(CONF + 1, len(cds)) if cds[i].h >= TP1 - 1e-9) == 22
# 8. downtrend vizibil înainte de piciorul 1
assert cds[LEG1].c < cds[0].c - 5, (cds[LEG1].c, cds[0].c)
# 9. divergența: lower low pe preț, higher low pe volum
assert cds[TRIG].l < cds[LEG1].l and VOLS[TRIG] > VOLS[LEG1]
assert VOLS[16] < VOLS[17] < VOLS[TRIG] == max(VOLS)
# 10. după intrare, retestul ține: nicio bară nu mai închide sub pivot
assert all(c.c > SUPORT for c in cds[CONF + 1:]), "prețul recade sub pivot după intrare"
assert min(c.l for c in cds[CONF + 1:]) > SUPORT, "o bară de după intrare rupe iar pivotul"
# 11. ieșirea pe trail: ultimul swing low înainte de întoarcere e linia de trail,
# iar bara de ieșire e prima care se închide sub ea
_trail = bull_trail(cds, CONF)
TRAIL = dict(_trail)[EXIT_I] # nivelul liniei de trail când vine bara de ieșire
assert TRAIL == 592.50, TRAIL
assert [p for _, p in _trail] == sorted(p for _, p in _trail), "linia de trail coboară"
assert all(cds[i].c > p for i, p in _trail if i < EXIT_I), "trail-ul era rupt mai devreme"
assert cds[EXIT_I].c == 592.40 < TRAIL, (cds[EXIT_I].c, TRAIL)
r_mult = round((cds[EXIT_I].c - ENTRY) / R, 2)
assert r_mult == 2.75, r_mult
if __name__ == "__main__":
levels = [
Level(SUPORT, f"pivot daily {SUPORT:.2f} — zona (M)", BLUE, label_at="left"),
Level(SL, f"SL {SL:.2f} = 0.10 sub piciorul 2 (risc {R:.2f})", RED),
Level(TP1, f"TP1 {TP1:.2f} = 1:1", GREEN, label_at="left"),
]
marks = [
Mark(LEG1, f"piciorul 1: low {leg1_l:.2f}", GRAY, "below"),
Mark(TRIG, f"piciorul 2: low {leg2_l:.2f}", RED, "below", dy=16),
]
arrows = [
Arrow(TRIG, "Buy Hammer: coadă lungă în jos\nvânzătorii au dus prețul sub nivel\nși nu l-au putut ține acolo",
where="below", price=leg2_l, color=RED, dx=210, dy=58),
Arrow(CONF, f"intrare {ENTRY:.2f}: intri abia aici,\nla close peste high-ul candelei-trigger",
where="above", color=GREEN, dx=40, dy=-44),
Arrow(EXIT_I, f"ieșire pe trail {cds[EXIT_I].c:.2f} = {r_mult:.2f}R\nprima închidere sub linia de trail",
where="right", color=GREEN, dy=-6),
]
ch = Chart(
candles=cds, levels=levels, marks=marks, zones=[
Zone(LEG1, CONF, "M (zonă + divergență) → A (trigger) → P (plan)", BLUE)],
arrows=arrows,
mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
scale=48.0, gap=15, body_w=9, left=44, width=660, top=74, vol_h=70,
vol_label="volum: la piciorul 2, volum mai mare decât la piciorul 1 = divergență",
)
svg = ch.render()
TITLE = ["SPY — 5 min (exemplu construit pe scara lecției;",
"cifrele planului nu vin din curs)"]
title_svg = "\n ".join(
f'{t}'
for i, t in enumerate(TITLE))
i = svg.index(">") + 1
svg = svg[:i] + "\n " + title_svg + svg[i:]
m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg)
W, H0 = int(m.group(1)), float(m.group(2))
legend = [
"Legendă: structura M → A → P e a cursului; nivelurile (590.00, 589.50) și planul",
"(intrare, SL, TP) sunt exemplul construit al autorului sumarului, nu citate din curs.",
]
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 "\n\n" not in svg, "linie goală în SVG"
OUT.write_text(svg)
print(f"scris {OUT.name}: picior 1 {leg1_l:.2f} / picior 2 {leg2_l:.2f} sub {SUPORT:.2f}, "
f"intrare {ENTRY:.2f}, SL {SL:.2f} (risc {R:.2f}), TP1 {TP1:.2f}, ieșire {r_mult:.2f}R")