#!/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'"]) + "\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")