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
191 lines
8.9 KiB
Python
191 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
||
"""LABU — Buy Doji la pivotul monthly (Nivel 2.5).
|
||
|
||
Citește `gen/strategii/buy_doji_labu.spec.txt` și scrie
|
||
`gen/strategii/buy_doji_labu.svg`: 49 de candele daily, downtrend → triple
|
||
bottom → doji la pivotul monthly 86.00 → confirmare (close 89.50 peste
|
||
high-ul dojiului) → bull reversal → higher high, cu SL 81.45 (−9 %) și
|
||
TP 116.35 (+30 %).
|
||
|
||
Toate cifrele din desen vin din specificație: pivot 86.00, intrare 89.50,
|
||
SL 81.45, TP 116.35, high/low-ul dojiului (86.30 / 84.20) și low-urile
|
||
triple bottom-ului (82.40 / 81.45 / 81.45). Procentele 9 % și 30 % sunt ale
|
||
cursului (reale); nivelurile de preț sunt construite de sumarizator —
|
||
legenda o spune explicit.
|
||
|
||
Rulare: python3 gen/strategii/buy_doji_labu.py
|
||
"""
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, "/workspace/atm-curs-dl")
|
||
from svg_candles import Chart, Level, MA, Mark, from_closes # noqa: E402
|
||
|
||
OUT = Path(__file__).with_suffix(".svg")
|
||
|
||
# --- nivelurile din specificație -------------------------------------------
|
||
PIVOT = 86.00 # pivot monthly (construit)
|
||
ENTRY = 89.50 # intrare = close-ul confirmării (construit)
|
||
SL = 81.45 # SL = −9 % față de intrare (construit; procentul e real)
|
||
TP = 116.35 # TP = +30 % față de intrare (construit; procentul e real)
|
||
DOJI_H, DOJI_L = 86.30, 84.20
|
||
|
||
# --- seria de close-uri din specificație -----------------------------------
|
||
closes = [
|
||
# downtrend 0-14
|
||
108.0, 106.5, 105.0, 103.0, 101.5, 100.0, 98.0, 96.5, 95.0, 93.5,
|
||
92.5, 91.0, 89.5, 87.5, 85.5,
|
||
# triple bottom 15 / 23 / 31
|
||
82.80, 84.5, 86.5, 88.0, 88.50,
|
||
87.0, 85.0, 83.0, 81.50,
|
||
83.5, 85.5, 87.0, 87.80, 87.0, 85.0,
|
||
83.0, 81.60, 85.00,
|
||
84.90, # 33 = doji (o 85.00, c 84.90, h 86.30, l 84.20)
|
||
89.50, # 34 = confirmare, close > 86.30 => intrare
|
||
# bull reversal / trend up 35-48
|
||
91.0, 93.0, 92.0, 95.0, 97.0, 96.0, 99.0, 102.0, 101.0, 105.0, 108.0,
|
||
112.0, 116.35, 118.0,
|
||
]
|
||
assert len(closes) == 49, f"seria are {len(closes)} bare, spec cere 49"
|
||
|
||
# wick-uri: (up, down). Restul (0.4, 0.5); ajustate ca low-urile/high-urile
|
||
# cerute de asserts să iasă exact (bar 15/23/31 = triple bottom; bar 16/24
|
||
# nu au voie să coboare sub piciorul din stânga; bar 33 = doji).
|
||
wicks = [(0.4, 0.5)] * len(closes)
|
||
wicks[15] = (0.4, 0.40) # low 82.40
|
||
wicks[16] = (0.4, 0.20) # low 82.60 > 82.40 (piciorul 1 rămâne minim local)
|
||
wicks[23] = (0.4, 0.05) # low 81.45
|
||
wicks[24] = (0.4, 0.00) # low 81.50 > 81.45 (piciorul 2 rămâne minim local)
|
||
wicks[31] = (0.4, 0.15) # low 81.45
|
||
wicks[32] = (0.4, 0.10) # low 81.50 (nu sub 81.45)
|
||
wicks[33] = (1.30, 0.70) # doji: high 86.30, low 84.20
|
||
|
||
# volume construite: vânzare peste medie la triple bottom și la confirmare.
|
||
# Nu sunt numere din curs (nu sunt etichetate), doar panoul de volum cerut.
|
||
vols = [
|
||
55, 52, 48, 50, 45, 47, 44, 46, 42, 44,
|
||
40, 42, 38, 41, 58,
|
||
62, 50, 46, 44, 43,
|
||
45, 44, 47, 66, 55,
|
||
50, 47, 45, 44, 42,
|
||
43, 45, 60, 72, 84,
|
||
78, 70, 64, 60, 58,
|
||
54, 52, 56, 60, 66,
|
||
74, 82, 90, 98,
|
||
]
|
||
assert len(vols) == len(closes), f"volume {len(vols)} != bare {len(closes)}"
|
||
|
||
cds = from_closes(closes, wicks=wicks, vols=vols, start=110.0)
|
||
assert abs(cds[0].o - 110.0) < 1e-9 and cds[0].c < cds[0].o, "prima bară nu e roșie"
|
||
|
||
# nota explicativă a dojiului (bloc stânga-jos, nu se suprapune cu candelele)
|
||
cds[33].note = ("bara 33 = doji: o 85.00, c 84.90, h 86.30, l 84.20 — "
|
||
"corp mic exact pe pivotul monthly 86.00 (indecizie la suport)")
|
||
|
||
# --- assert-urile din specificație (toate, ca assert reale) ----------------
|
||
# 1. pivot 86.00 e straddled de doji
|
||
assert cds[33].l <= PIVOT <= cds[33].h, "doji-ul nu straddlează 86.00"
|
||
|
||
# 2. confirmarea: close[34] == 89.50 == intrare și > high[33]
|
||
assert abs(cds[34].c - ENTRY) < 1e-9, f"close[34]={cds[34].c} != intrare 89.50"
|
||
assert cds[34].c > cds[33].h, "confirmarea nu închide peste high-ul dojiului"
|
||
|
||
# 3. SL = min(low[30:35]) (regula: extrema ultimelor 4 candele de la confirmare)
|
||
min_30_35 = min(c.l for c in cds[30:35])
|
||
assert abs(min_30_35 - SL) < 1e-9, f"min low[30:35]={min_30_35} != SL {SL}"
|
||
|
||
# 4./5. procentele REALE ale cursului
|
||
assert round((ENTRY - SL) / ENTRY * 100, 1) == 9.0, "stopul nu iese −9 %"
|
||
assert round((TP - ENTRY) / ENTRY * 100, 1) == 30.0, "TP nu iese +30 %"
|
||
|
||
# 6. triple bottom: exact trei minime locale sub 86.00, în zona 81-83,
|
||
# al doilea și al treilea fără low nou semnificativ față de primul
|
||
lows = [c.l for c in cds]
|
||
bottom_idx = [i for i in range(1, len(cds) - 1)
|
||
if lows[i] < lows[i - 1] and lows[i] < lows[i + 1]
|
||
and lows[i] < PIVOT and 81.0 <= lows[i] <= 83.0]
|
||
assert bottom_idx == [15, 23, 31], f"triple bottom la {bottom_idx}, nu [15, 23, 31]"
|
||
b1, b2, b3 = lows[15], lows[23], lows[31]
|
||
tol = 0.02 * b1 # „semnificativ" = > 2 % sub primul low
|
||
assert b2 >= b1 - tol and b3 >= b1 - tol, \
|
||
f"al doilea/treilea picior coboară semnificativ sub primul: {b1}/{b2}/{b3}"
|
||
assert abs(b1 - 82.40) < 1e-9 and abs(b2 - SL) < 1e-9 and abs(b3 - SL) < 1e-9, \
|
||
f"low-urile triple bottom: {b1}/{b2}/{b3}"
|
||
|
||
# 7. TP atins: există i > 34 cu high[i] >= 116.35
|
||
assert any(c.h >= TP for c in cds[35:]), "TP 116.35 nu e atins după intrare"
|
||
|
||
# 8. doji: corp <= 10 % din amplitudine
|
||
assert abs(cds[33].o - cds[33].c) <= 0.10 * (cds[33].h - cds[33].l), "bara 33 nu e doji"
|
||
assert abs(cds[33].h - DOJI_H) < 1e-9 and abs(cds[33].l - DOJI_L) < 1e-9, \
|
||
"doji h/l nu ies 86.30/84.20"
|
||
|
||
# în plus: confirmarea vine cu volum peste medie (cerut de spec)
|
||
mean_vol = sum(vols) / len(vols)
|
||
assert vols[34] > mean_vol, f"volumul confirmării {vols[34]} <= media {mean_vol:.0f}"
|
||
|
||
entry = cds[34].c
|
||
w = 820 # lățime mărită: 49 bare × gap 15 + etichetele din dreapta
|
||
# (spec sugera 760, dar ultima candelă ajunge la x=764)
|
||
ch = Chart(
|
||
candles=cds,
|
||
levels=[Level(PIVOT, "pivot monthly 86.00 (construit)", "#777",
|
||
dash="4 3", label_at="left"),
|
||
Level(SL, "SL 81.45 (construit) = −9 % (procent real)", "#c00",
|
||
label_at="left"),
|
||
Level(ENTRY, "intrare 89.50 (construit)", "#188a3e"),
|
||
Level(TP, "TP 116.35 (construit) = +30 % (procent real)", "#1a5fb4")],
|
||
marks=[Mark(15, f"bottom 1 {lows[15]:.2f}", "#c00", "below"),
|
||
Mark(23, f"bottom 2 {lows[23]:.2f}", "#c00", "below"),
|
||
Mark(31, f"bottom 3 {lows[31]:.2f}", "#c00", "below"),
|
||
Mark(33, "doji", "#b06000", "above"),
|
||
Mark(34, "confirmare (intrare)", "#188a3e", "above", dy=-14),
|
||
Mark(48, "higher high — TP atins", "#188a3e", "above")],
|
||
mas=[MA(20, "#b06000", dash="4 3", text="SMA-20")],
|
||
scale=8.0, gap=15, body_w=9, left=44, width=w, vol_h=70,
|
||
vol_label="volum", title="LABU — daily",
|
||
caption=("Procentele (stop 9 %, +30 %) sunt ale cursului; nivelurile de "
|
||
"preț sunt construite de sumarizator (18_2025-09-23.md:713)."),
|
||
)
|
||
svg = ch.render()
|
||
assert "\n\n" not in svg and "<svg" in svg, "SVG invalid (linie goală?)"
|
||
|
||
# --- citirea SVG-ului: etichete în panză, fără suprapuneri -----------------
|
||
TEXT = re.compile(r'<text x="([-\d.]+)" y="([-\d.]+)"([^>]*)>(.*?)</text>', re.S)
|
||
H = float(re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', svg).group(2))
|
||
labels = []
|
||
for m in TEXT.finditer(svg):
|
||
x, y, attrs, s = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
|
||
anchor = "end" if 'text-anchor="end"' in attrs else \
|
||
("middle" if 'text-anchor="middle"' in attrs else "start")
|
||
bold = "bold" in attrs
|
||
wpx = len(s) * 12 * (0.62 if bold else 0.52)
|
||
left = x - wpx if anchor == "end" else (x - wpx / 2 if anchor == "middle" else x)
|
||
labels.append((left, left + wpx, y, s))
|
||
|
||
for left, right, y, s in labels:
|
||
assert left >= -2 and right <= w + 2, f"text iese din panză: {s!r} [{left:.0f},{right:.0f}]"
|
||
assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y:.0f})"
|
||
|
||
for i in range(len(labels)):
|
||
li, ri, yi, si = labels[i]
|
||
for j in range(i + 1, len(labels)):
|
||
lj, rj, yj, sj = labels[j]
|
||
if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
|
||
raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}\n {labels[i]}\n {labels[j]}")
|
||
|
||
# candelele sunt o serie continuă, lipită
|
||
xs = [ch._cx(i) for i in range(len(cds))]
|
||
assert xs[-1] + ch.body_w / 2 < w, f"ultima candelă iese din panză (x={xs[-1]})"
|
||
assert all(xs[i + 1] - xs[i] == ch.gap for i in range(len(xs) - 1)), "serie cu goluri"
|
||
for a, b in zip(cds, cds[1:]):
|
||
assert abs(a.c - b.o) < 1e-9, "open != close-ul barei anterioare"
|
||
|
||
print(f"LABU: {len(cds)} bare, doji {cds[33].l:.2f}-{cds[33].h:.2f} peste pivot "
|
||
f"{PIVOT:.2f}, intrare {entry:.2f}, SL {SL:.2f} (−9 %), TP {TP:.2f} (+30 %)")
|
||
print(f"triple bottom: {b1:.2f} / {b2:.2f} / {b3:.2f} "
|
||
f"volum confirmare {vols[34]} > medie {mean_vol:.0f}")
|
||
print(f"scris {OUT} ({w}x{H:.0f}); {len(labels)} etichete verificate, fără suprapuneri")
|
||
OUT.write_text(svg, encoding="utf-8")
|