Files
atm-curs-dl/gen/strategii/selectia_exemplu.py
Claude Agent f1033a916c selectia: diagramele refacute dupa sablon (textul urmeaza)
Agentul a atins pragul de context dupa diagrame; capitolul 04_selectia
e inca in formatul vechi si se reia separat.

Ce s-a schimbat: ansamblul nu mai arata ULTA mergand la TP2 — ULTA pica
chiar filtrul de selectie (RVOL 0.61 si 5.01 % de suport), deci nu putea
fi exemplul-vitrina; acum arata doar ce citeste selectia, cu granita
"aici se termina selectia" marcata. Funnel-ul trece de la 8 trepte la 6,
aliniat cu pasii nivelului 2, si fiecare treapta spune de ce. Cadranele
primesc mini-histograme RS, ca sa se recunoasca dintr-un desen, nu
dintr-o propozitie. Trimiterile fisier:linie sunt scoase de pe desene.

Noi: univers (pragurile de pret si volum), sectoare (dominantul din cele
11), proximitate (banda de 3 % peste suportul monthly, cu observatia ca
nu e pragul de 1-2 % din Buy Doji — masoara altceva).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-20 12:43:12 +00:00

230 lines
10 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, Arrow, Zone, 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"),
]
zones = [
Zone(10, 13, "ce l-a scos în față din cele 16 rezultate", RED, p_top=80.4),
]
arrows = [
Arrow(10, f"gap down: deschide la {cds[10].o:.2f}, mult sub\n"
f"închiderea de ieri — vânzare, nu pauză",
where="above", color=RED, dx=-160, dy=32),
Arrow(12, "sell doji close 78.60: vânzătorii și cumpărătorii la egalitate,\nîncă nu e semnal",
where="below", price=77.6, color="#333", dx=-120, dy=70),
Arrow(13, "aici ar intra strategia de intrare, nu selecția:\nînchidere sub low-ul doji-ului",
where="right", price=78.10, color=GREEN, dx=40, dy=110),
]
title = "HIG — daily (Sell Doji + Bear Reversal, RVOL 200%)"
caption = ("Ale cursului: simbolul HIG, semnalul Sell Doji + Bear Reversal și RVOL-ul de 200 %, "
"cel mai mare din cele 16 rezultate ale scanerului. Nivelurile de preț "
"(79.20 / 78.60 / 78.10 / 82.00 / 72.00) și structura barelor sunt construite, la scară: "
"cursul nu dă cifre pentru HIG.")
ch = Chart(
candles=cds, levels=levels, zones=zones, arrows=arrows,
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=title,
caption=caption,
)
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"
# ---------------------------------------------------------------- RVOL, pe panoul de volum
# RVOL e o afirmație despre volum, deci eticheta stă pe panoul de volum, nu în
# cel de preț: linia întreruptă e media seriei, iar bara 10 e de două ori cât ea.
m_vb = re.search(r'<line x1="24" y1="([\d.]+)" x2="[\d.]+" y2="[\d.]+" stroke="#888"/>', svg)
assert m_vb, "nu gasesc linia de baza a panoului de volum"
vol_base = float(m_vb.group(1))
vmax = max(VOLS)
y_media = vol_base - media_vol / vmax * 70.0 # vol_h = 70
y_bara = vol_base - VOLS[10] / vmax * 70.0
assert abs((vol_base - y_bara) / (vol_base - y_media) - 2.0) < 1e-9, \
"bara de RVOL nu e de doua ori media, desenata"
m_w = re.search(r'viewBox="0 0 (\d+) ', svg)
assert m_w
w_svg = int(m_w.group(1))
vol_note = (f'<line x1="24" y1="{y_media:.1f}" x2="{w_svg - 14}" y2="{y_media:.1f}" '
f'stroke="{GRAY}" stroke-width="1.2" stroke-dasharray="4 3"/>'
f'<text x="{w_svg - 16}" y="{y_media - 4:.1f}" fill="{GRAY}" text-anchor="end">'
f'media volumului pe serie</text>'
f'<text x="92" y="{y_bara - 5:.1f}" fill="{RED}" style="font-weight:bold">'
f'ziua gap-ului: de {VOLS[10] / media_vol:.0f} ori media (RVOL 200 %) — '
f'se vinde dublu față de o zi normală</text>')
svg = svg.replace(m_vb.group(0), m_vb.group(0) + vol_note, 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}")