Files
atm-curs-dl/gen/strategii/buy_doji_aplus.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

296 lines
15 KiB
Python

"""Buy Doji — A+ vs. mediocru (două panouri, exemple construite).
Citește `gen/strategii/buy_doji_aplus.spec.txt` (marcajul `<!-- DIAGRAMA: aplus -->`).
Același pattern (trend → doji → confirmare), context opus:
- stânga (A+): downtrend 52 → 42.60, doji cu low 41.95 chiar pe suportul
42.00 (0.12 % sub nivel), confirmare close 43.60 pe volum peste media
ultimelor 10 bare — acumulare;
- dreapta (NU): uptrend 70 → 76.80, gap-up la open 78.20, doji lipit de
rezistența 78.50, confirmare close 79.30 pe volum sub medie („varză"), apoi
prețul cade înapoi sub rezistența 78.50, de unde plecase.
Nivelurile 42.00 / 78.50 și cifrele de preț sunt construite de mine ca să
susțină etichetele și assert-urile din spec (materialul nu dă prețuri pentru
acest contrast). Două `Chart`-uri randate separat, lipite într-un SVG wrapper:
coordonatele fiecărui panou rămân corecte prin construcție.
Rulează: python3 gen/strategii/buy_doji_aplus.py
Scrie: gen/strategii/buy_doji_aplus.svg
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Arrow, Candle, Chart, Level, Zone, from_closes # noqa: E402
OUT = Path(__file__).resolve().parent / "buy_doji_aplus.svg"
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
DOJI_A, CONF_A = 10, 11 # indicii în panoul A+
DOJI_N, CONF_N = 14, 15 # indicii în panoul NU
SUPORT_A = 42.00 # singurele prețuri DESENATE ca nivel; etichetele
REZIST_N = 78.50 # se scriu din ele, ca textul să nu ia altă cifră
# ============================================================ PANOUL A+ (24 bare)
# 10 bare de downtrend de la 52.00 spre 42.60, apoi doji, apoi confirmare.
CLOSES_A = [
51.60, 50.80, 50.00, 49.20, 48.40, 47.60, 46.80, 45.50, 44.20, 42.60, # 0-9 downtrend
42.60, # 10 doji (open = close)
43.60, # 11 confirmare
44.10, 44.50, 44.20, 44.80, 45.20, 45.00, 45.50, 45.90, 46.20, 46.50, 46.80, 47.10, # 12-23 acumulare
]
assert len(CLOSES_A) == 24, len(CLOSES_A)
WICKS_A = [(0.30, 0.30)] * 24
WICKS_A[DOJI_A] = (0.30, 0.65) # high 42.90, low 41.95 (pe suportul 42.00)
WICKS_A[CONF_A] = (0.25, 0.30) # high 43.85
VOLS_A = [56, 53, 51, 49, 47, 45, 44, 42, 41, 39, # 0-9 vânzare în scădere
60, # 10 doji: exces de vânzare
86, # 11 confirmare: volum mare
72, 75, 70, 78, 82, 80, 85, 88, 92, 95, 98, 100] # 12-23 acumulare
cds_a = from_closes(CLOSES_A, wicks=WICKS_A, vols=VOLS_A, start=52.00)
doji_a, conf_a = cds_a[DOJI_A], cds_a[CONF_A]
# ============================================================ PANOUL NU (24 bare)
# uptrend 70 → 76.80 (14 bare), gap-up la open 78.20 + doji, confirmare 79.30
# pe volum slab, apoi cădere înapoi sub rezistența 78.50. Lista e manuală pentru că gap-ul
# (open > high-ul barei precedente) nu iese din `from_closes`.
BARE_NU = [
# o, h, l, c, vol
(70.00, 70.70, 69.80, 70.50, 48), (70.50, 71.20, 70.30, 71.00, 50),
(71.00, 71.75, 70.85, 71.55, 49), (71.55, 72.30, 71.40, 72.10, 52),
(72.10, 72.80, 71.95, 72.60, 51), (72.60, 73.30, 72.45, 73.10, 54),
(73.10, 73.80, 72.95, 73.60, 53), (73.60, 74.25, 73.45, 74.05, 56),
(74.05, 74.70, 73.90, 74.50, 55), (74.50, 75.15, 74.35, 74.95, 58),
(74.95, 75.60, 74.80, 75.40, 57), (75.40, 76.00, 75.25, 75.80, 60),
(75.80, 76.40, 75.65, 76.20, 59), (76.20, 76.80, 76.05, 76.80, 62), # 13: high 76.80
(78.20, 78.50, 78.10, 78.20, 58), # 14: GAP-UP + doji (o = c = 78.20)
(78.20, 79.50, 78.10, 79.30, 30), # 15: confirmare, volum slab
(79.30, 79.40, 78.40, 78.60, 55), (78.60, 78.70, 77.80, 78.00, 60),
(78.00, 78.10, 77.20, 77.40, 64), (77.40, 77.55, 76.80, 77.00, 62),
(77.00, 77.15, 76.40, 76.60, 66), (76.60, 76.75, 76.10, 76.30, 63),
(76.30, 76.40, 75.80, 76.00, 65), (76.00, 76.15, 75.50, 75.70, 61),
]
assert len(BARE_NU) == 24, len(BARE_NU)
cds_n = [Candle(o, h, l, c, vol=v) for (o, h, l, c, v) in BARE_NU]
doji_n, conf_n = cds_n[DOJI_N], cds_n[CONF_N]
# ============================================================ assert-urile din spec
# 1. A+: low-ul dojiului e la ≤ 0,5 % de 42.00 (41.95 e la 0.12 %)
dist_a = abs(doji_a.l - 42.00) / 42.00
assert dist_a <= 0.005, f"low doji A+ {doji_a.l} la {dist_a * 100:.2f} % de 42.00"
assert abs(doji_a.l - 41.95) < 1e-9, f"low doji A+ = {doji_a.l}, aștept 41.95"
# 2. A+: volumul pe bara de confirmare > media volumelor pe 10 bare
media_a = sum(VOLS_A[CONF_A - 10:CONF_A]) / 10
assert VOLS_A[CONF_A] > media_a, f"vol confirmare {VOLS_A[CONF_A]} <= media {media_a}"
# 3. NU: doji-ul apare după un gap-up (open doji > high bara precedentă)
assert cds_n[DOJI_N].o > cds_n[DOJI_N - 1].h, "doji NU nu e după gap-up"
assert abs(cds_n[DOJI_N].o - 78.20) < 1e-9 and abs(cds_n[DOJI_N - 1].h - 76.80) < 1e-9
# 4. NU: |high doji - 78.50| / 78.50 <= 0.005
dist_n = abs(doji_n.h - 78.50) / 78.50
assert dist_n <= 0.005, f"high doji NU {doji_n.h} la {dist_n * 100:.2f} % de 78.50"
# 5. NU: volumul pe bara de confirmare < media pe 10 bare
media_n = sum(b[4] for b in BARE_NU[CONF_N - 10:CONF_N]) / 10
assert conf_n.vol < media_n, f"vol confirmare NU {conf_n.vol} >= media {media_n}"
# 6. ambele: close-ul de confirmare > high-ul dojiului (tehnic valid)
assert conf_a.c > doji_a.h, f"A+ close {conf_a.c} <= high doji {doji_a.h}"
assert conf_n.c > doji_n.h, f"NU close {conf_n.c} <= high doji {doji_n.h}"
assert abs(conf_a.c - 43.60) < 1e-9 and abs(conf_n.c - 79.30) < 1e-9
# 7. ambele doji: abs(o-c) <= 0.10*(h-l)
for tag, d in (("A+", doji_a), ("NU", doji_n)):
assert abs(d.o - d.c) <= 0.10 * (d.h - d.l), f"bara doji {tag} nu e doji"
# suplimentare: structura promisă de spec
assert min(c.l for c in cds_a[:10]) > doji_a.l, "low-ul dojiului A+ nu e minimul"
assert cds_a[9].c < cds_a[0].c - 8, "downtrend A+ prea mic"
assert cds_n[CONF_N].c > REZIST_N, "NU: confirmarea nu iese peste rezistență"
assert cds_n[-1].c < REZIST_N, "NU: prețul nu cade înapoi sub rezistență la final"
assert cds_n[13].c < REZIST_N <= doji_n.h, "NU: uptrendul nu se oprește sub rezistență"
# bara pe care stă săgeata „cade înapoi sub …" trebuie chiar să fie sub nivel
CADE_N = next(i for i in range(CONF_N + 1, len(BARE_NU)) if cds_n[i].c < REZIST_N)
assert CADE_N == 17 and cds_n[18].c < REZIST_N, f"prima cădere sub nivel e bara {CADE_N}"
print(f"A+ doji {doji_a.h:.2f}/{doji_a.l:.2f} conf {conf_a.c:.2f} "
f"vol {VOLS_A[CONF_A]} > medie {media_a:.1f}")
print(f"NU doji {doji_n.h:.2f}/{doji_n.l:.2f} conf {conf_n.c:.2f} "
f"vol {conf_n.vol} < medie {media_n:.1f}")
# ============================================================ desenele celor două panouri
PW = 560 # lățimea fiecărui panou
ch_a = Chart(
candles=cds_a,
levels=[Level(SUPORT_A, f"suport {SUPORT_A:.2f}", GREEN, label_at="left")],
zones=[Zone(DOJI_A, DOJI_A, "", GREEN, p_top=43.2, p_bot=41.7)],
arrows=[Arrow(DOJI_A, "doji chiar pe nivel:\nvânzarea s-a oprit pe suport",
where="below", color=GREEN, dx=-40),
Arrow(CONF_A, "confirmarea, pe volum mare:\ncumpărătorii mari intră",
where="above", color=GREEN, dx=60)],
mas=[], scale=22.0, gap=15, body_w=9, left=44, width=PW, vol_h=60,
vol_label="volum", title="A+ — doji la suport, volum în acumulare",
)
ch_n = Chart(
candles=cds_n,
levels=[Level(REZIST_N, f"rezistență {REZIST_N:.2f}", RED, label_at="right")],
# eticheta zonei ar urca peste titlul panoului (zona e chiar în vârf):
# textul stă pe săgeata din stânga
zones=[Zone(DOJI_N - 1, CONF_N, "", RED, p_top=79.62)],
arrows=[Arrow(DOJI_N, "gap-up în rezistență:\naici cumpără cei întârziați",
where="left", color=RED, dx=-30, dy=30),
Arrow(18, f"cade înapoi sub rezistența {REZIST_N:.2f}:\nsemnalul a mințit",
where="below", color=RED, dx=40)],
mas=[], scale=22.0, gap=15, body_w=9, left=44, width=PW, vol_h=60,
vol_label="volum", title="Mediocru — gap-up în rezistență, volum varză",
)
# ============================================================ wrapper: două panouri alăturate
def inner(svg: str) -> str:
"""Conținutul dintre tag-urile <svg> ale unui panou, fără elementul rădăcină."""
return svg[svg.index(">") + 1:svg.rindex("</svg>")]
def vb_height(svg: str) -> float:
m = re.search(r'viewBox="0 0 [\d.]+ ([\d.]+)"', svg)
assert m, "nu găsesc viewBox în panoul randat"
return float(m.group(1))
def vol_note(svg: str, i0: int, i1: int, text: str, color: str, box: bool) -> str:
"""Evidențiere în panoul de volum (Zone/Arrow din svg_candles lucrează doar pe preț).
Coordonatele se citesc din SVG-ul randat (barele de volum sunt rect-urile cu
opacity 0.55, în ordinea candelelor), ca să nu depindă de deplasarea internă
a lui Chart. Eticheta stă sub linia de bază, unde nu acoperă nimic;
`box` = dreptunghi peste bare, altfel săgeată de jos în sus, sub bară.
"""
bars = [tuple(map(float, m)) for m in re.findall(
r'<rect x="([\d.]+)" y="([\d.]+)" width="([\d.]+)" height="([\d.]+)" '
r'fill="#[0-9a-f]+" opacity="0.55"/>', svg)]
base = bars[0][1] + bars[0][3]
x0, x1 = bars[i0][0] - 4, bars[i1][0] + bars[i1][2] + 4
top = min(b[1] for b in bars[i0:i1 + 1]) - 4
if box:
mark = (f'<rect x="{x0:.1f}" y="{top:.1f}" width="{x1 - x0:.1f}" height="{base - top:.1f}" '
f'fill="{color}" fill-opacity="0.12" stroke="{color}" stroke-width="1"/>')
ty = base + 15
else:
cx = (x0 + x1) / 2
mark = (f'<line x1="{cx:.1f}" y1="{base + 22:.1f}" x2="{cx:.1f}" y2="{base + 9:.1f}" '
f'stroke="{color}" stroke-width="1.8"/>'
f'<polygon points="{cx:.1f},{base + 2:.1f} {cx - 4:.1f},{base + 10:.1f} '
f'{cx + 4:.1f},{base + 10:.1f}" fill="{color}"/>')
ty = base + 34
label = (f'<text x="{x0:.1f}" y="{ty:.1f}" fill="{color}" '
f'style="font-weight:bold">{text}</text>')
# dreptunghiul/săgeata în spatele barelor, eticheta la final
i = svg.index('<rect', svg.index(f'y1="{base:.1f}"'))
svg = svg[:i] + mark + svg[i:]
return svg.replace("</svg>", label + "</svg>")
def grow(svg: str, dh: float) -> str:
"""Mărește viewBox-ul unui panou cu dh px jos, pentru eticheta de sub volum."""
h = vb_height(svg)
return svg.replace(f'0 0 {PW} {h:.0f}"', f'0 0 {PW} {h + dh:.0f}"', 1)
PANEL_GAP, PAD = 24, 8
TITLE_H, FOOTER_H = 30, 0
svg_a = grow(vol_note(ch_a.render(), CONF_A, CONF_A + 4,
"volum în creștere = acumulare", GREEN, box=True), 10)
svg_n = grow(vol_note(ch_n.render(), CONF_N, CONF_N,
"volum slab: nimeni mare nu cumpără", RED, box=False), 30)
HA, HN = vb_height(svg_a), vb_height(svg_n)
assert svg_a.count("\n\n") == 0 and svg_n.count("\n\n") == 0, "linie goală în panou"
TOTAL_W = PAD + PW + PANEL_GAP + PW + PAD
TOTAL_H = PAD + TITLE_H + max(HA, HN) + FOOTER_H + PAD
OFF_A = (PAD, PAD + TITLE_H)
OFF_N = (PAD + PW + PANEL_GAP, PAD + TITLE_H)
wrapper = (
f'<svg viewBox="0 0 {TOTAL_W} {TOTAL_H:.0f}" width="{TOTAL_W}" height="{TOTAL_H:.0f}" '
f'xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;font:12px sans-serif">'
f'<text x="{TOTAL_W / 2:.0f}" y="20" fill="#333" text-anchor="middle" '
f'style="font-weight:bold">Setup A+ vs. setup mediocru (două panouri, exemple construite)</text>'
f'<g transform="translate({OFF_A[0]},{OFF_A[1]})">{inner(svg_a)}</g>'
f'<g transform="translate({OFF_N[0]},{OFF_N[1]})">{inner(svg_n)}</g>'
f'</svg>'
)
# ============================================================ verificare pe SVG-ul final
def texts(svg: str):
"""Toate (x, y, text-anchor, text) din elementele <text> ale unui fragment."""
out = []
for m in re.finditer(r'<text x="([-\d.]+)" y="([-\d.]+)"([^>]*)>(.*?)</text>', svg):
x, y, attrs, t = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
if t.startswith("<"):
continue
am = re.search(r'text-anchor="(\w+)"', attrs)
out.append((x, y, am.group(1) if am else "start", t))
return out
def span(x: float, anchor: str, t: str) -> tuple[float, float]:
"""Intervalul orizontal aproximativ al unui text, față de ancora lui."""
w = len(t) * 12 * 0.56
left = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
return left, left + w
assert wrapper.count("\n\n") == 0, "linie goală în SVG"
# fiecare text (după translatarea panoului) e în întregime în interiorul pânzei
all_txt = []
for frag, (ox, oy) in ((inner(svg_a), OFF_A), (inner(svg_n), OFF_N)):
for x, y, anchor, t in texts(frag):
gx, gy = x + ox, y + oy
gl, gr = gx + span(0, anchor, t)[0], gx + span(0, anchor, t)[1]
assert ox <= gl and gr <= ox + PW, f"text iese din panou: {t!r} ({gl:.0f}..{gr:.0f})"
assert -1 <= gl and gr <= TOTAL_W + 1, f"text ieșit pe orizontală: {t!r} ({gl:.0f}..{gr:.0f})"
assert 0 <= gy <= TOTAL_H, f"text ieșit pe verticală: {t!r} ({gy:.0f})"
all_txt.append((gx, gy, anchor, t))
# textele din wrapper (titlu + footer), în coordonate globale
for x, y, anchor, t in ((TOTAL_W / 2, 20.0, "middle",
"Setup A+ vs. setup mediocru (două panouri, exemple construite)"),):
gl, gr = span(x, anchor, t)
assert -1 <= gl and gr <= TOTAL_W + 1, f"text din wrapper iese din pânză: {t!r}"
assert 0 <= y <= TOTAL_H, f"text din wrapper în afara înălțimii: {t!r}"
all_txt.append((x, y, anchor, t))
assert len(all_txt) >= 8, "prea puține etichete"
# nu se suprapun două texte pe aceeași poziție sau pe rândul apropiat
for i in range(len(all_txt)):
xi, yi, ai, si = all_txt[i]
li, ri = (xi + span(0, ai, si)[0], xi + span(0, ai, si)[1])
for j in range(i + 1, len(all_txt)):
xj, yj, aj, sj = all_txt[j]
lj, rj = xj + span(0, aj, sj)[0], xj + span(0, aj, sj)[1]
if abs(yi - yj) < 12 and li < rj - 4 and lj < ri - 4:
raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
# etichetele cerute de spec există în desen
assert "Surse" not in wrapper and ".md:" not in wrapper, "trimiteri la surse în SVG"
for cheie in (f"suport {SUPORT_A:.2f}", "doji chiar pe nivel", "vânzarea s-a oprit", "volum în creștere = acumulare",
f"rezistență {REZIST_N:.2f}",
f"cade înapoi sub rezistența {REZIST_N:.2f}", "gap-up în rezistență", "cei întârziați",
"semnalul a mințit", "volum slab: nimeni mare nu cumpără",
"A+ — doji la suport, volum în acumulare",
"Mediocru — gap-up în rezistență, volum varză"):
assert cheie in wrapper, f"lipsește din desen: {cheie!r}"
# nicio etichetă nu are voie să numească un preț care nu e desenat ca nivel —
# exact bug-ul „cade înapoi sub 78.00" lângă singurul nivel desenat, 78.50
NIVELURI = {f"{SUPORT_A:.2f}", f"{REZIST_N:.2f}"}
for _, _, _, t in all_txt:
for n in re.findall(r"\d+\.\d\d", t):
assert n in NIVELURI, f"eticheta {t!r} numește prețul {n}, nedesenat ca nivel"
OUT.write_text(wrapper, encoding="utf-8")
print(f"scris {OUT.name}: {TOTAL_W}x{TOTAL_H:.0f}, "
f"{len(all_txt)} etichete, verificare OK")