Corectorul n-are voie la SVG, deci a semnalat si le-am reparat eu, in generatoare: - gen/selectia/cadrane.py: cadranul 3 scria "imbunatatire spre underperform" — chiar langa textul corectat. Acum "inrautatire, histograma inca peste linia 0". - gen/selectia/funnel.py: "0.96 si 0.74 au fost respinse" era fals. Pe YPF la 0.96 zice "din pacate" si il trece oricum pe lista. - gen/strategii/selectia_scor.py: "sub 7 se deduce ca excludere". Bonus, preexistenta: in selectia_exemplu.py eticheta rezistentei 84.50 cadea peste titlu. Mutata la dreapta. audit_diagrams: 0 pe toate. A1_risc: doua trimiteri la fise/selectia.md decalate cu 7 linii. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
209 lines
8.9 KiB
Python
209 lines
8.9 KiB
Python
"""HIG — daily (Sell Doji + Bear Reversal, RVOL 200%): exemplul oglinda (short).
|
|
|
|
Citește `gen/strategii/selectia_exemplu.spec.txt`. Un simbol scos în față de
|
|
filtrul de selecție prin Relative Volume mare (200%, cel mai ridicat din cele
|
|
16 rezultate ale scanerului), dus până la planul de intrare. Grafic daily, 30
|
|
de candele, serie continuă, cu panou de volum și SMA-20 — construit prin
|
|
`svg_candles` (from_closes), la scară.
|
|
|
|
Rulează:
|
|
python3 gen/strategii/selectia_exemplu.py
|
|
Scrie lângă el `selectia_exemplu.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 / "selectia_exemplu.svg"
|
|
|
|
GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000"
|
|
|
|
# ---------------------------------------------------------------- seria (30 bare)
|
|
# bare 0-9 : uptrend / rezistenta spre varful 84.50 (atins pe bara 9)
|
|
# bara 10 : gap down, open 79.20 (< close[9] = 84.20), bara rosie
|
|
# bara 11 : continuare in jos
|
|
# bara 12 : sell doji, close 78.60 (open = close[11] = 78.75, corp 0.15)
|
|
# bara 13 : rupere sub low-ul dojii -> intrare short 78.10
|
|
# bare 14-29 : scadere spre 72.00 (TP atins spre final), mici bare verzi de
|
|
# respiratie (15, 18, 21, 24), dar trend descendent
|
|
CLOSES = [
|
|
80.00, 80.60, 81.20, 81.90, 81.50, # 0-4
|
|
82.30, 82.90, 83.60, 84.00, 84.20, # 5-9 varful seriei
|
|
78.95, # 10 gap down (open 79.20, refacut mai jos)
|
|
78.75, # 11
|
|
78.60, # 12 sell doji
|
|
78.05, # 13 ruperea
|
|
77.50, 77.80, 77.20, 76.60, 76.90, # 14-18
|
|
76.20, 75.60, 75.90, 75.30, 74.80, # 19-23
|
|
75.10, 74.40, 73.80, 73.20, 72.60, # 24-28
|
|
71.90, # 29 TP 72.00 atins
|
|
]
|
|
assert len(CLOSES) == 30, len(CLOSES)
|
|
|
|
# wick-uri moderate (0.3-0.9); bara 9 urca exact la rezistenta 84.50
|
|
WICKS = [(0.45, 0.40)] * 30
|
|
WICKS[1] = (0.40, 0.45)
|
|
WICKS[2] = (0.50, 0.40)
|
|
WICKS[3] = (0.55, 0.45)
|
|
WICKS[4] = (0.45, 0.55)
|
|
WICKS[5] = (0.50, 0.40)
|
|
WICKS[6] = (0.45, 0.50)
|
|
WICKS[7] = (0.40, 0.45)
|
|
WICKS[8] = (0.35, 0.40)
|
|
WICKS[9] = (0.30, 0.45) # high 84.20 + 0.30 = 84.50 = rezistenta
|
|
WICKS[10] = (0.40, 0.50) # gap: open 79.20, high 79.60, low 78.45
|
|
WICKS[11] = (0.45, 0.40)
|
|
WICKS[12] = (0.35, 0.40) # doji: high 79.10, low 78.20
|
|
WICKS[13] = (0.40, 0.40) # low 77.65, sub intrarea 78.10
|
|
WICKS[29] = (0.50, 0.55)
|
|
assert len(WICKS) == 30, len(WICKS)
|
|
|
|
# panoul de volum: bara 10 (gap-ul) = 200% din media seriei, bara cea mai inalta;
|
|
# suma 3000 => media 100, bara 10 = 200.
|
|
VOLS = [
|
|
88, 82, 90, 100, 86, 108, 104, 120, 96, 100, # 0-9
|
|
200, # 10 RVOL 200%
|
|
130, 100, 150, 102, 88, 108, 96, 84, 100, # 11-19
|
|
90, 78, 98, 86, 74, 92, 82, 104, 76, 88, # 20-29
|
|
]
|
|
assert len(VOLS) == 30, len(VOLS)
|
|
media_vol = sum(VOLS) / len(VOLS)
|
|
assert VOLS[10] == max(VOLS), "bara 10 (gap-ul) nu e cea mai inalta"
|
|
assert abs(VOLS[10] / media_vol - 2.00) < 0.01, VOLS[10] / media_vol
|
|
|
|
cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=79.80)
|
|
|
|
# bara de gap: singura cu open != close-ul barei precedente (open 79.20 explicit)
|
|
cds[10] = Candle(79.20, max(79.20, 78.95) + 0.40, min(79.20, 78.95) - 0.50,
|
|
78.95, vol=VOLS[10])
|
|
|
|
# ---------------------------------------------------------------- assert-urile din spec
|
|
assert abs(cds[12].c - 78.60) < 1e-9, "doji-ul trebuie sa inchida la 78.60"
|
|
assert abs(max(cds[12].o, cds[12].c) - min(cds[12].o, cds[12].c)) <= 0.30, "bara 12 nu e doji"
|
|
assert 72.00 < 78.10 < 78.60 < 82.00 < 84.50, "ordinea nivelurilor short"
|
|
assert cds[13].l <= 78.10, "intrarea short nu e sub low-ul dojii"
|
|
assert cds[13].l < cds[12].l, "intrarea nu sparge low-ul dojii"
|
|
assert cds[10].o < cds[9].c, "bara 10 nu e gap down"
|
|
risc = 82.00 - 78.10 # 3.90
|
|
reward = 78.10 - 72.00 # 6.10
|
|
assert abs(reward / risc - 1.56) < 0.02, "raportul nu e ~1.56R"
|
|
assert cds[-1].c <= 72.00, "TP-ul nu e atins in serie"
|
|
# open = close-ul barei precedente peste tot, mai putin bara de gap (10)
|
|
for i, (a, b) in enumerate(zip(cds, cds[1:])):
|
|
if i + 1 == 10:
|
|
continue
|
|
assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial intre bare"
|
|
assert cds[9].h == 84.50, "varful nu atinge rezistenta 84.50"
|
|
assert min(c.l for c in cds) >= 71.30, "SL-ul de 82.00 nu poate fi sub minimul seriei"
|
|
|
|
# ---------------------------------------------------------------- niveluri, etichete
|
|
levels = [
|
|
Level(84.50, "rezistenta 84.50", GRAY, label_at="right"),
|
|
Level(82.00, "SL 82.00", RED, label_at="right"),
|
|
Level(78.10, "intrare short 78.10", BLUE, label_at="left"),
|
|
Level(72.00, "TP 72.00", GREEN, label_at="left"),
|
|
]
|
|
|
|
marks = [
|
|
Mark(10, "gap down, open 79.20", "#333", "above", dy=-16),
|
|
Mark(12, "sell doji close 78.60", RED, "above", dy=-4),
|
|
Mark(13, "intrare short 78.10", GREEN, "below", dy=26),
|
|
Mark(10, "RVOL 200%", RED, "below", dy=12),
|
|
]
|
|
|
|
title = "HIG — daily (Sell Doji + Bear Reversal, RVOL 200%)"
|
|
caption = ("RVOL 200% si simbolul sunt reale (04_MODUL_3/01:196-199). Toate "
|
|
"nivelurile de preț (79.20 / 78.60 / 78.10 / 82.00 / 72.00) sunt "
|
|
"construite in sumarul 04_MODUL_3/01:293-296, nu date de curs; "
|
|
"structura barelor e construita, la scara.")
|
|
|
|
ch = Chart(
|
|
candles=cds, levels=levels, marks=marks,
|
|
mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
|
|
scale=16.0, gap=15, body_w=9, left=44, width=640, vol_h=70,
|
|
vol_label="volum",
|
|
title="", # titlul se injecteaza sus, ca sa nu se suprapuna
|
|
caption=caption, # cu eticheta nivelului "rezistenta 84.50" (stanga)
|
|
)
|
|
svg = ch.render()
|
|
|
|
# ---------------------------------------------------------------- verificarea de scara
|
|
ys = {p: ch._y(p) for p in (82.00, 78.10, 72.00)}
|
|
px_risc = ys[78.10] - ys[82.00] # y creste in jos, SL-ul e deasupra intrarii
|
|
px_reward = ys[72.00] - ys[78.10]
|
|
assert px_risc > 0 and px_reward > 0
|
|
assert abs(px_risc / px_reward - risc / reward) < 1e-9, "SL/TP la scara gresita"
|
|
|
|
# ---------------------------------------------------------------- titlu injectat
|
|
m_svg = re.search(r"<svg[^>]*>", svg)
|
|
assert m_svg, "tag <svg> lipsa"
|
|
svg = svg.replace(
|
|
m_svg.group(0),
|
|
m_svg.group(0) + f'\n <text x="24" y="16" fill="#333" style="font-weight:bold">{title}</text>',
|
|
1,
|
|
)
|
|
|
|
# ---------------------------------------------------------------- caption pe randuri
|
|
def _wrap(text: str, max_chars: int) -> list[str]:
|
|
lines, cur = [], ""
|
|
for w in text.split():
|
|
if cur and len(cur) + 1 + len(w) > max_chars:
|
|
lines.append(cur)
|
|
cur = w
|
|
else:
|
|
cur = f"{cur} {w}".strip()
|
|
if cur:
|
|
lines.append(cur)
|
|
return lines
|
|
|
|
|
|
cap_lines = _wrap(caption, 70)
|
|
m = re.search(r'<text x="24" y="(\d+)" fill="#555">' + re.escape(caption) + r"</text>\s*</svg>", svg)
|
|
assert m, "caption-ul nu a fost gasit in SVG"
|
|
y0 = int(m.group(1))
|
|
wrapped = "\n ".join(f'<text x="24" y="{y0 + 17 * i}" fill="#555">{ln}</text>'
|
|
for i, ln in enumerate(cap_lines))
|
|
svg = svg[:m.start()] + wrapped + "\n</svg>"
|
|
vm = re.search(r'viewBox="0 0 (\d+) (\d+)"', svg)
|
|
assert vm
|
|
svg = svg.replace(vm.group(0),
|
|
f'viewBox="0 0 {vm.group(1)} {int(vm.group(2)) + 17 * (len(cap_lines) - 1)}"', 1)
|
|
|
|
# ---------------------------------------------------------------- verificare layout
|
|
width, height = int(vm.group(1)), int(vm.group(2)) + 17 * (len(cap_lines) - 1)
|
|
texts = re.findall(r'<text x="([\d.]+)" y="([\d.]+)"([^>]*)>(.*?)</text>', svg)
|
|
assert texts, "niciun text in SVG"
|
|
for x, y, _attrs, t in texts:
|
|
assert 0 <= float(x) <= width, f"text in afara pânzei pe x: {t!r} x={x}"
|
|
assert 0 <= float(y) <= height, f"text in afara pânzei pe y: {t!r} y={y}"
|
|
# nicio pereche de texte nu se suprapune pe ambele axe (12px inaltime,
|
|
# latime estimata 6.5px/caracter; ancorele start/middle/end sunt respectate)
|
|
boxes = []
|
|
for x, y, attrs, t in texts:
|
|
w = 6.5 * len(t)
|
|
if 'text-anchor="middle"' in attrs:
|
|
x0 = float(x) - w / 2
|
|
elif 'text-anchor="end"' in attrs:
|
|
x0 = float(x) - w
|
|
else:
|
|
x0 = float(x)
|
|
boxes.append((x0, float(y), t, w))
|
|
for i in range(len(boxes)):
|
|
for j in range(i + 1, len(boxes)):
|
|
xi, yi, ti, wi = boxes[i]
|
|
xj, yj, tj, wj = boxes[j]
|
|
dx = min(xi + wi, xj + wj) - max(xi, xj)
|
|
dy = abs(yi - yj)
|
|
if dx > 4 and dy < 11:
|
|
raise AssertionError(f"texte suprapuse: {ti!r}@{xi:.0f},{yi:.0f} vs {tj!r}@{xj:.0f},{yj:.0f}")
|
|
|
|
assert "<svg" in svg and "\n\n" not in svg, "linie goala in SVG"
|
|
OUT.write_text(svg)
|
|
print(f"scris {OUT.name}: {len(cds)} candele, doji {cds[12].c:.2f}, "
|
|
f"gap open {cds[10].o:.2f} (RVOL {VOLS[10]/media_vol:.2f}), "
|
|
f"intrare 78.10, SL 82.00 = 3.90, TP 72.00 = 6.10 ({reward/risc:.2f}R), "
|
|
f"ultimul close {cds[-1].c:.2f}")
|