#!/usr/bin/env python3 """Scanerul Buy Doji în TradeStation — ordinea filtrelor și capcanele. Schemă conceptuală (NU grafic de preț): pâlnie de cutii tot mai înguste, de sus în jos; în dreapta fiecărei cutii, notele și capcanele ei (roșu). Pentru `strategii/01_buy_doji.md`, Pasul 1. Spec: `buy_doji_scaner.spec.txt`. Fără cifre de „câte simboluri rămân" — materialul nu le dă. Rulare: .venv/bin/python gen/strategii/buy_doji_scaner.py """ import html import textwrap from pathlib import Path OUT = Path(__file__).resolve().parent / "buy_doji_scaner.svg" W = 640 BLACK, GRAY, RED, BLUE, GREEN = "#000000", "#666666", "#c00", "#1a5fb4", "#188a3e" GREEN_BG, BOX_BG = "#eef7f0", "#eef3fb" BOX_CX = 165 # axa pâlniei (coloana stângă) NOTE_X = 340 # începutul coloanei de note NOTE_CHARS = 44 # wrap pentru note, la 12px BOX_FS, NOTE_FS, LH = 13, 12, 16 WIDTHS = [300, 280, 260, 240, 220, 200] # (linii în cutie, [(tip, text)]); tip: "n" = notă gri, "c" = capcană BOXES = [ (["Univers:", "All Stocks (~13.000)", "S&P 500 · Russell 3000"], [("n", "mai mare = mai mulți candidați, scanare mai lentă")]), (["Trigger: jbtgrshdATM", "BDATM = 0", "(Sell Doji: SDATM = 0)"], [("c", "intervalul nu se aliniază singur cu TF-ul: îl pui de mână"), ("c", "vede doar candela închisă: daily rulat la prânz arată ieri")]), (["Nivel: RSV1ATM", "% of PP ≤ 0.5 (60 min)", "% of PP ≤ 2 (daily)"], [("n", "Pivot Interval: 2 = weekly (pe 60 min), 3 = monthly (pe daily)"), ("c", "RS/SR: 1 la filtrare, 100 doar la afișare")]), (["Lichiditate: Volume Colors", "Average Volume ≥ 500.000", "Average Length = 10"], [("n", "volum daily, indiferent de TF-ul scanerului")]), (["Opțional:", "Since Bear ≥ 8", "Bull Reversal = 1", "RVOL doar Display"], [("c", "pornește pe Bear Reversal: comută pe Bull")]), (["Lista de candidați", "→ o verifici pe grafic", "(pașii 2-6).", "Scanerul nu e undă verde."], []), ] def est_w(s, fs, bold=False): return len(s) * fs * (0.60 if bold else 0.55) def txt(x, y, s, fs, fill=BLACK, bold=False, anchor="start"): return (f'{html.escape(s, quote=False)}') svg, bboxes, rects = [], [], [] # bboxes: (x0, x1, ytop, ybot, text) def put(x, y, s, fs, fill=BLACK, bold=False, anchor="start"): w = est_w(s, fs, bold) x0 = x - w / 2 if anchor == "middle" else x bboxes.append((x0, x0 + w, y - fs * 0.8, y + fs * 0.25, s)) svg.append(txt(x, y, s, fs, fill, bold, anchor)) return x0, x0 + w put(W / 2, 30, "Scanerul Buy Doji în TradeStation", 18, bold=True, anchor="middle") put(W / 2, 52, "ordinea filtrelor și capcanele", 13, GRAY, anchor="middle") y = 76 for i, (lines, notes) in enumerate(BOXES): last = i == len(BOXES) - 1 note_lines = [] for kind, s in notes: pre = "capcană: " if kind == "c" else "" wrapped = textwrap.wrap(pre + s, NOTE_CHARS) note_lines.append([(kind, ln) for j, ln in enumerate(wrapped)]) n_note = sum(len(n) for n in note_lines) + max(0, len(note_lines) - 1) * 0.4 h = max(len(lines), n_note) * LH + 20 w = WIDTHS[i] x = BOX_CX - w / 2 rects.append((x, y, w, h)) stroke, fill = (GREEN, GREEN_BG) if last else (BLUE, BOX_BG) svg.append(f'') ty = y + (h - len(lines) * LH) / 2 + 12 for j, ln in enumerate(lines): x0, x1 = put(BOX_CX, ty + j * LH, ln, BOX_FS, GREEN if last else BLACK, bold=(j == 0), anchor="middle") assert x0 >= x + 6 and x1 <= x + w - 6, f"textul nu încape în cutia {i}: {ln!r}" if note_lines: assert x + w + 24 <= NOTE_X, f"cutia {i} prea aproape de note" ny = y + (h - n_note * LH) / 2 + 12 for group in note_lines: kind0 = group[0][0] cy = ny - 4 # capcana arată spre filtrul pe care îl strică (vârful pe cutie) col = RED if kind0 == "c" else "#bbbbbb" dash = '' if kind0 == "c" else ' stroke-dasharray="3 3"' svg.append(f'') if kind0 == "c": svg.append(f'') for kind, ln in group: put(NOTE_X, ny, ln, NOTE_FS, RED if kind == "c" else GRAY) ny += LH ny += 0.4 * LH y += h if not last: svg.append(f'') y += 18 H = int(y + 20) # ------------------------------------------------------------------ verificare assert all(a > b for a, b in zip(WIDTHS, WIDTHS[1:])), "lățimile nu scad strict" assert all(r1[2] > r2[2] for r1, r2 in zip(rects, rects[1:])) for x0, x1, t, b, s in bboxes: assert 0 <= x0 and x1 <= W and 0 <= t and b <= H, f"text în afara viewBox: {s!r}" for a in range(len(bboxes)): for b in range(a + 1, len(bboxes)): p, q = bboxes[a], bboxes[b] assert not (p[0] < q[1] and q[0] < p[1] and p[2] < q[3] and q[2] < p[3]), \ f"texte suprapuse: {p[4]!r} / {q[4]!r}" for x0, x1, t, b, s in bboxes: # notele nu intră în cutii if x0 >= NOTE_X - 1: assert all(x0 > rx + rw for rx, _, rw, _ in rects), f"notă peste cutie: {s!r}" out = "\n".join([ f'', f'', *svg, ""]) + "\n" assert ".md:" not in out and "Surse" not in out, "trimiteri la surse în SVG" assert "\n\n" not in out and not any(not ln.strip() for ln in out.splitlines()), "linie goală" OUT.write_text(out, encoding="utf-8") print(f"scris {OUT} ({W}x{H}), verificare OK")