"""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="left"),
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""
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']*)>(.*?)', 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 "