Files
atm-curs-dl/gen/strategii/buy_doji_labu.py
Claude Agent a92a12a65c buy_doji: reparatiile de diagrame din verificare (11 puncte)
doji_forma nu mai foloseste 49.20 ca low al dojiului — era fix pretul
stopului, in prima diagrama pe care o vede un incepator. Bara e pe
50.10/49.25, cu procentele recalculate.

LABU: stopul e etichetat "pe cel mai jos dintre cele trei minime", nu
"sub minime"; zonele arata doua teste ale pivotului, cu "revenire peste
pivot 88.90" intre ele; numarul barei a iesit din desen, nota spune
"a 6-a bara dupa intrare", derivat din serie.

perechi_tf nu mai pune pretul TF-ului mare pe panoul TF-ului mic. aplus
nu mai numeste un nivel care nu e desenat. directie nu mai zice
"saptamanile" pe un panou daily. sell acopera toata cresterea, 14 bare.

transe: cele trei sageti nu mai converg intr-un ghem — fiecare are linia
ei cu pretul la capat; linia de intrare e desenata, ca parantezele de
risc sa plece de undeva. locatie arata 49.25, cifra din care iese 0,51 %.
stop si iesire: linii si sageti care taiau texte, mutate.

Jumatate din probleme erau aceeasi greseala: un pret scris de mana intr-o
eticheta, ramas in urma cand se schimba seria. Sunt acum aserttiuni care
o fac imposibila; PROGRESS.md spune care merita generalizata.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-19 21:33:15 +00:00

