La mdsrt_aplus eticheta spunea "primul low 94.60" pentru o bara al carei low era 94.50. Reparatia a mutat low-ul, nu eticheta: regula capitolului numara piciorul dupa low, iar specul spune tot 94.60 - doar umbra de 0.10 o contrazicea. Nicio cifra tiparita nu se schimba, iar nivelul isi ia acum eticheta din date, cu assert, ca sa nu mai poata minti. Cele trei de la mp2 aveau toate aceeasi cauza fizica: text colorat peste un corp de candela de aceeasi culoare. Acolo caracterele nu se incaleca, dispar - "intrare 577.00" se citea "intrare 577. 0", "crossing" se citea "crossi g". VERDICT pe restul de ~94: nu merita un lot. Din 20 de suprapuneri vazute efectiv pe ecran, doar 3 schimbau ce citesti, si toate trei din cauza aia. Celelalte 17 - text peste fitil, peste linie de nivel, peste chenar, peste coada de sageata - raman lizibile, fiindca fundalul e o linie subtire, nu o suprafata plina. Filtrul util nu e tiparul geometric, ci: eticheta cu cifra, peste un <rect> de aceeasi culoare. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
289 lines
14 KiB
Python
289 lines
14 KiB
Python
"""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
|
||
sys.path.insert(0, "/workspace/atm-curs-dl/gen")
|
||
from eticheta_libera import eticheta_libera # 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} (peste suport)", RED, label_at="right"),
|
||
# label_dy: intrarea 577.00 și neckline-ul 578.00 sunt la 1.00
|
||
# (7.5 px) una de alta, iar linia portocalie trecea exact prin
|
||
# zecimala etichetei verzi; eticheta coboară sub linia ei
|
||
Level(entry_a, f"intrare {entry_a:.2f}", GREEN, label_at="left",
|
||
label_dy=18),
|
||
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),
|
||
# dx: pe candela ei, eticheta cădea peste corpul verde de
|
||
# alături și se citea „crossi g" (verde pe verde, 02.10.2026)
|
||
Mark(ENTRY_A, "crossing", GREEN, "above", dy=-6, dx=-32)],
|
||
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ță: 554.00 + double bottom + neckline 578.00 · 1R 22.60",
|
||
)
|
||
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")],
|
||
# idem: „(mijloc)" intra sub corpul verde următor; mai sus nu e loc,
|
||
# candelele urcă, deci eticheta pleacă lateral
|
||
marks=[Mark(ENTRY_M, "crossing (mijloc)", GREEN, "above", dy=-4, dx=-50)],
|
||
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.",
|
||
"Cifrele A+ sunt din material; trimiterile stau în fișa de surse a capitolului.",
|
||
]
|
||
|
||
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} (peste suport)", "intrare 577.00",
|
||
"crossing", "1R 22.60",
|
||
"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}"
|
||
|
||
# cele două etichete mutate pe 02.10.2026: verificarea rămâne pornită ca
|
||
# o mutare viitoare să nu le bage înapoi sub corpurile verzi
|
||
eticheta_libera(wrapper, "aplus-crossing", {"crossing", "crossing (mijloc)"},
|
||
rects=True)
|
||
|
||
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")
|