Userul a respins mostra de format tutorial: prea lunga, prea multe trimiteri in text, termeni tehnici nedefiniti, prea multe cifre fara desen. Cerintele lui sunt acum contract in STYLE.md (FORMAT TUTORIAL v2). Textul: 8430 -> 4190 de cuvinte. Nivel 1 (din avion, cu mini-glosar) -> nivel 2 (6 pasi mecanici, o diagrama fiecare) -> oglinda short -> nivel 3 (rafinari). Trimiterile fisier:linie ies din corp si se aduna la final, cu legenda de prescurtari. Doua tabele in tot documentul. Toate cele 14 diagrame refacute: zone dreptunghiulare de evidentiere si sageti care spun si de ce, nu doar ce. svg_candles primeste Zone si Arrow; render() nu mai muta obiectul la iesire. Trei corecturi de continut: - un singur trade prin toata procedura (doji 50.10/49.25, intrare 50.40, stop 49.20, 1R 1.20, TP1 51.60); seria e acum o functie comuna, importata de stop/transe/iesire - short: stopul urca la 100.15, ca sa fie deasupra maximului cum cere regula oglindita; cifra cursului ramane in text ca observatie - LABU: trei minime dar doua atingeri ale pivotului 86.00, ceea ce impaca "triple bottom" cu "al doilea test", ambele din transcriere Reparat si bug-ul vechi din buy_doji_sell.svg: titlul peste eticheta dojiului. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
142 lines
6.3 KiB
Python
142 lines
6.3 KiB
Python
#!/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'<text x="{x:g}" y="{y:g}" font-family="system-ui, sans-serif" '
|
|
f'font-size="{fs}" font-weight="{700 if bold else 400}" fill="{fill}" '
|
|
f'text-anchor="{anchor}">{html.escape(s, quote=False)}</text>')
|
|
|
|
|
|
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'<rect x="{x:g}" y="{y:g}" width="{w}" height="{h:g}" rx="6" '
|
|
f'fill="{fill}" stroke="{stroke}" stroke-width="{2 if last else 1.5}"/>')
|
|
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'<line x1="{NOTE_X - 10}" y1="{cy:g}" x2="{x + w + 12:g}" y2="{cy:g}" '
|
|
f'stroke="{col}" stroke-width="{1.6 if kind0 == "c" else 1}"{dash}/>')
|
|
if kind0 == "c":
|
|
svg.append(f'<path d="M {x + w + 4:g} {cy:g} L {x + w + 13:g} {cy - 4:g} '
|
|
f'L {x + w + 13:g} {cy + 4:g} Z" fill="{RED}"/>')
|
|
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'<path d="M {BOX_CX - 8} {y + 4:g} L {BOX_CX + 8} {y + 4:g} '
|
|
f'L {BOX_CX} {y + 14:g} Z" fill="{BLUE}"/>')
|
|
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'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" '
|
|
f'height="{H}" role="img" aria-label="Scanerul Buy Doji în TradeStation — '
|
|
f'ordinea filtrelor și capcanele">',
|
|
f'<rect x="0" y="0" width="{W}" height="{H}" fill="#ffffff"/>',
|
|
*svg, "</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")
|