295 lines
16 KiB
Python
Raw Permalink 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`: 57 de candele daily, downtrend → triple
bottom (trei minime aproape egale, dar pivotul lunar atins doar de două ori)
→ 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. Capitolul spune și „triple bottom", și „doji la al
doilea test al pivotului": desenul le satisface pe amândouă — trei minime în
zona 81-83, dintre care doar primele două vin după o urcare peste 86.00.
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 Arrow, Chart, Level, MA, Mark, Zone, 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.80, 84.90
I_DOJI, I_CONF = 33, 34
B1, B2, B3 = 16, 25, 31 # cele trei picioare ale triplului fund
# (B1, B2 = testele pivotului; B3 rămâne sub el)
I_RETEST = 37 # revenirea care testează intrarea și ține
FLAG = (42, 45) # consolidarea scurtă (bull flag)
I_TP = 55 # bara pe care prețul atinge 116.35
# --- seria de close-uri -----------------------------------------------------
# Textul capitolului e sursa: „triple bottom" (trei minime aproape egale) și
# „doji la al doilea test al pivotului". Nu se bat cap în cap: sunt trei
# minime de preț, dar doar două dintre ele vin după o urcare peste 86.00 —
# al treilea e o alunecare care nu mai atinge pivotul de sus în jos.
# Restul: „+30 % într-o lună" (21 de bare daily de la confirmare) și etapele
# „retest → bull flag → breakout". Prețurile sunt construite în jurul
# nivelurilor deja folosite în capitol (86.00 / 89.50 / 81.45 / 97.55 / 116.35).
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,
# 15-16: primul test al pivotului (bottom 1, low 82.40)
83.5, 82.80,
# 17-20: revenire peste pivot (88.90 high) — de aici vine al doilea test
84.5, 86.5, 88.0, 88.5,
# 21-24: coboară înapoi prin pivot: al doilea test al lui 86.00
87.0, 85.5, 84.0, 82.8,
# 25: bottom 2 (low 82.00), la capătul celui de-al doilea test
82.30,
# 26-29: un pull-up scurt care NU mai ajunge la pivot (high max 85.80)
83.5, 84.8, 85.4, 84.2,
# 30: alunecă iar, fără să fi atins pivotul între timp
83.0,
# 31: bottom 3 (low 81.45 = stopul) — al treilea minim, nu un test nou
81.90,
# 32: bara de întoarcere, închide peste pivot
86.10,
# 33 = doji: o 86.10, c 86.00, h 86.80, l 84.90 — corpul chiar pe pivot
86.00,
# 34 = confirmare: close 89.50 peste high-ul dojiului => intrare
89.50,
# 35-36: pornirea
92.0, 94.0,
# 37: retest — cade la 89.60, pe intrare, și ține
91.0,
# 38-41: reia urcarea și trece de ținta 1:1 (97.55)
93.5, 95.5, 98.5, 100.0,
# 42-45: bull flag — patru candele care se odihnesc
99.2, 99.8, 98.9, 99.5,
# 46-54: breakout și continuare
102.5, 104.0, 103.0, 106.0, 108.5, 107.5, 110.5, 112.5, 114.0,
# 55: +30 % față de intrare; 56: încă un higher high
116.35, 117.5,
]
assert len(closes) == 57, f"seria are {len(closes)} bare, aștept 57"
# wick-uri: (sus, jos) față de corp. Restul (0.4, 0.5); ajustate ca low-urile
# cerute de assert-uri să iasă exact.
wicks = [(0.4, 0.5)] * len(closes)
wicks[B1] = (0.4, 0.40) # bottom 1: low 82.40
wicks[17] = (0.4, 0.00) # low 82.80 > 82.40 (piciorul 1 rămâne minim local)
wicks[B2] = (0.4, 0.30) # bottom 2: low 82.00
wicks[26] = (0.4, 0.00) # low 82.30 > 82.00 (piciorul 2 rămâne minim local)
wicks[B3] = (0.4, 0.45) # bottom 3: low 81.45 = stopul
wicks[32] = (0.4, 0.00) # low 81.90 (nu coboară sub 81.45)
wicks[I_DOJI] = (0.70, 1.10) # doji: high 86.80, low 84.90
wicks[35] = (0.4, 0.00) # prima bară după intrare nu se mai duce sub 89.50
wicks[I_RETEST] = (0.4, 1.40) # retest: low 89.60, peste intrarea de 89.50
# volume construite: vânzare peste medie la cele două picioare, volum mare pe
# confirmare, volum mic pe flag. Nu sunt numere din curs.
vols = [
55, 52, 48, 50, 45, 47, 44, 46, 42, 44,
40, 42, 38, 41, 58,
62, 68, 50, 46, 44,
43, 45, 44, 47, 46,
45, 47, 52, 45, 44,
48, 70, 62, 58, 88,
76, 70, 58, 62, 66,
72, 70, 44, 40, 38,
42, 74, 70, 64, 68,
72, 66, 74, 78, 82,
90, 96,
]
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[I_DOJI].note = ("doji = corp mic (o 86.10, c 86.00): cumpărătorii și vânzătorii sunt la "
"egalitate, exact pe pivotul lunar 86.00")
TP1 = round(ENTRY + (ENTRY - SL), 2) # ținta 1:1: cât ai riscat
assert abs(TP1 - 97.55) < 1e-9, f"ținta 1:1 = {TP1}, capitolul spune 97.55"
R = (TP - ENTRY) / (ENTRY - SL)
assert 3.2 <= R <= 3.4, f"{R:.2f}R, capitolul spune „cam 3,3R”"
I_TP1 = next(i for i in range(35, len(cds)) if cds[i].h >= TP1) # prima bară peste 1:1
# „a câta bară după intrare" — numărătoare fără ambiguitate 0/1, spre deosebire
# de „bara 40", care e indexul din serie, dar a 41-a bară dacă numeri cu degetul.
BARS_TP1 = I_TP1 - I_CONF
assert cds[I_TP1 - 1].h < TP1 <= cds[I_TP1].h, "I_TP1 nu e prima bară peste ținta 1:1"
assert BARS_TP1 == 6, f"ținta 1:1 la {BARS_TP1} bare după intrare, aștept 6"
cds[I_TP1].note = (f"ținta 1:1 ({TP1:.2f}) e atinsă a {BARS_TP1}-a bară după intrare: acolo "
"vinzi o treime, restul merge mai departe — de asta nu vinzi tot la 1:1")
# --- assert-urile din specificație (toate, ca assert reale) ----------------
# 1. pivot 86.00 e straddled de doji
assert cds[I_DOJI].l <= PIVOT <= cds[I_DOJI].h, "doji-ul nu straddlează 86.00"
# corpul dojiului stă pe pivot, nu la un dolar sub el (eticheta spune „exact pe")
assert min(cds[I_DOJI].o, cds[I_DOJI].c) <= PIVOT <= max(cds[I_DOJI].o, cds[I_DOJI].c) + 0.15, \
f"corpul dojiului ({cds[I_DOJI].o}/{cds[I_DOJI].c}) nu e pe pivotul {PIVOT}"
# 2. confirmarea: close[34] == 89.50 == intrare și > high-ul dojiului
assert abs(cds[I_CONF].c - ENTRY) < 1e-9, f"close[{I_CONF}]={cds[I_CONF].c} != intrare 89.50"
assert cds[I_CONF].c > cds[I_DOJI].h, "confirmarea nu închide peste high-ul dojiului"
# 3. SL = extrema ultimelor 4 candele de la confirmare
min_win = min(c.l for c in cds[I_CONF - 4:I_CONF + 1])
assert abs(min_win - SL) < 1e-9, f"min low[{I_CONF - 4}:{I_CONF + 1}]={min_win} != 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, dar pivotul atins de două ori — aserțiunea care ține
# laolaltă cele două formulări din capitol („trei minime aproape egale" și
# „doji la al doilea test al pivotului 86.00").
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 == [B1, B2, B3], f"minimele fundului: {bottom_idx}, aștept [{B1}, {B2}, {B3}]"
assert len(bottom_idx) == 3, f"{len(bottom_idx)} minime locale în zona fundului, aștept 3"
b1, b2, b3 = (lows[i] for i in bottom_idx)
# „aproape egale": nici unul nu e cu mai mult de 2 % sub primul
assert max(b1, b2, b3) - min(b1, b2, b3) <= 0.02 * b1, \
f"minimele nu sunt aproape egale: {b1}/{b2}/{b3}"
assert abs(b1 - 82.40) < 1e-9 and abs(b2 - 82.00) < 1e-9 and abs(b3 - SL) < 1e-9, \
f"low-urile triplului fund: {b1}/{b2}/{b3}"
# un minim e „atingere a pivotului" doar dacă prețul a fost peste 86.00 după
# minimul dinainte: coboară prin pivot ca să ajungă acolo. Al treilea nu e.
tests = []
for k, i in enumerate(bottom_idx):
prev = bottom_idx[k - 1] + 1 if k else 0
if max(c.h for c in cds[prev:i]) >= PIVOT:
tests.append(i)
assert tests == [B1, B2], f"atingeri ale pivotului la {tests}, aștept [{B1}, {B2}]"
assert len(tests) == 2, f"{len(tests)} atingeri ale pivotului 86.00, aștept 2"
# între primele două picioare prețul urcă clar peste pivot (al doilea test)
assert max(c.h for c in cds[B1 + 1:B2]) > PIVOT + 1.5, "nu e revenire peste pivot înainte de B2"
# între al doilea și al treilea NU mai ajunge la pivot: al treilea nu e test
assert max(c.h for c in cds[B2 + 1:B3]) < PIVOT, \
f"pull-up-ul dintre B2 și B3 atinge pivotul ({max(c.h for c in cds[B2 + 1:B3]):.2f})"
# 7. TP atins la ~o lună de bursă de la confirmare (capitolul: „într-o lună")
assert abs(cds[I_TP].c - TP) < 1e-9, f"close[{I_TP}]={cds[I_TP].c} != {TP}"
assert max(c.h for c in cds[I_CONF + 1:I_TP]) < TP, "TP atins mai devreme de bara 55"
bars_to_tp = I_TP - I_CONF
assert 20 <= bars_to_tp <= 22, f"{bars_to_tp} bare până la +30 %, aștept ~21 (o lună)"
# 8. doji: corp <= 10 % din amplitudine
assert abs(cds[I_DOJI].o - cds[I_DOJI].c) <= 0.10 * (cds[I_DOJI].h - cds[I_DOJI].l), \
"bara doji nu e doji"
assert abs(cds[I_DOJI].h - DOJI_H) < 1e-9 and abs(cds[I_DOJI].l - DOJI_L) < 1e-9, \
"doji h/l nu ies 86.80/84.90"
# 9. retestul: revine până pe intrare și ține (nu o rupe)
assert ENTRY <= cds[I_RETEST].l <= ENTRY + 0.5, \
f"retestul nu atinge intrarea: low {cds[I_RETEST].l}"
assert min(c.l for c in cds[I_CONF + 1:]) >= ENTRY, "prețul cade sub intrare după confirmare"
# 10. bull flag: 4 candele strânse, sub high-ul barei dinainte, urmate de breakout
f0, f1 = FLAG
flag_h = max(c.h for c in cds[f0:f1 + 1])
flag_l = min(c.l for c in cds[f0:f1 + 1])
assert flag_h - flag_l <= 2.5, f"flag prea larg: {flag_h - flag_l:.2f}"
assert flag_h <= cds[f0 - 1].h, "flagul face high nou — nu mai e consolidare"
assert cds[f1 + 1].c > flag_h, "nu există breakout după flag"
# în plus: confirmarea vine cu volum peste medie, flagul cu volum sub medie
mean_vol = sum(vols) / len(vols)
assert vols[I_CONF] > mean_vol, f"volumul confirmării {vols[I_CONF]} <= media {mean_vol:.0f}"
assert max(vols[f0:f1 + 1]) < mean_vol, "flagul nu vine pe volum scăzut"
entry = cds[34].c
w = 990 # 57 bare × gap 15 + loc pentru etichetele din dreapta
ch = Chart(
candles=cds,
levels=[Level(PIVOT, "pivot lunar 86.00", "#777", dash="4 3", label_at="left"),
Level(SL, "stop 81.45 = 9 % sub intrare (procent real)", "#c00", label_at="left"),
Level(ENTRY, "intrare 89.50", "#188a3e"),
Level(TP1, f"ținta 1:1 {TP1:.2f}", "#777", dash="2 3", label_at="left"),
Level(TP, "116.35 = +30 % față de intrare (procent real)", "#1a5fb4", label_at="left")],
zones=[Zone(0, B1 + 1, "1. setup: scădere lungă, apoi primul test al pivotului 86.00", "#777"),
Zone(21, I_DOJI, "testul 2: minimele 2, 3 și doji-ul — fără revenire peste 86.00", "#777"),
Zone(I_DOJI, I_CONF, "2. doji + confirmare", "#b06000"),
# p_top sub 116.35: altfel eticheta zonei 3 se așază peste linia țintei
Zone(I_CONF + 1, I_TP - 1, "3. trendul pornește", "#188a3e", p_top=113.0),
Zone(I_TP, len(cds) - 1, "4. ținta", "#1a5fb4")],
arrows=[Arrow(I_CONF, "aici intri, la close 89.50:\nabia acum știi că au câștigat cumpărătorii",
ENTRY, where="above", color="#188a3e", dx=-150),
Arrow(B3, "minimul 3: 81.45 — tot din testul 2,\nprețul n-a mai urcat peste 86.00 după minimul 2\naici pui stopul, pe cel mai jos dintre cele trei minime",
SL, where="below", color="#c00", dx=-150),
Arrow(I_RETEST, "retest: cade înapoi pe intrare\nși ține — trendul e sănătos",
cds[I_RETEST].l, where="below", color="#188a3e", dx=110, dy=56),
Arrow(FLAG[0] + 1, "aici se odihnește, nu se strică:\nbull flag, pe volum mic",
cds[FLAG[0] + 1].h, where="above", color="#188a3e", dx=-20),
Arrow(I_TP, f"a ajuns la 116.35: +30 % în 21 de zile de bursă,\ncam {R:.1f}R din cât ai riscat".replace("3.3R", "3,3R"),
TP, where="left", color="#1a5fb4", dx=-30, dy=-40)],
marks=[Mark(B1, f"testul 1 · {lows[B1]:.2f}", "#c00", "below"),
Mark(20, "revenire peste pivot · 88.90", "#777", "above"),
Mark(B2, f"testul 2 · {lows[B2]:.2f}", "#c00", "below")],
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 puse de sumarizator."),
)
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)
W_SVG, H = (float(x) for x in re.search(r'viewBox="0 0 (\d+) ([\d.]+)"', svg).groups())
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_SVG + 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 ".md:" not in svg and "Surse" not in svg, "trimiteri la surse în SVG"
assert xs[-1] + ch.body_w / 2 < W_SVG, 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}; pivotul 86.00 atins de "
f"{len(tests)} ori (barele {tests}); +30 % după {bars_to_tp} bare "
f"volum confirmare {vols[34]} > medie {mean_vol:.0f}")
print(f"scris {OUT} ({W_SVG:.0f}x{H:.0f}); {len(labels)} etichete verificate, fără suprapuneri")
OUT.write_text(svg, encoding="utf-8")