Files
atm-curs-dl/gen/strategii/mdsrt_exemplu.py
Claude Agent 76011566ee diagrame: ultimele patru etichete ramase in urma textului corectat
selectia_rvol: 'sub jumatate fata de normal' pentru 0,61 -> '61 % dintr-o zi
normala'. mdsrt_exemplu: legenda spunea ca nivelurile SPY sunt 'reale, din
curs'; sunt reconstruite dupa ilustratia unui sumar Q&A. mbt_iesire: 2.04R ->
2,0R, cifra sursei, prin rotunjire la afisare, fara sa se atinga nicio serie;
assert-ul verifica acum textul din SVG-ul randat, nu doar valoarea bruta.
atmi_exemplu: '13 luni mai tarziu' era citit ca fapt al materialului - acum
spune ca 13 e cifra desenului, materialul da 'aproximativ un an'; si cele
patru marcaje de completare din desenele atmi sunt uniformizate.

Plus cei trei agenti opencode scrisi pentru rundele astea: verif_strat,
corector_strat, reparator_grafic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-22 20:13:12 +00:00

238 lines
11 KiB
Python

"""Diagrama din `gen/strategii/mdsrt_exemplu.spec.txt` (TIP: PRET).
Sesiunea de Q&A 15.07.2025 (`09_QA/24_2025-07-15.md`): SPY pe 5 min, double
bottom cu al doilea picior mai jos (MDSRT pe 5 min, suportul = pivotul daily).
Downtrend → picior 1 la 622.40 → revenire → picior 2 la 621.40 (rupe pivotul
622.00) → Buy Hammer care închide la 622.60, înapoi peste pivot. După trigger,
un plan construit de mine (marcat) duce trade-ul până la TP1.
Nivelurile 622.40 / 622.00 / 621.40 / 622.60 și cele 8 bare dintre picioare
sunt reconstruite după ilustrația sumarului Q&A — NU sunt rostite în
transcriere: sumarul însuși le dă ca „ale mele", construite
(`09_QA/24_2025-07-15.md:601-602`). Construcția mea: entry 623.00, SL 621.30,
TP1 624.70 (1:1) — materialul nu le dă (transcrierea planului se întrerupe).
Volumele sunt relative, alese de generator.
Rulează:
python3 gen/strategii/mdsrt_exemplu.py
Scrie lângă el `mdsrt_exemplu.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_exemplu.svg"
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
# ── nivelurile reconstruite din ilustrația sumarului + planul construit ────
PIVOT = 622.00 # pivot daily, suportul (reconstruit după ilustrație)
LEG1 = 622.40 # swing low picior 1 (reconstruit)
LEG2 = 621.40 # picior 2 (mai jos, rupe pivotul; reconstruit)
HAMMER_C = 622.60 # close-ul Buy Hammer — reconstruit după ilustrația sumarului
ENTRY = 623.00 # close-ul candelei care sparge high-ul hammer-ului (construit)
SL = 621.30 # 0.10 sub low-ul piciorului 2 (construit)
TP1 = 624.70 # entry + risc = 1:1 (construit)
# ── seria: 30 de close-uri, exact ca în specificație ───────────────────────
CLOSES = [
627.00, 626.40, 626.70, 626.00, 625.40, 624.80, 624.40, 623.80, # 0-7
623.20, 622.80, # 8-9
622.40, # 10 = picior 1 (swing low 622.40)
622.70, 623.10, 623.50, 623.20, 622.90, 622.60, 622.30, # 11-17
622.60, # 18 = picior 2: low 621.40, Buy Hammer care închide 622.60
623.00, 623.40, 623.90, # 19-21, 21 = TP1 1:1 (construit)
624.20, 624.00, 624.40, 624.80, 625.20, 625.60, # 22-27
625.30, 625.70, # 28-29
]
assert len(CLOSES) == 30, len(CLOSES)
# wick-uri (sus, jos): alternanță naturală; excepțiile sunt piciorul 1 și
# hammer-ul de la piciorul 2 (cerute exact de specificație).
WICKS = [
(0.10, 0.10), (0.12, 0.14), (0.15, 0.12), (0.18, 0.15), (0.20, 0.16),
(0.18, 0.14), (0.16, 0.13), (0.15, 0.12), (0.14, 0.12), (0.12, 0.10),
(0.15, 0.05), # 10 = picior 1: low 622.40 - 0.05 = 622.35
(0.12, 0.12), (0.15, 0.14), (0.18, 0.15), (0.16, 0.13), (0.14, 0.12),
(0.12, 0.10), (0.10, 0.12),
(0.10, 0.90), # 18 = hammer: high 622.70, low 621.40
(0.12, 0.12), (0.14, 0.12), (0.15, 0.13),
(0.16, 0.15), (0.14, 0.12), (0.15, 0.13), (0.16, 0.14), (0.17, 0.14),
(0.18, 0.15), (0.14, 0.12), (0.15, 0.13),
]
assert len(WICKS) == 30, len(WICKS)
# volum relativ (ales de generator; materialul nu dă volumul pentru exemplu):
# scade pe downtrend, spike de absorbție pe hammer, higher low vs piciorul 1.
VOLS = [
58, 56, 54, 52, 50, 49, 48, 47, 46, 45, # 0-9
44, # 10 = picior 1
43, 42, 41, 40, 39, 38, 39, # 11-17
100, # 18 = hammer, maximul seriei
62, 58, 54, # 19-21
52, 50, 49, 48, 47, 46, 45, 44, # 22-29
]
assert len(VOLS) == 30, len(VOLS)
cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=627.00)
# ── assert-urile din specificație ──────────────────────────────────────────
# 1. picior 1: low 622.35, close 622.40
assert abs(cds[10].l - 622.35) < 1e-9, cds[10].l
assert cds[10].c == 622.40, cds[10].c
# 2. picior 2 mai jos (621.40 < 622.35) și rupe pivotul daily 622.00
assert abs(cds[18].l - LEG2) < 1e-9, cds[18].l
assert cds[18].l < cds[10].l, (cds[18].l, cds[10].l)
assert cds[18].l < PIVOT, cds[18].l
# 3. trigger-ul închide înapoi peste pivot
assert cds[18].c == 622.60, cds[18].c
assert cds[18].c > PIVOT, cds[18].c
# 4. 8 candele între picioare (peste minimul de 5)
assert 18 - 10 == 8
# 5. hammer: coadă lungă în jos (0.90) > corp (0.30)
corp = abs(cds[18].o - cds[18].c)
coada = min(cds[18].o, cds[18].c) - cds[18].l
assert abs(corp - 0.30) < 1e-9, corp
assert abs(coada - 0.90) < 1e-9, coada
assert coada > corp, (coada, corp)
# 6. regula de intrare a capitolului: close-ul candelei care sparge high-ul
# candelei-trigger (aici bara 19), nu close-ul hammer-ului însuși
assert cds[18].c == HAMMER_C
assert cds[19].c == ENTRY and cds[19].c > cds[18].h, (cds[19].c, cds[18].h)
assert abs(cds[18].l - SL - 0.10) < 1e-9, (cds[18].l, SL)
risk = round(ENTRY - SL, 2)
assert risk == 1.70, risk
# 7. TP1 simetric 1:1
assert abs((ENTRY - SL) - (TP1 - ENTRY)) < 1e-9
# 8. după trigger, primul high care atinge TP1 624.70 e bara 25 (nu mai devreme).
# Seria pornește la 627.00, deci „atinge" se citește de jos în sus, din zona
# picioarelor: barele 19-24 rămân sub TP1, bara 25 îl atinge.
assert max(c.h for c in cds[19:25]) < TP1, [c.h for c in cds[19:25]]
assert cds[25].h >= TP1 > cds[25].o, (cds[25].h, cds[25].o)
TP_BAR = 25
# 9. divergență: 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])
assert VOLS[18] == max(VOLS)
# 10. fiecare open == close-ul barei anterioare
for a, b in zip(cds, cds[1:]):
assert a.c == b.o, (a.c, b.o)
# ── geometria, ca să așez marks-urile fără suprapunere de text ─────────────
SCALE, GAP, LEFT, WIDTH, BODY_W, VOL_H = 62, 15, 44, 640, 9, 70
def cx(i: int) -> float:
return LEFT + i * GAP
levels = [
Level(PIVOT, "pivot daily 622.00", ORANGE, label_at="left"),
Level(SL, "SL 621.30 (construit)", RED, label_at="left"),
Level(ENTRY, "intrare 623.00 (construit)", GREEN, label_at="right"),
Level(TP1, "TP1 624.70 = 1:1 (construit)", GREEN, label_at="left"),
]
# dy-uri calculate manual ca etichetele „below" să nu calce linia pivotului
# (bara 10) și ca „hammer" să nu calce eticheta intrării (bara 18).
marks = [
Mark(10, "picior 1 = 622.40", BLUE, "below", dy=15),
Mark(18, "picior 2 = 621.40 (mai jos) · Buy Hammer", RED, "below"),
Mark(18, "hammer închide 622.60, înapoi peste pivot", GREEN, "above", dy=-42),
Mark(19, "intri aici: close peste high-ul hammer-ului (622.70)", GREEN, "above", dy=-62),
]
ch = Chart(
candles=cds, levels=levels, marks=marks,
mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
scale=SCALE, gap=GAP, body_w=BODY_W, left=LEFT, width=WIDTH, vol_h=VOL_H,
vol_label="volum",
title="SPY — 5 min (Q&amp;A 15.07.2025; intrare/SL/TP construite)",
caption="",
)
svg = ch.render()
# ── legenda de onestitate (assert 11): ce e reconstruit, ce e construit ────
legend = [
"Legendă: nivelurile 622.40 / 622.00 / 621.40 / 622.60 și cele 8 candele dintre",
"picioare sunt reconstruite după ilustrația sumarului Q&amp;A, nu valori rostite în",
"transcriere. Intrarea 623.00, SL-ul 621.30 și TP-ul 624.70 sunt construcția mea —",
"materialul nu le dă.",
]
m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg)
W, H0 = int(m.group(1)), float(m.group(2))
legend_svg = [f'<text x="24" y="{H0 + 16 + 17 * i:.1f}" fill="#555">{t}</text>'
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</svg>", "\n " + "\n ".join(legend_svg) + "\n</svg>")
assert svg.count("\n\n") == 0, "linie goală în SVG rupe blocul din markdown"
assert "reconstruite după ilustrația sumarului Q&amp;A" in svg, \
"legenda trebuie să spună că nivelurile sunt reconstruite după sumarul Q&amp;A"
assert "nu valori rostite în" in svg, \
"legenda trebuie să nege că nivelurile ar fi rostite în transcriere"
assert "construcția mea" in svg, \
"legenda nu spune explicit ce e construit"
# defectul vechi: legenda pretindea că nivelurile ar fi „reale, din curs",
# deși vin din ilustrația sumarului, nu din transcriere.
assert "reale, din curs" not in svg, \
"legenda veche pretindea că nivelurile sunt reale, din curs"
# toate etichetele cerute de specificație sunt prezente
for eticheta in ("pivot daily 622.00", "picior 1 = 622.40",
"picior 2 = 621.40 (mai jos) · Buy Hammer",
"hammer închide 622.60, înapoi peste pivot",
"intri aici: close peste high-ul hammer-ului (622.70)",
"intrare 623.00 (construit)", "SL 621.30 (construit)",
"TP1 624.70 = 1:1 (construit)"):
assert eticheta in svg, f"lipsește eticheta: {eticheta!r}"
assert "SPY — 5 min" in svg, "lipsește titlul"
# 1:1 desenat la 1:1 (STYLE.md: raportul din preț = raportul din pixeli)
ys = {p: ch._y(p) for p in (SL, ENTRY, TP1)}
assert abs((ys[SL] - ys[ENTRY]) - (ys[ENTRY] - ys[TP1])) < 1e-6, ys
# ── verificare: fiecare etichetă e în pânză și nu se suprapune alta ────────
m = re.search(r'viewBox="0 0 (\d+) ([0-9.]+)"', svg)
W, H = int(m.group(1)), float(m.group(2))
texte = re.findall(r'<text x="([\d.]+)" y="([\d.]+)"([^>]*)>([^<]*)</text>', svg)
cutii = []
for xs, ys, attrs, continut in texte:
x, yv = float(xs), float(ys)
w = len(continut) * (7.44 if "bold" in attrs else 6.72)
if 'text-anchor="end"' in attrs:
x0 = x - w
elif 'text-anchor="middle"' in attrs:
x0 = x - w / 2
else:
x0 = x
assert -2 <= x0 and x0 + w <= W + 2, f"etichetă în afara pânzei: {continut!r}"
assert 0 <= yv <= H, f"etichetă în afara înălțimii: {continut!r}"
cutii.append((x0, x0 + w, yv, continut))
for i in range(len(cutii)):
for j in range(i + 1, len(cutii)):
a, b = cutii[i], cutii[j]
if abs(a[2] - b[2]) < 13 and a[0] < b[1] - 2 and b[0] < a[1] - 2:
raise AssertionError(f"texte suprapuse: {a[3]!r} / {b[3]!r}")
OUT.write_text(svg, encoding="utf-8")
print(f"scris {OUT.name}: {len(cds)} candele | picior 1 {cds[10].l:.2f} (close "
f"{cds[10].c:.2f}) | picior 2 {cds[18].l:.2f} (mai jos) | hammer închide "
f"{cds[18].c:.2f} | intrare {ENTRY:.2f} | SL {SL:.2f} (risc {risk:.2f}) | "
f"TP1 {TP1:.2f} = {(TP1 - ENTRY) / risk:.2f}:1 la bara {TP_BAR} | "
f"vol[18]={VOLS[18]} > vol[10]={VOLS[10]}")