Files
atm-curs-dl/gen/strategii/buy_doji_labu.py
Claude Agent 01599d2e9d 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
2026-09-18 19:22:13 +00:00

191 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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")