"""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"]*>", svg) assert m_svg, "tag lipsa" svg = svg.replace( m_svg.group(0), m_svg.group(0) + f'\n {title}', 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'' + re.escape(caption) + r"\s*", svg) assert m, "caption-ul nu a fost gasit in SVG" y0 = int(m.group(1)) wrapped = "\n ".join(f'{ln}' for i, ln in enumerate(cap_lines)) svg = svg[:m.start()] + wrapped + "\n" 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 "