#!/usr/bin/env python3 """Buy Doji — ieșirea: trailing stop pe gap-uri și limita de 8 candele. Citește `gen/strategii/buy_doji_iesire.spec.txt` și scrie `gen/strategii/buy_doji_iesire.svg`: două panouri stivuite. A — seria din buy_doji_transe (barele 0-18) continuată cu 19-23. Barele 19 și 21 deschid cu GAP (excepție cerută de spec): regula de trail cere o bară cu low peste high-ul precedentei, imposibil dacă open = close anterior. De aceea barele 15-23 sunt construite explicit, nu cu `from_closes`. B — doji 50.10 / 49.20, intrare 50.40, apoi 8 candele fără TP și fără SL. Barele de după intrare, în ambele panouri, sunt CONSTRUITE. Sursa regulilor: summaries/03_MODUL_2/13_strategia_3_byh.md:196-213. Rulare: python3 gen/strategii/buy_doji_iesire.py """ 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 OUT = Path(__file__).with_suffix(".svg") W = 660 VGAP = 10 GREEN, RED, ORANGE, BLUE, GREY = "#188a3e", "#c00", "#b06000", "#1a5fb4", "#555" # --- PANOUL A ---------------------------------------------------------------- ENTRY_A, SL_A, TP1_A = 53.70, 51.55, 55.85 R_A = round(ENTRY_A - SL_A, 2) # 2.15 a = from_closes([60.40, 59.60, 59.90, 58.80, 58.10, 58.40, 57.20, 56.30, 56.60, 55.30, 54.20, 52.40], wick=0.15, start=61.00) a += [Candle(*ohlc) for ohlc in [ (52.40, 53.00, 51.60, 52.30), # 12 doji (52.30, 52.85, 52.10, 52.60), # 13 +1 (52.60, 53.85, 52.45, 53.70), # 14 confirmare = intrare (53.70, 53.80, 53.10, 53.45), # 15 (53.45, 54.20, 53.35, 54.10), # 16 (54.10, 54.85, 54.00, 54.70), # 17 (54.70, 56.00, 54.60, 55.90), # 18 atinge TP1 (56.30, 56.95, 56.20, 56.80), # 19 GAP (56.80, 57.35, 56.60, 57.20), # 20 (57.60, 58.10, 57.45, 57.90), # 21 GAP (57.90, 58.00, 57.50, 57.60), # 22 (57.60, 57.70, 57.10, 57.20), # 23 close sub trail ]] assert len(a) == 24 for c in a: assert c.l <= min(c.o, c.c) and c.h >= max(c.o, c.c), "h/l nu cuprind o/c" # A1. gap-urile reale assert a[19].l > a[18].h and a[21].l > a[20].h, "barele de gap nu au low > high anterior" # A2. open = close anterior, în afară de 19 și 21 (și 0-14, ca în transe) for i in range(1, 24): if i not in (19, 21): assert abs(a[i].o - a[i - 1].c) < 1e-9, f"A: open[{i}] != close[{i-1}]" # regula de trail, calculată pe serie: primul gap după intrare stabilește # trail-ul, fiecare gap următor îl urcă; ieși la primul close sub el trail, steps, exit_i = None, [], None for i in range(15, 24): if trail is not None and a[i].c < trail: exit_i = i break if a[i].l > a[i - 1].h: trail = a[i].l steps.append((i, trail)) assert [(i, round(t, 2)) for i, t in steps] == [(19, 56.20), (21, 57.45)], steps # A3. assert a[22].c >= 57.45 and a[23].c < 57.45 and exit_i == 23, exit_i EXIT_A = a[23].c # A4. assert R_A == 2.15 and round((EXIT_A - ENTRY_A) / R_A, 2) == 1.63 # A5. TP1 = 1R, atins prima oară la bara 18 assert round(ENTRY_A + R_A, 2) == TP1_A assert next(i for i in range(15, 24) if a[i].h >= TP1_A) == 18 assert abs(a[14].c - ENTRY_A) < 1e-9 ch_a = Chart( candles=a, levels=[Level(ENTRY_A, "intrare 53.70", GREEN), Level(TP1_A, "TP1 55.85 — tranșa 1: +1R", GREEN, dash="2 3")], scale=40, gap=16, body_w=9, left=44, width=W, title="A — trailing stop pe gap-uri (seria de la intrarea în tranșe)", caption="a treia treime: o lași mai departe (break-even / trend)", ) # --- PANOUL B ---------------------------------------------------------------- ENTRY_B, SL_B, TP1_B = 50.40, 49.20, 51.60 R_B = round(ENTRY_B - SL_B, 2) # 1.20 b = from_closes([52.20, 51.40, 50.60, 49.65], wick=0.15, start=53.00) b += [Candle(*ohlc) for ohlc in [ (49.65, 50.10, 49.20, 49.60), # 4 doji (49.60, 50.05, 49.45, 49.90), # 5 +1 (49.90, 50.55, 49.75, 50.40), # 6 confirmare = intrare ]] b += from_closes([50.60, 50.30, 50.90, 50.50, 51.10, 50.80, 51.20, 50.70], wick=0.15, start=b[-1].c) assert len(b) == 15 # B1. for i in range(1, 15): assert abs(b[i].o - b[i - 1].c) < 1e-9, f"B: open[{i}] != close[{i-1}]" d = b[4] assert abs(d.o - d.c) <= 0.10 * (d.h - d.l) and (d.h, d.l) == (50.10, 49.20) assert b[6].c > d.h and abs(b[6].c - ENTRY_B) < 1e-9 # B2. 8 candele fără TP și fără SL after = b[7:15] assert all(c.h < TP1_B and c.l > SL_B for c in after) assert round(max(c.h for c in after), 2) == 51.35 and round(min(c.l for c in after), 2) == 50.15 assert round(ENTRY_B + R_B, 2) == TP1_B # B3. EXIT_B = b[14].c assert round((EXIT_B - ENTRY_B) / R_B, 2) == 0.25 for k, c in enumerate(after, 1): c.tag = str(k) ch_b = Chart( candles=b, levels=[Level(TP1_B, "TP1 51.60", GREEN, dash="2 3"), Level(ENTRY_B, "intrare 50.40", GREEN), Level(SL_B, "SL 49.20", RED)], marks=[Mark(4, "doji", ORANGE, "below")], scale=48, gap=16, body_w=9, left=44, width=W, title="B — limita de 8 candele", ) # --- etichete plasate de mână: Mark centrează textul, iar aici textele lungi # ar cădea peste candelele vecine. Le ancorez lângă bară, pe partea liberă. def label(ch, i, price, text, color, anchor="start", dx=0): x = ch._cx(i) + dx return (f'{text}') def panel(ch, extra): svg = ch.render() # render() fixează scala (_pmax, top) h = float(re.search(r'viewBox="0 0 \d+ ([\d.]+)"', svg).group(1)) body = svg[svg.index(">") + 1:svg.rindex("")].strip() return body + "".join("\n " + e for e in extra(ch)), h def trail_a(ch): x19, x21, x23 = ch._cx(19) - 7, ch._cx(21) - 8, ch._cx(23) + 7 y1, y2 = ch._y(steps[0][1]), ch._y(steps[1][1]) # golul dintre high-ul barei precedente și low-ul barei de gap, hașurat: # la 0.10-0.20 de preț are doar câțiva pixeli și se pierde sub linia de trail gaps = [f'' for i, _ in steps] return gaps + [ f'', label(ch, 18, 56.25, "TP1: ieși cu 1/3", GREEN, "end", 4), label(ch, 19, 55.35, "gap: low > high anterior → trail 56.20", ORANGE, "start", -4), label(ch, 21, 58.35, "gap → trail 57.45", ORANGE, "middle"), label(ch, 23, 57.30, "close sub trail: ieși cu 1/3", RED, "start", 14), label(ch, 23, 56.88, f"la 57.20 = +{(EXIT_A - ENTRY_A) / R_A:.2f}R", RED, "start", 14), ] def mark_b(ch): return [ label(ch, 14, 51.05, "a 8-a candelă: nici TP, nici SL", GREY, "start", 10), label(ch, 14, 50.75, f"→ ieși la 50.70 = +{(EXIT_B - ENTRY_B) / R_B:.2f}R", GREY, "start", 10), ] # panourile se stivuiesc prin `top` (nu prin ): audit_diagrams # citește coordonatele brute și ar vedea panourile suprapuse. T = 26 ch_a.top += T body_a, h_a = panel(ch_a, trail_a) ch_b.top += h_a + VGAP body_b, H_B = panel(ch_b, mark_b) TITLE = "Ieșirea: trailing stop pe gap-uri și limita de 8 candele — exemple construite" CAP = "Barele de după intrare sunt construite. Regulile: 13_strategia_3_byh.md:196-213." cap_y = H_B + 12 H = cap_y + 10 svg = "\n ".join([ f'', f'{TITLE}', body_a, body_b, f'{CAP}', ]) + "\n" assert "\n\n" not in svg # --- etichete în panză și fără suprapuneri (lățimi aproximate) --------------- TEXT = re.compile(r']*)>(.*?)', re.S) def texts(body, y_off, tag): for m in TEXT.finditer(body): x, y, attrs = float(m.group(1)), float(m.group(2)) + y_off, m.group(3) s = m.group(4).replace(">", ">") wpx = len(s) * 12 * (0.62 if "bold" in attrs else 0.52) anchor = re.search(r'text-anchor="(\w+)"', attrs) anchor = anchor.group(1) if anchor else "start" x0 = x - wpx if anchor == "end" else (x - wpx / 2 if anchor == "middle" else x) yield x0, x0 + wpx, y, s, tag labels = list(texts(body_a, 0, "A")) + list(texts(body_b, 0, "B")) for x0, x1, y, s, tag in labels: assert x0 >= -2 and x1 <= W + 2 and 0 <= y <= H, f"[{tag}] text iese din panză: {s!r}" for i, (l1, r1, y1, s1, t1) in enumerate(labels): for l2, r2, y2, s2, t2 in labels[i + 1:]: assert not (t1 == t2 and abs(y1 - y2) < 12 and l1 < r2 - 4 and l2 < r1 - 4), \ f"[{t1}] texte suprapuse: {s1!r} / {s2!r}" OUT.write_text(svg, encoding="utf-8") print(f"A: trail {steps}, ieșire bara {exit_i} la {EXIT_A:.2f} = " f"+{(EXIT_A - ENTRY_A) / R_A:.2f}R; B: ieșire la {EXIT_B:.2f} = " f"+{(EXIT_B - ENTRY_B) / R_B:.2f}R") print(f"scris {OUT} ({W}x{H:.0f})")