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
248 lines
12 KiB
Python
248 lines
12 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 Candle, Chart, Level, Mark, MA, 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")],
|
|
marks=[Mark(DOJI_A, "doji la nivel (low 41.95)", ORANGE, "below"),
|
|
Mark(CONF_A, "acumulare", GREEN, "above", dy=-6)],
|
|
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",
|
|
caption="acumulare: volum peste media ultimelor 10 bare.",
|
|
)
|
|
ch_n = Chart(
|
|
candles=cds_n,
|
|
levels=[Level(78.50, "rezistență 78.50", RED, label_at="right")],
|
|
marks=[Mark(DOJI_N, "doji 78.50 / 78.10", ORANGE, "below", dy=93)],
|
|
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ă",
|
|
caption="gap-up + doji în rezistență, confirmare pe volum slab.",
|
|
)
|
|
|
|
# ============================================================ 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))
|
|
|
|
|
|
PANEL_GAP, PAD = 24, 8
|
|
TITLE_H, FOOTER_H = 30, 22
|
|
svg_a, svg_n = ch_a.render(), ch_n.render()
|
|
|
|
# „gap-up, volum slab" stă în colțul liber din stânga-sus, cu linie indicatoare
|
|
# spre golul dintre bara 13 (high 76.80) și dojiul barei 14 (open 78.20).
|
|
GAP_X = (ch_n._cx(DOJI_N - 1) + ch_n._cx(DOJI_N)) / 2
|
|
GAP_Y = ch_n._y(77.50)
|
|
gap_ann = (
|
|
f'<text x="28" y="120" fill="{RED}" style="font-weight:bold">gap-up, volum slab</text>'
|
|
f'<line x1="152" y1="116" x2="{GAP_X:.1f}" y2="{GAP_Y:.1f}" stroke="{RED}" stroke-width="1"/>'
|
|
f'<circle cx="{GAP_X:.1f}" cy="{GAP_Y:.1f}" r="2.5" fill="{RED}"/>'
|
|
)
|
|
svg_n = svg_n.replace("</svg>", gap_ann + "</svg>")
|
|
|
|
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)
|
|
|
|
FOOTER = ("Surse: 13_strategia_3_byh.md:188-190 (doji la nivel); "
|
|
"01_buy_doji_si_strategia_mici.md:227-235 (context invalid).")
|
|
|
|
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'<text x="{TOTAL_W / 2:.0f}" y="{TOTAL_H - PAD:.0f}" fill="#555" '
|
|
f'text-anchor="middle">{FOOTER}</text>'
|
|
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)"),
|
|
(TOTAL_W / 2, TOTAL_H - PAD, "middle", FOOTER)):
|
|
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
|
|
for cheie in ("suport 42.00", "doji la nivel", "acumulare",
|
|
"rezistență 78.50", "gap-up, volum slab",
|
|
"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")
|