strategii: primele 30 de diagrame, cu generatoarele lor

Cate un agent grafician per diagrama, fiecare cu generatorul si SVG-ul lui.
Graficele de pret trec prin svg_candles, cu assert-urile din specificatie
scrise in generator — verifica structura, nu doar ca ruleaza: ca al doilea
picior e mai jos, ca intrarea e close-ul barei de confirmare, ca 1:1 e exact.

Verificare prin sondaj pe buy_doji_ansamblu, cu screenshot citit: serie
continua de 32 de candele, panou de volum, MA(20), doji-ul si ieșirea pe
trail etichetate, cifrele identice cu specificatia.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
This commit is contained in:
Claude Agent
2026-09-18 19:22:13 +00:00
parent af448dda69
commit 01599d2e9d
60 changed files with 9035 additions and 0 deletions

View File

@@ -0,0 +1,222 @@
"""Diagrama din `gen/strategii/mdsrt_exemplu.spec.txt` (TIP: PRET).
Exemplul real din 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.
Reale din curs: nivelurile 622.40 / 622.00 / 621.40 / 622.60 și cele 8 bare
dintre picioare. Construcția mea: entry 622.60, SL 621.30, TP1 623.90 (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 reale din curs + planul construit (marcat) ──────────────────
PIVOT = 622.00 # pivot daily, suportul
LEG1 = 622.40 # swing low picior 1
LEG2 = 621.40 # picior 2 (mai jos, rupe pivotul)
ENTRY = 622.60 # close-ul Buy Hammer (construit)
SL = 621.30 # 0.10 sub low-ul piciorului 2 (construit)
TP1 = 623.90 # 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. entry = close-ul trigger-ului (construit), SL 0.10 sub low-ul piciorului 2
assert cds[18].c == ENTRY
assert abs(cds[18].l - SL - 0.10) < 1e-9, (cds[18].l, SL)
risk = ENTRY - SL
assert abs(risk - 1.30) < 1e-9, risk
# 7. TP1 simetric 1:1
assert abs((ENTRY - SL) - (TP1 - ENTRY)) < 1e-9
# 8. după trigger, primul close care atinge 623.90 e bara 21 (nu mai devreme).
# Seria pornește la 627.00, deci „atinge" se citește de jos în sus, din zona
# picioarelor: barele 10-20 rămân sub TP1, bara 21 îl atinge.
assert all(c.c < TP1 for c in cds[10:21]), [c.c for c in cds[10:21]]
assert max(c.h for c in cds[19:21]) < TP1, [c.h for c in cds[19:21]]
assert cds[21].c == TP1, cds[21].c
# 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 622.60 (construit)", GREEN, label_at="left"),
Level(TP1, "TP1 623.90 = 1:1 (construit)", GREEN, label_at="left"),
Level(LEG1, "picior 1 = 622.40", BLUE, label_at="right"),
]
# 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=-20),
]
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 real, ce e construit ───────────
legend = [
"Legendă: nivelurile 622.40 / 622.00 / 621.40 / 622.60 și cele 8 candele dintre",
"picioare sunt reale, din curs. Intrarea 622.60, SL-ul 621.30 și TP-ul 623.90",
"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 "sunt reale, din curs" in svg and "construcția mea" in svg, \
"legenda nu spune explicit ce e real și ce e construit"
# 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",
"intrare 622.60 (construit)", "SL 621.30 (construit)",
"TP1 623.90 = 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 21 | "
f"vol[18]={VOLS[18]} > vol[10]={VOLS[10]}")