Userul a respins mostra de format tutorial: prea lunga, prea multe trimiteri in text, termeni tehnici nedefiniti, prea multe cifre fara desen. Cerintele lui sunt acum contract in STYLE.md (FORMAT TUTORIAL v2). Textul: 8430 -> 4190 de cuvinte. Nivel 1 (din avion, cu mini-glosar) -> nivel 2 (6 pasi mecanici, o diagrama fiecare) -> oglinda short -> nivel 3 (rafinari). Trimiterile fisier:linie ies din corp si se aduna la final, cu legenda de prescurtari. Doua tabele in tot documentul. Toate cele 14 diagrame refacute: zone dreptunghiulare de evidentiere si sageti care spun si de ce, nu doar ce. svg_candles primeste Zone si Arrow; render() nu mai muta obiectul la iesire. Trei corecturi de continut: - un singur trade prin toata procedura (doji 50.10/49.25, intrare 50.40, stop 49.20, 1R 1.20, TP1 51.60); seria e acum o functie comuna, importata de stop/transe/iesire - short: stopul urca la 100.15, ca sa fie deasupra maximului cum cere regula oglindita; cifra cursului ramane in text ca observatie - LABU: trei minime dar doua atingeri ale pivotului 86.00, ceea ce impaca "triple bottom" cu "al doilea test", ambele din transcriere Reparat si bug-ul vechi din buy_doji_sell.svg: titlul peste eticheta dojiului. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
281 lines
14 KiB
Python
281 lines
14 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 78.00.
|
|
|
|
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
|
|
|
|
# ============================================================ 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 78.00. 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 > 78.50 and cds_n[-1].c < 78.00, "NU nu cade sub 78.00 la final"
|
|
assert cds_n[13].c < 78.00 < cds_n[DOJI_N].o, "NU: uptrendul nu se oprește sub 78.00"
|
|
|
|
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(42.00, "suport 42.00", 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(78.50, "rezistență 78.50", 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, "cade înapoi sub 78.00:\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 ("suport 42.00", "doji chiar pe nivel", "vânzarea s-a oprit", "volum în creștere = acumulare",
|
|
"rezistență 78.50", "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}"
|
|
|
|
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")
|