Files
atm-curs-dl/gen/strategii/selectia_ansamblu.py
Claude Agent ccfc4a7f7f strategii: diagramele puse in capitole
36 din 37 de diagrame substituite in .md. substituie.py accepta acum si
cheia cu slug inclus (unii agenti au scris mp2_exemplu, altii exemplu) si
marcajul indentat. A ramas mdsrt_aplus: agentul a raportat OK fara sa scrie
niciun fisier — exact capcana din CLAUDE.md, un raport de agent nu e dovada.
Relansat separat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-18 19:34:11 +00:00

163 lines
7.7 KiB
Python

"""ULTA — daily (pivoți monthly): diagrama de ansamblu a selecției (TIP: PRET).
Citește `gen/strategii/selectia_ansamblu.spec.txt`. Un simbol care vine dintr-un
downtrend weekly, atinge suportul monthly 355.48 și face un buy doji la 373.30
cu RVOL 0.61; apoi confirmarea (intrare 374.80) și drumul spre pivot/TP1 383.74,
respectiv TP2 402.00. Grafic daily, 32 de candele, serie continuă, cu panou de
volum și SMA-20 — construit prin `svg_candles` (from_closes), la scară.
Rulează:
python3 gen/strategii/selectia_ansamblu.py
Scrie lângă el `selectia_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 / "selectia_ansamblu.svg"
GRAY, RED, GREEN, BLUE, ORANGE, TEAL = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000", "#00a0aa"
# ---------------------------------------------------------------- seria (32 bare)
# bare 0-11 : downtrend vizibil de la ~395 la ~362 (~0.8%/bara)
# bare 12-19 : baza/churn 356-373, minimul atinge suportul 355.48 pe bara 15
# bara 20 : buy doji, close 373.30 (open = close[19] = 373.00, corp 0.30)
# bara 21 : confirmare, inchide peste intrarea 374.80
# bare 22-31 : urcare spre pivot/TP1 383.74 (bara 26) si apoi spre TP2 402.00
CLOSES = [
395.00, 391.80, 388.60, 385.20, 382.00, 378.90, 375.70, 372.80, # 0-7
369.90, 367.00, 364.20, 361.80, # 8-11
363.20, 361.00, 358.50, 356.20, 360.00, 365.00, 369.50, 373.00, # 12-19
373.30, 375.60, # 20-21
377.00, 378.50, 380.40, 382.00, 384.50, 386.50, 389.50, 393.00, # 22-29
397.50, 401.50, # 30-31
]
assert len(CLOSES) == 32, len(CLOSES)
WICKS = [(0.35, 0.40)] * 32
WICKS[0] = (0.40, 0.50) # prima bara, deschide dintr-un varf al downtrend-ului
WICKS[15] = (0.30, 0.72) # low exact 355.48 = suportul monthly
WICKS[16] = (0.40, 0.30) # rebound, nu sparge suportul
WICKS[20] = (0.80, 0.30) # doji: high 374.10, low 372.70
WICKS[31] = (0.55, 0.40) # high 402.05, atinge TP2 402.00
assert len(WICKS) == 32, len(WICKS)
# barele 25-31 in crestere; bara 20 (doji) sub media seriei (RVOL 0.61)
VOLS = [
42, 44, 41, 46, 43, 45, 40, 44, 47, 43, # 0-9
40, 42, 38, 41, 52, # 10-14
56, 50, 45, 42, 39, # 15-19
30, # 20 doji, sub medie
46, # 21 confirmare
48, 50, 52, 54, 58, 62, 66, 70, 76, 82, # 22-31
]
assert len(VOLS) == 32, len(VOLS)
media_vol = sum(VOLS) / len(VOLS)
assert VOLS[20] < media_vol, "bara 20 (doji) trebuie sub media seriei"
assert all(a < b for a, b in zip(VOLS[25:32], VOLS[26:32])), "barele 25-31 nu cresc"
# RVOL afisat (0.61) ~ raportul barei de doji fata de media seriei
assert abs(VOLS[20] / media_vol - 0.61) < 0.02, VOLS[20] / media_vol
cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=396.50)
# ---------------------------------------------------------------- assert-urile din spec
assert abs(cds[20].c - 373.30) < 1e-9, "doji-ul trebuie sa inchida la 373.30"
assert abs(max(cds[20].o, cds[20].c) - min(cds[20].o, cds[20].c)) <= 0.40, "bara 20 nu e doji"
assert abs(min(c.l for c in cds[12:21]) - 355.48) < 0.05, "suportul 355.48 nu e atins"
assert 352.00 < 355.48 < 373.30 < 374.80 < 383.74 < 402.00, "ordinea nivelurilor"
risc = 374.80 - 352.00 # 22.80
assert abs((383.74 - 374.80) / risc - 0.39) < 0.02, "TP1 nu e ~0.39R"
assert abs((402.00 - 374.80) / risc - 1.19) < 0.02, "TP2 nu e ~1.19R"
assert cds[-1].c >= 383.74, "TP1 nu e atins in serie"
assert min(c.l for c in cds) >= 351.5, "SL-ul de 352.00 nu poate fi sub minimul seriei"
for a, b in zip(cds, cds[1:]):
assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial intre bare"
# structura ceruta explicit: doji mic cu fitilele date, confirmare peste intrare
assert abs(cds[20].h - 374.10) < 0.05 and abs(cds[20].l - 372.70) < 0.05, (cds[20].h, cds[20].l)
assert cds[21].c > 374.80, "bara 21 nu confirma intrarea"
assert max(cds[i].h for i in range(22, 27)) >= 383.74, "TP1 nu e atins in jurul barei 26"
assert cds[31].h >= 402.00, "TP2 nu e atins spre final"
# ---------------------------------------------------------------- niveluri, etichete
levels = [
Level(352.00, "SL 352.00", RED, label_at="left"),
Level(355.48, "suport monthly 355.48", BLUE, label_at="left"),
Level(374.80, "intrare 374.80", GREEN),
Level(383.74, "pivot / TP1 383.74", BLUE),
Level(402.00, "TP2 402.00", BLUE),
]
marks = [
# „sub bara 0" ar insemna peste candelele care coboara (staircase); dy-ul
# o ridica deasupra varfului de start, unde textul se citeste curat
Mark(0, "downtrend weekly", GRAY, "below", dy=-62),
Mark(20, "buy doji close 373.30 (RVOL 0.61)", TEAL, "below"),
Mark(21, "intrare 374.80", GREEN, "above"),
Mark(26, "TP1 383.74 = pivot", BLUE, "above", dy=-8),
Mark(31, "TP2 402.00", BLUE, "above", dy=-22),
]
caption = ("Ale cursului: close 373.30, suport 355.48, pivot 383.74, RVOL 0.61. "
"Intrarea 374.80, SL 352.00 si TP2 402.00 sunt estimari din 04_MODUL_3/01; "
"structura barelor dintre niveluri e construita, la scara.")
ch = Chart(
candles=cds, levels=levels, marks=marks,
mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
scale=8.5, gap=15, body_w=9, left=44, width=640, vol_h=70,
vol_label="volum",
title="ULTA — daily (pivoți monthly)",
caption=caption,
)
svg = ch.render()
# ---------------------------------------------------------------- verificarea de scara
ys = {p: ch._y(p) for p in (352.00, 355.48, 374.80, 383.74, 402.00)}
px_risc = ys[352.00] - ys[374.80] # y creste in jos, deci SL-ul are y mai mare
assert abs((ys[374.80] - ys[383.74]) / px_risc - (383.74 - 374.80) / risc) < 1e-9, "TP1 la scara gresita"
assert abs((ys[374.80] - ys[402.00]) / px_risc - (402.00 - 374.80) / risc) < 1e-9, "TP2 la scara gresita"
# ---------------------------------------------------------------- finisaj text
# caption-ul pe un singur rand depaseste latimea panzei (se taie la viewBox):
# il rupem in linii si crestem inaltimea desenului.
def _wrap(text: str, max_chars: int) -> list[str]:
lines, cur = [], ""
for w in text.split():
if cur and len(cur) + 1 + len(w) > max_chars:
lines.append(cur)
cur = w
else:
cur = f"{cur} {w}".strip()
if cur:
lines.append(cur)
return lines
cap_lines = _wrap(caption, 92)
m = re.search(r'<text x="24" y="(\d+)" fill="#555">' + re.escape(caption) + r"</text>\s*</svg>", svg)
assert m, "caption-ul nu a fost gasit in SVG"
y0 = int(m.group(1))
wrapped = "\n ".join(f'<text x="24" y="{y0 + 17 * i}" fill="#555">{ln}</text>'
for i, ln in enumerate(cap_lines))
svg = svg[:m.start()] + wrapped + "\n</svg>"
vm = re.search(r'viewBox="0 0 (\d+) (\d+)"', svg)
assert vm
svg = svg.replace(vm.group(0),
f'viewBox="0 0 {vm.group(1)} {int(vm.group(2)) + 17 * (len(cap_lines) - 1)}"', 1)
# „SMA-20" (x=515, y=269) sta la 12.6px de eticheta „intrare 374.80" (y=281.6),
# adica lipite: o ridicam 12px pe linia libera de deasupra.
svg, n = re.subn(r'(<text x="\d+" y=")(\d+)(" fill="#b06000">SMA-20</text>)',
lambda mm: f'{mm.group(1)}{int(mm.group(2)) - 12}{mm.group(3)}', svg)
assert n == 1, f"eticheta SMA-20: {n} potriviri"
assert "<svg" in svg and "\n\n" not in svg, "linie goala in SVG"
OUT.write_text(svg)
print(f"scris {OUT.name}: {len(cds)} candele, doji {cds[20].c:.2f} (RVOL {VOLS[20]/media_vol:.2f}), "
f"suport {min(c.l for c in cds[12:21]):.2f}, intrare 374.80, SL 352.00, "
f"TP1 383.74 = 0.39R, TP2 402.00 = 1.19R, ultimul close {cds[-1].c:.2f}")