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
This commit is contained in:
272
gen/strategii/mp2_aplus.py
Normal file
272
gen/strategii/mp2_aplus.py
Normal file
@@ -0,0 +1,272 @@
|
||||
"""MP2 crossing long — setup A+ vs. setup mediocru (două panouri).
|
||||
|
||||
Citește `gen/strategii/mp2_aplus.spec.txt` (TIP: PRET).
|
||||
|
||||
Panoul stânga (A+): SPY, aprilie — downtrend spre suportul quarterly 554.00,
|
||||
bază cu Inverted Head & Shoulders (umăr stâng 566, head 555, umăr drept 568,
|
||||
neckline 578.00, adică și un double bottom la ~554.5), intrare 577.00 pe prima
|
||||
candelă verde după umărul drept, SL 554.40 chiar peste suport, 1R = 22.60,
|
||||
TP1 599.60 (1:1).
|
||||
|
||||
Panoul dreapta (Mediocru): integral construit (cifre rotunde), NU din
|
||||
material — range 100.00–110.00, crossing în mijlocul range-ului la 105.00
|
||||
fără niciun pivot/suport dedesubte, intrare 105.00, SL 103.00, TP1 107.00.
|
||||
Eticheta „exemplu construit, nu date reale" e cerută explicit de spec.
|
||||
|
||||
Două `Chart`-uri randate separat, lipite într-un SVG wrapper: fiecare panou
|
||||
are scara lui, deci coordonatele rămân corecte prin construcție.
|
||||
|
||||
Rulează: python3 gen/strategii/mp2_aplus.py
|
||||
Scrie: gen/strategii/mp2_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, from_closes # noqa: E402
|
||||
|
||||
OUT = Path(__file__).resolve().parent / "mp2_aplus.svg"
|
||||
|
||||
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
|
||||
|
||||
# ============================================================ PANOUL A+ (33 bare)
|
||||
# 12 bare de cădere, 10 bare de bază (IHS), candela de intrare, 10 bare de urcare.
|
||||
ENTRY_A = 22 # prima candelă verde după umărul drept
|
||||
LS_A, HEAD_A, RS_A = 14, 17, 20 # umăr stâng / head / umăr drept
|
||||
SUP_A, SL_A, NECK_A, TP1_A = 554.00, 554.40, 578.00, 599.60
|
||||
|
||||
CLOSES_A = [
|
||||
596.0, 592.5, 589.0, 585.5, 582.5, 579.5, 576.5, 573.5, 571.0, 568.5, 566.5, 564.0, # 0-11 cădere
|
||||
561.0, 559.5, # 12-13
|
||||
566.0, # 14 umăr stâng (566)
|
||||
561.5, # 15
|
||||
555.8, 555.0, # 16-17 double bottom + head (555)
|
||||
558.5, 563.0, # 18-19
|
||||
568.0, # 20 umăr drept (568)
|
||||
566.5, # 21 pullback
|
||||
577.0, # 22 INTRARE 577.00
|
||||
580.5, 583.5, 585.0, 583.5, 587.0, 590.0, 592.0, 594.0, 596.0, 597.5, # 23-32 urcare
|
||||
]
|
||||
assert len(CLOSES_A) == 33, len(CLOSES_A)
|
||||
|
||||
WICKS_A = [(0.30, 0.30)] * 33
|
||||
WICKS_A[LS_A] = (0.00, 0.30) # high exact 566.00
|
||||
WICKS_A[16] = (0.30, 1.20) # low 554.60 (primul picior al double bottom)
|
||||
WICKS_A[HEAD_A] = (0.30, 0.55) # low 554.45, deasupra suportului 554.00
|
||||
WICKS_A[RS_A] = (0.00, 0.30) # high exact 568.00
|
||||
WICKS_A[ENTRY_A] = (0.25, 0.25)
|
||||
|
||||
VOLS_A = [58, 56, 54, 52, 50, 48, 47, 45, 44, 42, 41, 40,
|
||||
38, 44, 52, 46, 61, 66, 58, 54, 62, 50,
|
||||
88,
|
||||
74, 70, 66, 61, 68, 63, 59, 57, 55, 60]
|
||||
|
||||
cds_a = from_closes(CLOSES_A, wicks=WICKS_A, vols=VOLS_A, start=598.0)
|
||||
|
||||
# ============================================================ PANOUL MEDIOCRU (28 bare)
|
||||
# Integral construit. Range 100.00–110.00, crossing în mijloc la 105.00.
|
||||
ENTRY_M = 7
|
||||
SL_M, TP1_M = 103.00, 107.00
|
||||
RANGE_LO, RANGE_HI = 100.00, 110.00
|
||||
|
||||
CLOSES_M = [
|
||||
100.5, 101.1, 101.8, 102.5, 103.2, 103.9, 104.6, # 0-6 urcare spre 105
|
||||
105.0, # 7 crossing / intrare 105.00
|
||||
105.8, 106.5, 107.0, 106.2, 104.8, 103.4, 102.0, # 8-14 oscilație în range
|
||||
103.0, 104.5, 106.0, 107.5, 108.8, 109.5, 108.6, # 15-21
|
||||
107.2, 105.8, 104.4, 103.2, 104.5, 106.0, # 22-27
|
||||
]
|
||||
assert len(CLOSES_M) == 28, len(CLOSES_M)
|
||||
|
||||
WICKS_M = [(0.30, 0.30)] * 28
|
||||
WICKS_M[ENTRY_M] = (0.30, 0.20)
|
||||
WICKS_M[21] = (0.30, 0.30) # vârf 109.80
|
||||
|
||||
VOLS_M = [42, 44, 46, 48, 50, 52, 55, 72,
|
||||
64, 60, 58, 56, 60, 54, 52,
|
||||
50, 53, 56, 58, 62, 60, 58,
|
||||
55, 52, 50, 48, 51, 54]
|
||||
|
||||
cds_m = from_closes(CLOSES_M, wicks=WICKS_M, vols=VOLS_M, start=100.5)
|
||||
|
||||
# ============================================================ assert-urile din spec
|
||||
entry_a = cds_a[ENTRY_A].c
|
||||
# A+: intrare 577.00
|
||||
assert abs(entry_a - 577.00) < 0.05, f"intrare A+ = {entry_a}, aștept 577.00"
|
||||
# A+: SL 554.40, peste suportul quarterly 554.00 (nu sub el)
|
||||
assert abs(SL_A - 554.40) < 0.05 and SL_A > SUP_A, f"SL A+ {SL_A} vs suport {SUP_A}"
|
||||
# A+: 1R = 22.60
|
||||
r_a = entry_a - SL_A
|
||||
assert abs(r_a - 22.60) < 0.05, f"1R A+ = {r_a}, aștept 22.60"
|
||||
# A+: head 555 < umărul stâng 566 și < umărul drept 568
|
||||
head_a, ls_a, rs_a = cds_a[HEAD_A].c, cds_a[LS_A].h, cds_a[RS_A].h
|
||||
assert head_a < ls_a and head_a < rs_a, f"head {head_a} vs umeri {ls_a}/{rs_a}"
|
||||
# A+: TP1 = entry + 1R = 599.60
|
||||
assert abs(TP1_A - (entry_a + r_a)) < 0.05, f"TP1 A+ {TP1_A}, entry+1R {entry_a + r_a:.2f}"
|
||||
# suplimentar: structura promisă de spec
|
||||
assert cds_a[HEAD_A].l > SUP_A, f"low head {cds_a[HEAD_A].l} a spart suportul {SUP_A}"
|
||||
assert cds_a[HEAD_A].l == min(c.l for c in cds_a), "head-ul nu e minimul seriei"
|
||||
assert CLOSES_A[0] - CLOSES_A[HEAD_A] > 35, "downtrendul A+ e prea mic"
|
||||
assert entry_a > cds_a[RS_A].c, "intrarea nu e deasupra umărului drept"
|
||||
|
||||
# Mediocru: intrare 105.00, SL 103.00, TP1 107.00
|
||||
entry_m = cds_m[ENTRY_M].c
|
||||
assert abs(entry_m - 105.00) < 0.05, f"intrare mediocru = {entry_m}, aștept 105.00"
|
||||
assert abs(SL_M - 103.00) < 0.05 and abs(TP1_M - 107.00) < 0.05, "SL/TP mediocru greșite"
|
||||
# Mediocru: niciun nivel de suport între SL 103.00 și intrarea 105.00
|
||||
levels_m = [RANGE_LO, RANGE_HI, entry_m, SL_M, TP1_M]
|
||||
assert not any(SL_M < p < entry_m for p in levels_m), \
|
||||
f"există suport între {SL_M} și {entry_m}: {levels_m}"
|
||||
# Mediocru: oscilația stă în range
|
||||
assert all(RANGE_LO <= c.c <= RANGE_HI for c in cds_m), "bare în afara range-ului"
|
||||
assert min(c.l for c in cds_m) >= RANGE_LO, "low sub range"
|
||||
|
||||
print(f"A+ : intrare {entry_a:.2f} SL {SL_A:.2f} 1R {r_a:.2f} "
|
||||
f"head {head_a:.0f} < umeri {ls_a:.0f}/{rs_a:.0f} TP1 {TP1_A:.2f}")
|
||||
print(f"MED: intrare {entry_m:.2f} SL {SL_M:.2f} TP1 {TP1_M:.2f} "
|
||||
f"niveluri între SL și intrare: {[p for p in levels_m if SL_M < p < entry_m]}")
|
||||
|
||||
# ============================================================ desenele celor două panouri
|
||||
PW = 560
|
||||
ch_a = Chart(
|
||||
candles=cds_a,
|
||||
levels=[Level(SUP_A, f"suport quarterly {SUP_A:.2f}", GRAY, label_at="left"),
|
||||
Level(SL_A, f"SL {SL_A:.2f}", RED, label_at="right"),
|
||||
Level(entry_a, f"intrare {entry_a:.2f}", GREEN, label_at="left"),
|
||||
Level(NECK_A, f"neckline {NECK_A:.2f}", ORANGE, label_at="right"),
|
||||
Level(TP1_A, f"TP1 {TP1_A:.2f} = 1:1", BLUE, label_at="right")],
|
||||
marks=[Mark(LS_A, f"umăr stâng {ls_a:.0f}", GRAY, "above", dy=-4),
|
||||
Mark(HEAD_A, f"head {head_a:.0f} / double bottom", RED, "below", dy=8),
|
||||
Mark(RS_A, f"umăr drept {rs_a:.0f}", GRAY, "above", dy=-4),
|
||||
Mark(ENTRY_A, "crossing", GREEN, "above", dy=-6)],
|
||||
scale=7.5, gap=15, body_w=9, left=44, width=PW, vol_h=64,
|
||||
vol_label="volum", title="A+ — crossing la confluență (SPY, aprilie)",
|
||||
trend="down",
|
||||
caption="Confluență: suport 554.00 + double bottom 554.45 + neckline 578.00",
|
||||
)
|
||||
ch_m = Chart(
|
||||
candles=cds_m,
|
||||
levels=[Level(RANGE_HI, f"range {RANGE_HI:.2f}", GRAY, label_at="right"),
|
||||
Level(RANGE_LO, f"range {RANGE_LO:.2f}", GRAY, label_at="left"),
|
||||
Level(SL_M, f"SL {SL_M:.2f}", RED, label_at="right"),
|
||||
Level(entry_m, f"intrare {entry_m:.2f}", GREEN, label_at="right"),
|
||||
Level(TP1_M, f"TP1 {TP1_M:.2f}", BLUE, label_at="right")],
|
||||
marks=[Mark(ENTRY_M, "crossing (mijloc)", GREEN, "above", dy=-4)],
|
||||
scale=34.0, gap=15, body_w=9, left=44, width=PW, vol_h=64,
|
||||
vol_label="volum", title="Mediocru — crossing în mijlocul range-ului (construit)",
|
||||
caption="exemplu construit, nu date reale — SL nu stă pe niciun pivot",
|
||||
)
|
||||
|
||||
# ============================================================ 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, 62
|
||||
svg_a, svg_m = ch_a.render(), ch_m.render()
|
||||
HA, HM = vb_height(svg_a), vb_height(svg_m)
|
||||
assert svg_a.count("\n\n") == 0 and svg_m.count("\n\n") == 0, "linie goală în panou"
|
||||
|
||||
TOTAL_W = PAD + PW + PANEL_GAP + PW + PAD
|
||||
TOTAL_H = PAD + TITLE_H + max(HA, HM) + FOOTER_H + PAD
|
||||
OFF_A = (PAD, PAD + TITLE_H)
|
||||
OFF_M = (PAD + PW + PANEL_GAP, PAD + TITLE_H)
|
||||
|
||||
TITLU = "MP² crossing long — setup A+ vs. setup mediocru (aceeași strategie)"
|
||||
LEGENDA = [
|
||||
"Legendă: ce separă cele două sunt nivelurile independente dedesubte "
|
||||
"(suport quarterly + double bottom + neckline),",
|
||||
"nu un filtru pus în scaner. Panoul din dreapta e exemplu construit, nu date reale.",
|
||||
"Surse A+: 09_QA/01_2026-08-25.md:389-394, 485-490, 501-507.",
|
||||
]
|
||||
|
||||
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">{TITLU}</text>'
|
||||
f'<g transform="translate({OFF_A[0]},{OFF_A[1]})">{inner(svg_a)}</g>'
|
||||
f'<g transform="translate({OFF_M[0]},{OFF_M[1]})">{inner(svg_m)}</g>'
|
||||
+ "".join(
|
||||
f'<text x="{TOTAL_W / 2:.0f}" y="{TOTAL_H - FOOTER_H + 18 + 18 * i:.0f}" '
|
||||
f'fill="#555" text-anchor="middle">{ln}</text>'
|
||||
for i, ln in enumerate(LEGENDA)
|
||||
)
|
||||
+ '</svg>'
|
||||
)
|
||||
|
||||
# ============================================================ verificarea 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_m), OFF_M)):
|
||||
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}"
|
||||
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 + legenda), în coordonate globale
|
||||
wtxt = [(TOTAL_W / 2, 20.0, "middle", TITLU)]
|
||||
wtxt += [(TOTAL_W / 2, TOTAL_H - FOOTER_H + 18 + 18 * i, "middle", ln)
|
||||
for i, ln in enumerate(LEGENDA)]
|
||||
for x, y, anchor, t in wtxt:
|
||||
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) >= 12, f"prea puține etichete: {len(all_txt)}"
|
||||
|
||||
# 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 quarterly 554.00", f"SL {SL_A:.2f}", "intrare 577.00",
|
||||
"neckline 578.00", "TP1 599.60 = 1:1",
|
||||
f"umăr stâng {ls_a:.0f}", f"head {head_a:.0f}", f"umăr drept {rs_a:.0f}",
|
||||
"A+ — crossing la confluență (SPY, aprilie)",
|
||||
"Mediocru — crossing în mijlocul range-ului (construit)",
|
||||
"range 110.00", "range 100.00", "SL 103.00", "intrare 105.00", "TP1 107.00",
|
||||
"crossing (mijloc)", "exemplu construit, nu date reale"):
|
||||
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")
|
||||
Reference in New Issue
Block a user