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
223 lines
9.8 KiB
Python
223 lines
9.8 KiB
Python
"""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&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]}")
|