Noi: pivoti (PPQ/SQ calculati din trimestrul precedent, cu formula pe desen), perechi_tf (weekly -> daily ca descompunere reala, cu caseta, ghidaje si PPQ pe ambele panouri), trigger (BIM apoi verde, fata de verde inainte de BIM — cutitul in cadere), risc (ce inseamna "fara stop": 24 de bare sub apa, -20 % pe pozitie), reinvestire (ciclul banilor pe trei cicluri). aplus devine corectie: diagrama arata un prag de corectie, nu un setup A+, si numele vechi trimitea la altceva. Trei bug-uri reale: exemplul era etichetat weekly cu 20 de bare, dar textul spunea "un an mai tarziu" — trecut pe monthly cu assert pe numarul de luni; ansamblul avea o trimitere fisier:linie desenata in SVG; si benzile PPQ/SQ erau inversate ca sens, cu SQ fix pe intrare. PPQ 95.80 si SQ 90.80 sunt completare proprie — cursul spune explicit ca nu da valorile; derivate cu formula standard, cu assert. Textul capitolului e neatins — agentul a atins pragul de context. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
226 lines
10 KiB
Python
226 lines
10 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)
|
|
HAMMER_C = 622.60 # close-ul Buy Hammer — cifră reală, din sesiunea Q&A
|
|
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 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 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", 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&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 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 "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 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]}")
|