strategii: diagramele puse in capitole

36 din 37 de diagrame substituite in .md. substituie.py accepta acum si
cheia cu slug inclus (unii agenti au scris mp2_exemplu, altii exemplu) si
marcajul indentat. A ramas mdsrt_aplus: agentul a raportat OK fara sa scrie
niciun fisier — exact capcana din CLAUDE.md, un raport de agent nu e dovada.
Relansat separat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
This commit is contained in:
Claude Agent
2026-09-18 19:34:11 +00:00
parent 01599d2e9d
commit ccfc4a7f7f
25 changed files with 5821 additions and 60 deletions

View File

@@ -132,7 +132,7 @@ PW = 560
ch_a = Chart(
candles=cds_a,
levels=[Level(SUP_A, f"suport quarterly {SUP_A:.2f}", GRAY, label_at="left"),
Level(SL_A, f"SL {SL_A:.2f}", RED, label_at="right"),
Level(SL_A, f"SL {SL_A:.2f} (peste suport)", RED, label_at="right"),
Level(entry_a, f"intrare {entry_a:.2f}", GREEN, label_at="left"),
Level(NECK_A, f"neckline {NECK_A:.2f}", ORANGE, label_at="right"),
Level(TP1_A, f"TP1 {TP1_A:.2f} = 1:1", BLUE, label_at="right")],
@@ -143,7 +143,7 @@ ch_a = Chart(
scale=7.5, gap=15, body_w=9, left=44, width=PW, vol_h=64,
vol_label="volum", title="A+ — crossing la confluență (SPY, aprilie)",
trend="down",
caption="Confluență: suport 554.00 + double bottom 554.45 + neckline 578.00",
caption="Confluență: 554.00 + double bottom + neckline 578.00 · 1R 22.60",
)
ch_m = Chart(
candles=cds_m,
@@ -258,7 +258,8 @@ for i in range(len(all_txt)):
raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
# etichetele cerute de spec există în desen
for cheie in ("suport quarterly 554.00", f"SL {SL_A:.2f}", "intrare 577.00",
for cheie in ("suport quarterly 554.00", f"SL {SL_A:.2f} (peste suport)", "intrare 577.00",
"crossing", "1R 22.60",
"neckline 578.00", "TP1 599.60 = 1:1",
f"umăr stâng {ls_a:.0f}", f"head {head_a:.0f}", f"umăr drept {rs_a:.0f}",
"A+ — crossing la confluență (SPY, aprilie)",

View File

@@ -3,7 +3,7 @@
<line x1="24" y1="454.0" x2="546" y2="454.0" stroke="#777" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="30" y="448.0" fill="#777">suport quarterly 554.00</text>
<line x1="24" y1="451.0" x2="546" y2="451.0" stroke="#c00" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="532" y="445.0" fill="#c00" text-anchor="end">SL 554.40</text>
<text x="532" y="445.0" fill="#c00" text-anchor="end">SL 554.40 (peste suport)</text>
<line x1="24" y1="281.5" x2="546" y2="281.5" stroke="#188a3e" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="30" y="275.5" fill="#188a3e">intrare 577.00</text>
<line x1="24" y1="274.0" x2="546" y2="274.0" stroke="#b06000" stroke-width="1.5" stroke-dasharray="4 3"/>
@@ -117,7 +117,7 @@
<text x="299.0" y="468.0" fill="#c00" text-anchor="middle" style="font-weight:bold">head 555 / double bottom</text>
<text x="344.0" y="335.0" fill="#777" text-anchor="middle" style="font-weight:bold">umăr drept 568</text>
<text x="374.0" y="263.6" fill="#188a3e" text-anchor="middle" style="font-weight:bold">crossing</text>
<text x="24" y="574" fill="#555">Confluență: suport 554.00 + double bottom 554.45 + neckline 578.00</text>
<text x="24" y="574" fill="#555">Confluență: 554.00 + double bottom + neckline 578.00 · 1R 22.60</text>
</g><g transform="translate(592,38)">
<text x="24" y="48" fill="#333" style="font-weight:bold">Mediocru — crossing în mijlocul range-ului (construit)</text>
<line x1="24" y1="56.0" x2="546" y2="56.0" stroke="#777" stroke-width="1.5" stroke-dasharray="4 3"/>

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -117,7 +117,7 @@ def render_pivot_panel(cds, pivots, title) -> tuple[str, float, Chart]:
"""Randază un panou cu pivot weekly (Level) + pivot daily (polilinie injectată)."""
ch = Chart(candles=cds,
levels=[Level(WEEKLY, f"pivot weekly (cel MARE) {WEEKLY:.2f}",
BLUE, dash="", label_at="left")],
BLUE, dash="", label_at="right")],
marks=[],
scale=SCALE, gap=15, body_w=9, left=44, width=W,
title=title)
@@ -145,26 +145,30 @@ circle = (f'<circle cx="{xc:.1f}" cy="{yc:.1f}" r="13" fill="none" '
f'stroke="{GREEN}" stroke-width="2"/>')
cross_txt = (f'<text x="{xc + 16:.1f}" y="{ch_da._y(PDA[cross_da - 1]) + 18:.1f}" '
f'fill="{GREEN}" style="font-weight:bold">crossing: 61.70 → 62.20</text>')
mark_txt = (f'<text x="{ch_da._cx(trigger):.1f}" y="{ch_da._y(cds_da[trigger].h) - 10:.1f}" '
f'fill="{GREEN}" text-anchor="middle" style="font-weight:bold">'
f'intrare: trigger (prima candelă verde)</text>')
mark_txt = (f'<text x="{ch_da._cx(trigger) + 4:.1f}" y="{ch_da._y(cds_da[trigger].h) - 10:.1f}" '
f'fill="{GREEN}" text-anchor="end" style="font-weight:bold">'
f'intrare: trigger</text>')
i = body_da.rindex("</text>") + len("</text>") # după candele/marks, ca să fie vizibile
body_da = body_da[:i] + "\n " + circle + "\n " + cross_txt + "\n " + mark_txt + body_da[i:]
# panoul NU: eticheta din spec pe semnalele ulterioare
body_nu, h_nu, ch_nu = render_pivot_panel(
cds_nu, PNU, "NU — continuare/buy the dip (+1)")
body_nu = body_nu.rstrip() + (f'\n <text x="{ch_nu._cx(13):.1f}" y="{h_nu - 12:.1f}" '
f'fill="{GRAY}" text-anchor="middle" style="font-weight:bold">'
f'semnale ulterioare = BUY THE DIP, nu crossing</text>')
body_nu = body_nu.rstrip() + (
f'\n <text x="24" y="68.0" fill="{GRAY}">'
f'fără crossing nou; nu a mai căzut sub pivotul mare</text>'
f'\n <text x="{ch_nu._cx(13):.1f}" y="{h_nu - 12:.1f}" '
f'fill="{GRAY}" text-anchor="middle" style="font-weight:bold">'
f'semnale ulterioare = BUY THE DIP, nu crossing</text>')
# ------------------------------------------------- lipirea celor două panouri
CAPS = [
"Pivot weekly 62.00 = linia orizontală (pivotul MARE).",
"Pivot daily (cel MIC) se recalculează zilnic și sare pe nivel nou.",
"Crossing = pivotul mic traversează 62.00 în sus.",
"Fără re-încrucișare, semnalele ulterioare sunt buy the dip,",
"nu crossing.",
"Trigger = prima candelă verde după crossing,",
"care închide peste pivotul daily.",
"Crossing = pivotul mic traversează 62.00 în sus; fără re-încrucișare,",
"semnalele ulterioare sunt buy the dip, nu crossing.",
]
cap_top = h_da + VGAP + h_nu + 16
H = cap_top + 17 * len(CAPS) + 10

View File

@@ -1,13 +1,13 @@
<svg viewBox="0 0 480 671" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;font:12px sans-serif">
<svg viewBox="0 0 480 688" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;font:12px sans-serif">
<g transform="translate(0,0)">
<polyline points="44.0,182.3 59.0,182.3 74.0,182.3 89.0,182.3 104.0,182.3 119.0,182.3 134.0,182.3 149.0,182.3 164.0,182.3 179.0,182.3 194.0,182.3 194.0,134.8 209.0,134.8 224.0,134.8 239.0,134.8 254.0,134.8 269.0,134.8 284.0,134.8 299.0,134.8 314.0,134.8 329.0,134.8 344.0,134.8 359.0,134.8" fill="none" stroke="#b06000" stroke-width="1.8"/>
<text x="470" y="128.8" fill="#b06000" text-anchor="end" style="font-weight:bold">pivot daily (cel MIC)</text>
<text x="24" y="48" fill="#333" style="font-weight:bold">DA — crossing (+2)</text>
<line x1="24" y1="153.9" x2="466" y2="153.9" stroke="#1a5fb4" stroke-width="1.5" stroke-dasharray=""/>
<text x="30" y="147.9" fill="#1a5fb4">pivot weekly (cel MARE) 62.00</text>
<text x="452" y="147.9" fill="#1a5fb4" text-anchor="end">pivot weekly (cel MARE) 62.00</text>
<circle cx="194.0" cy="158.6" r="13" fill="none" stroke="#188a3e" stroke-width="2"/>
<text x="210.0" y="200.3" fill="#188a3e" style="font-weight:bold">crossing: 61.70 → 62.20</text>
<text x="209.0" y="85.0" fill="#188a3e" text-anchor="middle" style="font-weight:bold">intrare: trigger (prima candelă verde)</text>
<text x="213.0" y="85.0" fill="#188a3e" text-anchor="end" style="font-weight:bold">intrare: trigger</text>
<line x1="44.0" y1="201.4" x2="44.0" y2="229.9" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="39.5" y="209.0" width="9" height="11.4" fill="#c00"/>
<line x1="59.0" y1="212.8" x2="59.0" y2="244.1" stroke="#1a1a1a" stroke-width="1.2"/>
@@ -58,7 +58,7 @@
<text x="470" y="143.1" fill="#b06000" text-anchor="end" style="font-weight:bold">pivot daily (cel MIC)</text>
<text x="24" y="48" fill="#333" style="font-weight:bold">NU — continuare/buy the dip (+1)</text>
<line x1="24" y1="168.1" x2="466" y2="168.1" stroke="#1a5fb4" stroke-width="1.5" stroke-dasharray=""/>
<text x="30" y="162.1" fill="#1a5fb4">pivot weekly (cel MARE) 62.00</text>
<text x="452" y="162.1" fill="#1a5fb4" text-anchor="end">pivot weekly (cel MARE) 62.00</text>
<line x1="44.0" y1="191.9" x2="44.0" y2="220.4" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="39.5" y="199.5" width="9" height="11.4" fill="#c00"/>
<line x1="59.0" y1="189.0" x2="59.0" y2="220.4" stroke="#1a1a1a" stroke-width="1.2"/>
@@ -103,10 +103,12 @@
<rect x="339.5" y="63.6" width="9" height="14.2" fill="#188a3e"/>
<line x1="359.0" y1="56.0" x2="359.0" y2="77.9" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="354.5" y="63.6" width="9" height="4.8" fill="#c00"/>
<text x="24" y="68.0" fill="#777">fără crossing nou; nu a mai căzut sub pivotul mare</text>
<text x="239.0" y="234.0" fill="#777" text-anchor="middle" style="font-weight:bold">semnale ulterioare = BUY THE DIP, nu crossing</text></g>
<text x="24" y="576" fill="#555">Pivot weekly 62.00 = linia orizontală (pivotul MARE).</text>
<text x="24" y="593" fill="#555">Pivot daily (cel MIC) se recalculează zilnic și sare pe nivel nou.</text>
<text x="24" y="610" fill="#555">Crossing = pivotul mic traversează 62.00 în sus.</text>
<text x="24" y="627" fill="#555">Fără re-încrucișare, semnalele ulterioare sunt buy the dip,</text>
<text x="24" y="644" fill="#555">nu crossing.</text>
<text x="24" y="610" fill="#555">Trigger = prima candelă verde după crossing,</text>
<text x="24" y="627" fill="#555">care închide peste pivotul daily.</text>
<text x="24" y="644" fill="#555">Crossing = pivotul mic traversează 62.00 în sus; fără re-încrucișare,</text>
<text x="24" y="661" fill="#555">semnalele ulterioare sunt buy the dip, nu crossing.</text>
</svg>

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,315 @@
#!/usr/bin/env python3
"""Foaie de Excel stilizată: pivoții clasici calculați din OHLC, pas cu pas,
și comparația PP zilnic vs. PP săptămânal (crossing).
Pentru `strategii/02_mp2.md`, Transfer — Pe BVB, subsecțiunea „Pivoții în
Excel". Spec: `gen/strategii/mp2_pivoti_excel.spec.txt`.
Nu e grafic de preț (TIP: CONCEPT) — SVG scris de mână, doar stdlib; nu se
folosește svg_candles / pchart. Nu sunt candele, e un tabel.
Ce arată:
- titlul „Pivoții clasici în Excel — formula, pas cu pas";
- un tabel cu coloanele Perioada | High | Low | Close | PP | R1 | S1 | R2 | S2,
cu literele de coloană (A..I) și numerele de rând Excel (1..3), ca formulele
=(B2+C2+D2)/3 etc. să aibă sens;
- două rânduri de date: „Săptămâna încheiată (PPW)" și „Ziua încheiată (PPD)",
fiecare celulă de calcul arătând formula ȘI rezultatul;
- sub tabel, rândul de comparație evidențiat:
„PPD 21.57 > PPW 21.37 → CROSSING BULL (long)";
- legenda că cifrele sunt un exemplu construit, pe TLV, nu date reale.
Valorile și formulele vin din spec (CIFRE ȘI FORMULE); toate assert-urile
cerute de spec sunt scrise ca assert reale mai jos.
Rulare: python3 gen/strategii/mp2_pivoti_excel.py -> mp2_pivoti_excel.svg
"""
from pathlib import Path
from xml.sax.saxutils import escape, unescape
OUT = Path(__file__).with_suffix(".svg")
W, H = 700, 444
BLACK, GRAY, BLUE, GREEN, RED, ORANGE, WHITE = (
"#000000", "#777777", "#1a5fb4", "#188a3e", "#c00", "#b06000", "#ffffff")
BORDER = "#333333"
GRID = "#c9d2dc"
HDR_BG = "#eef1f5"
ALT_BG = "#fafbfc"
# ---------------------------------------------------------------- date (spec)
# Săptămâna încheiată: H=22.00, L=20.50, C=21.60 (row 2)
# Ziua încheiată: H=21.90, L=21.00, C=21.80 (row 3)
WH = (22.00, 20.50, 21.60)
DH = (21.90, 21.00, 21.80)
PPW = (WH[0] + WH[1] + WH[2]) / 3
PPD = (DH[0] + DH[1] + DH[2]) / 3
# derivate standard: R1=2*PP-Low, S1=2*PP-High, R2=PP+(High-Low), S2=PP-(High-Low)
W_R1, W_S1 = 2 * PPW - WH[1], 2 * PPW - WH[0]
W_R2, W_S2 = PPW + (WH[0] - WH[1]), PPW - (WH[0] - WH[1])
D_R1, D_S1 = 2 * PPD - DH[1], 2 * PPD - DH[0]
D_R2, D_S2 = PPD + (DH[0] - DH[1]), PPD - (DH[0] - DH[1])
# ---------------------------------------------------------------- asserturi spec
assert abs((22.00 + 20.50 + 21.60) / 3 - 21.3667) < 0.001, "PPW din formulă"
assert abs((21.90 + 21.00 + 21.80) / 3 - 21.5667) < 0.001, "PPD din formulă"
assert abs(PPW - 21.3667) < 0.001 and abs(PPD - 21.5667) < 0.001
assert PPD > PPW, "PPD trebuie deasupra PPW pentru crossing bull"
# R1/S1/R2/S2 ies din formulă, nu din cap (daily: 2*21.5667-21.00=22.1333 etc.)
assert abs(D_R1 - (2 * 21.5667 - 21.00)) < 0.001, "daily R1"
assert abs(D_S1 - (2 * 21.5667 - 21.90)) < 0.001, "daily S1"
assert abs(D_R2 - (21.5667 + (21.90 - 21.00))) < 0.001, "daily R2"
assert abs(D_S2 - (21.5667 - (21.90 - 21.00))) < 0.001, "daily S2"
# ATENȚIE: spec, linia 29, listează daily R2 = 22.5667. Formula dată chiar de
# spec (R2 = PP + (High-Low), linia 21) și datele ei (H=21.90, L=21.00)
# dau 22.4667: 21.5667 + 0.90. S2 din spec (20.6667) confirmă H-L = 0.90.
# Assert-ul din spec („R2 iese din formulă, nu din cap") decide: desenăm 22.47.
assert abs(D_R2 - 22.4667) < 0.001, "daily R2 trebuie derivat din formulă"
assert abs(W_R1 - (2 * 21.3667 - 20.50)) < 0.001, "weekly R1"
assert abs(W_S1 - (2 * 21.3667 - 22.00)) < 0.001, "weekly S1"
assert abs(W_R2 - (21.3667 + (22.00 - 20.50))) < 0.001, "weekly R2"
assert abs(W_S2 - (21.3667 - (22.00 - 20.50))) < 0.001, "weekly S2"
# ---------------------------------------------------------------- geometrie
LEFT, RIGHT = 8.0, 692.0
GUT_X, GUT_W = LEFT, 20.0
COLS = [(28.0, 116.0), (116.0, 164.0), (164.0, 212.0), (212.0, 260.0),
(260.0, 372.0), (372.0, 440.0), (440.0, 508.0),
(508.0, 600.0), (600.0, 692.0)]
HEADERS = ["Perioada", "High", "Low", "Close", "PP", "R1", "S1", "R2", "S2"]
LETTERS = ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
TBL_Y0, HDR_Y1 = 72.0, 116.0
ROW1_Y0, ROW1_Y1 = 116.0, 184.0 # săptămâna (rândul 2)
ROW2_Y0, ROW2_Y1 = 184.0, 252.0 # ziua (rândul 3)
CMP_Y0, CMP_Y1 = 268.0, 312.0
TEXTS = [] # (x, y, s, size, weight, anchor, cell) cell=(x0,x1) sau None
def txt(x, y, s, size=13, weight="400", fill=BLACK, anchor="middle", cell=None):
TEXTS.append((x, y, s, size, weight, anchor, cell))
return (f'<text x="{x:.1f}" y="{y:.1f}" font-size="{size}" '
f'font-weight="{weight}" fill="{fill}" text-anchor="{anchor}">'
f'{escape(s)}</text>')
def line(x1, y1, x2, y2, stroke=GRID, width=1.0, dash=None):
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
f'stroke="{stroke}" stroke-width="{width}"{d}/>')
def rect(x, y, w, h, fill=WHITE, stroke=BORDER, width=1.2, rx=0):
r = f' rx="{rx}"' if rx else ""
return (f'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}"'
f'{r} fill="{fill}" stroke="{stroke}" stroke-width="{width}"/>')
def fmt(v):
return f"{v:.2f}"
def build():
p = [
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" '
f'width="{W}" height="{H}" role="img" aria-label="Tabel Excel: '
f'pivotii clasici calculati din OHLC, PP saptamanal vs PP zilnic, '
f'crossing bull">',
'<g font-family="system-ui, sans-serif">',
f'<rect x="0" y="0" width="{W}" height="{H}" fill="{WHITE}"/>',
txt(W / 2, 38, "Pivoții clasici în Excel — formula, pas cu pas",
20, "700", BLACK),
]
# ------- fundaluri: bandă de antet, rânduri alternate, gutter
p.append(rect(GUT_X, TBL_Y0, GUT_W,
ROW2_Y1 - TBL_Y0, fill=HDR_BG, stroke=BORDER, width=1.0))
p.append(rect(COLS[0][0], TBL_Y0, RIGHT - COLS[0][0], HDR_Y1 - TBL_Y0,
fill=HDR_BG, stroke=BORDER, width=1.0))
p.append(rect(COLS[0][0], ROW1_Y0, RIGHT - COLS[0][0], ROW1_Y1 - ROW1_Y0,
fill=WHITE, stroke=BORDER, width=1.0))
p.append(rect(COLS[0][0], ROW2_Y0, RIGHT - COLS[0][0], ROW2_Y1 - ROW2_Y0,
fill=ALT_BG, stroke=BORDER, width=1.0))
# ------- grilă verticală + orizontală
for x0, _ in COLS[1:]:
p.append(line(x0, TBL_Y0, x0, ROW2_Y1))
p.append(line(GUT_X + GUT_W, TBL_Y0, GUT_X + GUT_W, ROW2_Y1, BORDER, 1.0))
for y in (HDR_Y1, ROW1_Y1):
p.append(line(GUT_X, y, RIGHT, y, BORDER, 1.0))
p.append(rect(GUT_X, TBL_Y0, RIGHT - GUT_X, ROW2_Y1 - TBL_Y0,
fill="none", stroke=BORDER, width=1.4))
# ------- numere de rând Excel și litere de coloană
for num, (y0, y1) in zip("123", ((TBL_Y0, HDR_Y1), (ROW1_Y0, ROW1_Y1),
(ROW2_Y0, ROW2_Y1))):
p.append(txt(GUT_X + GUT_W / 2, (y0 + y1) / 2 + 5, num, 13, "400",
GRAY))
for (x0, x1), let in zip(COLS, LETTERS):
p.append(txt((x0 + x1) / 2, 90, let, 13, "400", GRAY))
for (x0, x1), h in zip(COLS, HEADERS):
p.append(txt((x0 + x1) / 2, 110, h, 13, "700", BLACK,
cell=(x0, x1)))
# ------- rândul 2: săptămâna (PPW) — celulele de calcul = formulă + rezultat
cA = COLS[0]
for k, s in enumerate(("Săptămâna", "încheiată", "(PPW)")):
p.append(txt((cA[0] + cA[1]) / 2, 138 + k * 18, s, 13, "400",
BLACK if k < 2 else GRAY, cell=cA))
for col, val in zip(COLS[1:4], WH):
p.append(txt((col[0] + col[1]) / 2, 158, fmt(val), 14, "400", BLACK,
cell=col))
week_calc = [("=(B2+C2+D2)/3", PPW), ("=2*E2-C2", W_R1), ("=2*E2-B2", W_S1),
("=E2+(B2-C2)", W_R2), ("=E2-(B2-C2)", W_S2)]
for col, (formula, res) in zip(COLS[4:], week_calc):
cx = (col[0] + col[1]) / 2
p.append(txt(cx, ROW1_Y0 + 26, formula, 13, "400", GRAY, cell=col))
p.append(txt(cx, ROW1_Y0 + 53, fmt(res), 15, "700", BLUE, cell=col))
# ------- rândul 3: ziua (PPD)
for k, s in enumerate(("Ziua", "încheiată", "(PPD)")):
p.append(txt((cA[0] + cA[1]) / 2, 200 + k * 18, s, 13, "400",
BLACK if k < 2 else GRAY, cell=cA))
for col, val in zip(COLS[1:4], DH):
p.append(txt((col[0] + col[1]) / 2, 226, fmt(val), 14, "400", BLACK,
cell=col))
day_calc = [("=(B3+C3+D3)/3", PPD), ("=2*E3-C3", D_R1), ("=2*E3-B3", D_S1),
("=E3+(B3-C3)", D_R2), ("=E3-(B3-C3)", D_S2)]
for col, (formula, res) in zip(COLS[4:], day_calc):
cx = (col[0] + col[1]) / 2
p.append(txt(cx, ROW2_Y0 + 26, formula, 13, "400", GRAY, cell=col))
p.append(txt(cx, ROW2_Y0 + 53, fmt(res), 15, "700", BLUE, cell=col))
# ------- rândul de comparație, evidențiat
p.append(rect(LEFT, CMP_Y0, RIGHT - LEFT, CMP_Y1 - CMP_Y0,
fill="#eaf7ee", stroke=GREEN, width=1.6, rx=6))
p.append(txt(W / 2, CMP_Y0 + 28,
f"PPD {fmt(PPD)} > PPW {fmt(PPW)} → CROSSING BULL (long)",
16, "700", BLACK))
# ------- legenda (exemplu construit) + note
p.append(txt(W / 2, 340,
"Cifrele sunt un exemplu construit, cu valori rotunde, pe TLV —",
13, "400", GRAY))
p.append(txt(W / 2, 358, "nu sunt date reale de piață.", 13, "400", GRAY))
p.append(txt(W / 2, 386,
"crossing = PPD trece deasupra PPW; dacă rămâne deasupra — "
"continuare (09_QA/16:35-43).", 13, "400", BLACK))
p.append(txt(W / 2, 408,
"Surse formule: 03_MODUL_2/02_zone_cerere_oferta_support_"
"resistance.md:42-50", 13, "400", GRAY))
p.append(txt(W / 2, 426,
"Calcul Excel 1-la-1, MP² = comparația a doi pivoți: "
"09_QA/09_2025-12-09.md:1176-1180.", 13, "400", GRAY))
p.append('</g></svg>')
return "\n".join(p) + "\n"
# ---------------------------------------------------------------- verificare
# Măsurăm textul cu fontul real folosit la rasterizare (DejaVu Sans), care e
# mai lat decât majoritatea fonturilor „system-ui": dacă încape aici, încape.
try:
from PIL import ImageFont
_REG = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
_FCACHE = {}
def _pil_w(s, size, weight):
path = _BOLD if weight == "700" else _REG
f = _FCACHE.get((path, size))
if f is None:
f = ImageFont.truetype(path, size)
_FCACHE[(path, size)] = f
return f.getlength(s)
_HAVE_PIL = True
except Exception: # pragma: no cover
_HAVE_PIL = False
def est_w(s, size, weight):
if _HAVE_PIL:
return _pil_w(s, size, weight)
# fallback conservator: supraestimează cu ~12%
narrow = " .,:;'|()[]{}"
total = 0.0
for ch in s:
if ch in narrow:
total += 0.30
elif ch.isdigit():
total += 0.60
elif ch.isupper():
total += 0.70
elif ch.islower():
total += 0.56
else:
total += 0.56
return total * size * (1.06 if weight == "700" else 1.0) * 1.12
def spans(x, y, s, size, weight, anchor):
ew = est_w(s, size, weight)
left = x - ew if anchor == "end" else (x - ew / 2 if anchor == "middle"
else x)
return left, left + ew
def verify(svg):
# 1. toate textele în panză
for x, y, s, size, weight, anchor, cell in TEXTS:
lo, hi = spans(x, y, s, size, weight, anchor)
assert -2 <= lo and hi <= W + 2, f"text în afara panzei: {s!r} ({lo:.0f},{hi:.0f})"
assert 0 <= y <= H, f"text în afara înălțimii: {s!r} (y={y})"
# 2. textele din tabel încap în celula lor
for x, y, s, size, weight, anchor, cell in TEXTS:
if cell is not None:
lo, hi = spans(x, y, s, size, weight, anchor)
assert cell[0] + 1 <= lo and hi <= cell[1] - 1, \
f"text iese din celulă: {s!r} ({lo:.0f},{hi:.0f}) vs {cell}"
# 3. texte care nu se suprapun
for i in range(len(TEXTS)):
xi, yi, si, szi, wi, ai, _ = TEXTS[i]
li, ri = spans(xi, yi, si, szi, wi, ai)
for j in range(i + 1, len(TEXTS)):
xj, yj, sj, szj, wj, aj, _ = TEXTS[j]
lj, rj = spans(xj, yj, sj, szj, wj, aj)
if abs(yi - yj) < 13 and li < rj - 3 and lj < ri - 3:
raise AssertionError(f"texte suprapuse: {si!r} / {sj!r}")
# 4. condiția de crossing din desen
assert CMP_Y0 > ROW2_Y1, "rândul de comparație nu e sub tabel"
assert CMP_Y1 < 340, "comparația intră în legendă"
assert PPD > PPW
# 5. conținutul obligatoriu din spec (textul vizibil, cu &gt; decodat)
plain = unescape(svg)
for cheie in (
"Pivoții clasici în Excel — formula, pas cu pas",
"Perioada", "High", "Low", "Close", "PP", "R1", "S1", "R2", "S2",
"Săptămâna", "încheiată", "(PPW)",
"Ziua", "(PPD)",
"=(B2+C2+D2)/3", "=2*E2-C2", "=2*E2-B2", "=E2+(B2-C2)", "=E2-(B2-C2)",
"=(B3+C3+D3)/3", "=2*E3-C3", "=2*E3-B3", "=E3+(B3-C3)", "=E3-(B3-C3)",
"21.37", "21.57", "22.23", "20.73", "22.87", "19.87",
"22.13", "21.23", "22.47", "20.67",
"PPD 21.57 > PPW 21.37", "CROSSING BULL (long)",
"exemplu construit", "nu sunt date reale de piață",
"09_QA/16:35-43", "09_QA/09_2025-12-09.md:1176-1180",
):
assert cheie in plain, f"lipsește din desen: {cheie!r}"
# 6. fără candele / axe de preț / prețuri inventate
for interzis in ("candela", "SL ", "TP1", "TP2", "intrare ", "$", "USD"):
assert interzis not in svg, f"nu ar trebui să apară: {interzis!r}"
return True
SVG = build()
assert verify(SVG), "verificare eșuată"
OUT.write_text(SVG, encoding="utf-8")
print(f"scris {OUT} ({W}x{H}), verificare OK: {len(TEXTS)} etichete")

View File

@@ -0,0 +1,81 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 444" width="700" height="444" role="img" aria-label="Tabel Excel: pivotii clasici calculati din OHLC, PP saptamanal vs PP zilnic, crossing bull">
<g font-family="system-ui, sans-serif">
<rect x="0" y="0" width="700" height="444" fill="#ffffff"/>
<text x="350.0" y="38.0" font-size="20" font-weight="700" fill="#000000" text-anchor="middle">Pivoții clasici în Excel — formula, pas cu pas</text>
<rect x="8.0" y="72.0" width="20.0" height="180.0" fill="#eef1f5" stroke="#333333" stroke-width="1.0"/>
<rect x="28.0" y="72.0" width="664.0" height="44.0" fill="#eef1f5" stroke="#333333" stroke-width="1.0"/>
<rect x="28.0" y="116.0" width="664.0" height="68.0" fill="#ffffff" stroke="#333333" stroke-width="1.0"/>
<rect x="28.0" y="184.0" width="664.0" height="68.0" fill="#fafbfc" stroke="#333333" stroke-width="1.0"/>
<line x1="116.0" y1="72.0" x2="116.0" y2="252.0" stroke="#c9d2dc" stroke-width="1.0"/>
<line x1="164.0" y1="72.0" x2="164.0" y2="252.0" stroke="#c9d2dc" stroke-width="1.0"/>
<line x1="212.0" y1="72.0" x2="212.0" y2="252.0" stroke="#c9d2dc" stroke-width="1.0"/>
<line x1="260.0" y1="72.0" x2="260.0" y2="252.0" stroke="#c9d2dc" stroke-width="1.0"/>
<line x1="372.0" y1="72.0" x2="372.0" y2="252.0" stroke="#c9d2dc" stroke-width="1.0"/>
<line x1="440.0" y1="72.0" x2="440.0" y2="252.0" stroke="#c9d2dc" stroke-width="1.0"/>
<line x1="508.0" y1="72.0" x2="508.0" y2="252.0" stroke="#c9d2dc" stroke-width="1.0"/>
<line x1="600.0" y1="72.0" x2="600.0" y2="252.0" stroke="#c9d2dc" stroke-width="1.0"/>
<line x1="28.0" y1="72.0" x2="28.0" y2="252.0" stroke="#333333" stroke-width="1.0"/>
<line x1="8.0" y1="116.0" x2="692.0" y2="116.0" stroke="#333333" stroke-width="1.0"/>
<line x1="8.0" y1="184.0" x2="692.0" y2="184.0" stroke="#333333" stroke-width="1.0"/>
<rect x="8.0" y="72.0" width="684.0" height="180.0" fill="none" stroke="#333333" stroke-width="1.4"/>
<text x="18.0" y="99.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">1</text>
<text x="18.0" y="155.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">2</text>
<text x="18.0" y="223.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">3</text>
<text x="72.0" y="90.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">A</text>
<text x="140.0" y="90.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">B</text>
<text x="188.0" y="90.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">C</text>
<text x="236.0" y="90.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">D</text>
<text x="316.0" y="90.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">E</text>
<text x="406.0" y="90.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">F</text>
<text x="474.0" y="90.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">G</text>
<text x="554.0" y="90.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">H</text>
<text x="646.0" y="90.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">I</text>
<text x="72.0" y="110.0" font-size="13" font-weight="700" fill="#000000" text-anchor="middle">Perioada</text>
<text x="140.0" y="110.0" font-size="13" font-weight="700" fill="#000000" text-anchor="middle">High</text>
<text x="188.0" y="110.0" font-size="13" font-weight="700" fill="#000000" text-anchor="middle">Low</text>
<text x="236.0" y="110.0" font-size="13" font-weight="700" fill="#000000" text-anchor="middle">Close</text>
<text x="316.0" y="110.0" font-size="13" font-weight="700" fill="#000000" text-anchor="middle">PP</text>
<text x="406.0" y="110.0" font-size="13" font-weight="700" fill="#000000" text-anchor="middle">R1</text>
<text x="474.0" y="110.0" font-size="13" font-weight="700" fill="#000000" text-anchor="middle">S1</text>
<text x="554.0" y="110.0" font-size="13" font-weight="700" fill="#000000" text-anchor="middle">R2</text>
<text x="646.0" y="110.0" font-size="13" font-weight="700" fill="#000000" text-anchor="middle">S2</text>
<text x="72.0" y="138.0" font-size="13" font-weight="400" fill="#000000" text-anchor="middle">Săptămâna</text>
<text x="72.0" y="156.0" font-size="13" font-weight="400" fill="#000000" text-anchor="middle">încheiată</text>
<text x="72.0" y="174.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">(PPW)</text>
<text x="140.0" y="158.0" font-size="14" font-weight="400" fill="#000000" text-anchor="middle">22.00</text>
<text x="188.0" y="158.0" font-size="14" font-weight="400" fill="#000000" text-anchor="middle">20.50</text>
<text x="236.0" y="158.0" font-size="14" font-weight="400" fill="#000000" text-anchor="middle">21.60</text>
<text x="316.0" y="142.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">=(B2+C2+D2)/3</text>
<text x="316.0" y="169.0" font-size="15" font-weight="700" fill="#1a5fb4" text-anchor="middle">21.37</text>
<text x="406.0" y="142.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">=2*E2-C2</text>
<text x="406.0" y="169.0" font-size="15" font-weight="700" fill="#1a5fb4" text-anchor="middle">22.23</text>
<text x="474.0" y="142.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">=2*E2-B2</text>
<text x="474.0" y="169.0" font-size="15" font-weight="700" fill="#1a5fb4" text-anchor="middle">20.73</text>
<text x="554.0" y="142.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">=E2+(B2-C2)</text>
<text x="554.0" y="169.0" font-size="15" font-weight="700" fill="#1a5fb4" text-anchor="middle">22.87</text>
<text x="646.0" y="142.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">=E2-(B2-C2)</text>
<text x="646.0" y="169.0" font-size="15" font-weight="700" fill="#1a5fb4" text-anchor="middle">19.87</text>
<text x="72.0" y="200.0" font-size="13" font-weight="400" fill="#000000" text-anchor="middle">Ziua</text>
<text x="72.0" y="218.0" font-size="13" font-weight="400" fill="#000000" text-anchor="middle">încheiată</text>
<text x="72.0" y="236.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">(PPD)</text>
<text x="140.0" y="226.0" font-size="14" font-weight="400" fill="#000000" text-anchor="middle">21.90</text>
<text x="188.0" y="226.0" font-size="14" font-weight="400" fill="#000000" text-anchor="middle">21.00</text>
<text x="236.0" y="226.0" font-size="14" font-weight="400" fill="#000000" text-anchor="middle">21.80</text>
<text x="316.0" y="210.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">=(B3+C3+D3)/3</text>
<text x="316.0" y="237.0" font-size="15" font-weight="700" fill="#1a5fb4" text-anchor="middle">21.57</text>
<text x="406.0" y="210.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">=2*E3-C3</text>
<text x="406.0" y="237.0" font-size="15" font-weight="700" fill="#1a5fb4" text-anchor="middle">22.13</text>
<text x="474.0" y="210.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">=2*E3-B3</text>
<text x="474.0" y="237.0" font-size="15" font-weight="700" fill="#1a5fb4" text-anchor="middle">21.23</text>
<text x="554.0" y="210.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">=E3+(B3-C3)</text>
<text x="554.0" y="237.0" font-size="15" font-weight="700" fill="#1a5fb4" text-anchor="middle">22.47</text>
<text x="646.0" y="210.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">=E3-(B3-C3)</text>
<text x="646.0" y="237.0" font-size="15" font-weight="700" fill="#1a5fb4" text-anchor="middle">20.67</text>
<rect x="8.0" y="268.0" width="684.0" height="44.0" rx="6" fill="#eaf7ee" stroke="#188a3e" stroke-width="1.6"/>
<text x="350.0" y="296.0" font-size="16" font-weight="700" fill="#000000" text-anchor="middle">PPD 21.57 &gt; PPW 21.37 → CROSSING BULL (long)</text>
<text x="350.0" y="340.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">Cifrele sunt un exemplu construit, cu valori rotunde, pe TLV —</text>
<text x="350.0" y="358.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">nu sunt date reale de piață.</text>
<text x="350.0" y="386.0" font-size="13" font-weight="400" fill="#000000" text-anchor="middle">crossing = PPD trece deasupra PPW; dacă rămâne deasupra — continuare (09_QA/16:35-43).</text>
<text x="350.0" y="408.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">Surse formule: 03_MODUL_2/02_zone_cerere_oferta_support_resistance.md:42-50</text>
<text x="350.0" y="426.0" font-size="13" font-weight="400" fill="#777777" text-anchor="middle">Calcul Excel 1-la-1, MP² = comparația a doi pivoți: 09_QA/09_2025-12-09.md:1176-1180.</text>
</g></svg>

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -0,0 +1,162 @@
"""ULTA — daily (pivoți monthly): diagrama de ansamblu a selecției (TIP: PRET).
Citește `gen/strategii/selectia_ansamblu.spec.txt`. Un simbol care vine dintr-un
downtrend weekly, atinge suportul monthly 355.48 și face un buy doji la 373.30
cu RVOL 0.61; apoi confirmarea (intrare 374.80) și drumul spre pivot/TP1 383.74,
respectiv TP2 402.00. Grafic daily, 32 de candele, serie continuă, cu panou de
volum și SMA-20 — construit prin `svg_candles` (from_closes), la scară.
Rulează:
python3 gen/strategii/selectia_ansamblu.py
Scrie lângă el `selectia_ansamblu.svg`.
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, "/workspace/atm-curs-dl")
from svg_candles import Chart, Level, Mark, MA, from_closes # noqa: E402
OUT = Path(__file__).resolve().parent / "selectia_ansamblu.svg"
GRAY, RED, GREEN, BLUE, ORANGE, TEAL = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000", "#00a0aa"
# ---------------------------------------------------------------- seria (32 bare)
# bare 0-11 : downtrend vizibil de la ~395 la ~362 (~0.8%/bara)
# bare 12-19 : baza/churn 356-373, minimul atinge suportul 355.48 pe bara 15
# bara 20 : buy doji, close 373.30 (open = close[19] = 373.00, corp 0.30)
# bara 21 : confirmare, inchide peste intrarea 374.80
# bare 22-31 : urcare spre pivot/TP1 383.74 (bara 26) si apoi spre TP2 402.00
CLOSES = [
395.00, 391.80, 388.60, 385.20, 382.00, 378.90, 375.70, 372.80, # 0-7
369.90, 367.00, 364.20, 361.80, # 8-11
363.20, 361.00, 358.50, 356.20, 360.00, 365.00, 369.50, 373.00, # 12-19
373.30, 375.60, # 20-21
377.00, 378.50, 380.40, 382.00, 384.50, 386.50, 389.50, 393.00, # 22-29
397.50, 401.50, # 30-31
]
assert len(CLOSES) == 32, len(CLOSES)
WICKS = [(0.35, 0.40)] * 32
WICKS[0] = (0.40, 0.50) # prima bara, deschide dintr-un varf al downtrend-ului
WICKS[15] = (0.30, 0.72) # low exact 355.48 = suportul monthly
WICKS[16] = (0.40, 0.30) # rebound, nu sparge suportul
WICKS[20] = (0.80, 0.30) # doji: high 374.10, low 372.70
WICKS[31] = (0.55, 0.40) # high 402.05, atinge TP2 402.00
assert len(WICKS) == 32, len(WICKS)
# barele 25-31 in crestere; bara 20 (doji) sub media seriei (RVOL 0.61)
VOLS = [
42, 44, 41, 46, 43, 45, 40, 44, 47, 43, # 0-9
40, 42, 38, 41, 52, # 10-14
56, 50, 45, 42, 39, # 15-19
30, # 20 doji, sub medie
46, # 21 confirmare
48, 50, 52, 54, 58, 62, 66, 70, 76, 82, # 22-31
]
assert len(VOLS) == 32, len(VOLS)
media_vol = sum(VOLS) / len(VOLS)
assert VOLS[20] < media_vol, "bara 20 (doji) trebuie sub media seriei"
assert all(a < b for a, b in zip(VOLS[25:32], VOLS[26:32])), "barele 25-31 nu cresc"
# RVOL afisat (0.61) ~ raportul barei de doji fata de media seriei
assert abs(VOLS[20] / media_vol - 0.61) < 0.02, VOLS[20] / media_vol
cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=396.50)
# ---------------------------------------------------------------- assert-urile din spec
assert abs(cds[20].c - 373.30) < 1e-9, "doji-ul trebuie sa inchida la 373.30"
assert abs(max(cds[20].o, cds[20].c) - min(cds[20].o, cds[20].c)) <= 0.40, "bara 20 nu e doji"
assert abs(min(c.l for c in cds[12:21]) - 355.48) < 0.05, "suportul 355.48 nu e atins"
assert 352.00 < 355.48 < 373.30 < 374.80 < 383.74 < 402.00, "ordinea nivelurilor"
risc = 374.80 - 352.00 # 22.80
assert abs((383.74 - 374.80) / risc - 0.39) < 0.02, "TP1 nu e ~0.39R"
assert abs((402.00 - 374.80) / risc - 1.19) < 0.02, "TP2 nu e ~1.19R"
assert cds[-1].c >= 383.74, "TP1 nu e atins in serie"
assert min(c.l for c in cds) >= 351.5, "SL-ul de 352.00 nu poate fi sub minimul seriei"
for a, b in zip(cds, cds[1:]):
assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial intre bare"
# structura ceruta explicit: doji mic cu fitilele date, confirmare peste intrare
assert abs(cds[20].h - 374.10) < 0.05 and abs(cds[20].l - 372.70) < 0.05, (cds[20].h, cds[20].l)
assert cds[21].c > 374.80, "bara 21 nu confirma intrarea"
assert max(cds[i].h for i in range(22, 27)) >= 383.74, "TP1 nu e atins in jurul barei 26"
assert cds[31].h >= 402.00, "TP2 nu e atins spre final"
# ---------------------------------------------------------------- niveluri, etichete
levels = [
Level(352.00, "SL 352.00", RED, label_at="left"),
Level(355.48, "suport monthly 355.48", BLUE, label_at="left"),
Level(374.80, "intrare 374.80", GREEN),
Level(383.74, "pivot / TP1 383.74", BLUE),
Level(402.00, "TP2 402.00", BLUE),
]
marks = [
# „sub bara 0" ar insemna peste candelele care coboara (staircase); dy-ul
# o ridica deasupra varfului de start, unde textul se citeste curat
Mark(0, "downtrend weekly", GRAY, "below", dy=-62),
Mark(20, "buy doji close 373.30 (RVOL 0.61)", TEAL, "below"),
Mark(21, "intrare 374.80", GREEN, "above"),
Mark(26, "TP1 383.74 = pivot", BLUE, "above", dy=-8),
Mark(31, "TP2 402.00", BLUE, "above", dy=-22),
]
caption = ("Ale cursului: close 373.30, suport 355.48, pivot 383.74, RVOL 0.61. "
"Intrarea 374.80, SL 352.00 si TP2 402.00 sunt estimari din 04_MODUL_3/01; "
"structura barelor dintre niveluri e construita, la scara.")
ch = Chart(
candles=cds, levels=levels, marks=marks,
mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")],
scale=8.5, gap=15, body_w=9, left=44, width=640, vol_h=70,
vol_label="volum",
title="ULTA — daily (pivoți monthly)",
caption=caption,
)
svg = ch.render()
# ---------------------------------------------------------------- verificarea de scara
ys = {p: ch._y(p) for p in (352.00, 355.48, 374.80, 383.74, 402.00)}
px_risc = ys[352.00] - ys[374.80] # y creste in jos, deci SL-ul are y mai mare
assert abs((ys[374.80] - ys[383.74]) / px_risc - (383.74 - 374.80) / risc) < 1e-9, "TP1 la scara gresita"
assert abs((ys[374.80] - ys[402.00]) / px_risc - (402.00 - 374.80) / risc) < 1e-9, "TP2 la scara gresita"
# ---------------------------------------------------------------- finisaj text
# caption-ul pe un singur rand depaseste latimea panzei (se taie la viewBox):
# il rupem in linii si crestem inaltimea desenului.
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, 92)
m = re.search(r'<text x="24" y="(\d+)" fill="#555">' + re.escape(caption) + r"</text>\s*</svg>", svg)
assert m, "caption-ul nu a fost gasit in SVG"
y0 = int(m.group(1))
wrapped = "\n ".join(f'<text x="24" y="{y0 + 17 * i}" fill="#555">{ln}</text>'
for i, ln in enumerate(cap_lines))
svg = svg[:m.start()] + wrapped + "\n</svg>"
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)
# „SMA-20" (x=515, y=269) sta la 12.6px de eticheta „intrare 374.80" (y=281.6),
# adica lipite: o ridicam 12px pe linia libera de deasupra.
svg, n = re.subn(r'(<text x="\d+" y=")(\d+)(" fill="#b06000">SMA-20</text>)',
lambda mm: f'{mm.group(1)}{int(mm.group(2)) - 12}{mm.group(3)}', svg)
assert n == 1, f"eticheta SMA-20: {n} potriviri"
assert "<svg" in svg and "\n\n" not in svg, "linie goala in SVG"
OUT.write_text(svg)
print(f"scris {OUT.name}: {len(cds)} candele, doji {cds[20].c:.2f} (RVOL {VOLS[20]/media_vol:.2f}), "
f"suport {min(c.l for c in cds[12:21]):.2f}, intrare 374.80, SL 352.00, "
f"TP1 383.74 = 0.39R, TP2 402.00 = 1.19R, ultimul close {cds[-1].c:.2f}")

View File

@@ -0,0 +1,121 @@
<svg viewBox="0 0 640 653" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;font:12px sans-serif">
<text x="24" y="48" fill="#333" style="font-weight:bold">ULTA — daily (pivoți monthly)</text>
<line x1="24" y1="481.4" x2="626" y2="481.4" stroke="#c00" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="30" y="475.4" fill="#c00">SL 352.00</text>
<line x1="24" y1="451.8" x2="626" y2="451.8" stroke="#1a5fb4" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="30" y="445.8" fill="#1a5fb4">suport monthly 355.48</text>
<line x1="24" y1="287.6" x2="626" y2="287.6" stroke="#188a3e" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="612" y="281.6" fill="#188a3e" text-anchor="end">intrare 374.80</text>
<line x1="24" y1="211.6" x2="626" y2="211.6" stroke="#1a5fb4" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="612" y="205.6" fill="#1a5fb4" text-anchor="end">pivot / TP1 383.74</text>
<line x1="24" y1="56.4" x2="626" y2="56.4" stroke="#1a5fb4" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="612" y="50.4" fill="#1a5fb4" text-anchor="end">TP2 402.00</text>
<polyline points="329.0,311.7 344.0,320.9 359.0,327.8 374.0,332.8 389.0,335.6 404.0,336.3 419.0,335.0 434.0,331.2 449.0,325.4 464.0,317.1 479.0,306.0 494.0,291.9 509.0,275.0" fill="none" stroke="#b06000" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="515" y="257" fill="#b06000">SMA-20</text>
<line x1="44.0" y1="99.8" x2="44.0" y2="120.2" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="39.5" y="103.2" width="9" height="12.8" fill="#c00"/>
<line x1="59.0" y1="112.9" x2="59.0" y2="146.5" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="54.5" y="115.9" width="9" height="27.2" fill="#c00"/>
<line x1="74.0" y1="140.1" x2="74.0" y2="173.7" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="69.5" y="143.1" width="9" height="27.2" fill="#c00"/>
<line x1="89.0" y1="167.3" x2="89.0" y2="202.6" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="84.5" y="170.3" width="9" height="28.9" fill="#c00"/>
<line x1="104.0" y1="196.2" x2="104.0" y2="229.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="99.5" y="199.2" width="9" height="27.2" fill="#c00"/>
<line x1="119.0" y1="223.4" x2="119.0" y2="256.2" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="114.5" y="226.4" width="9" height="26.4" fill="#c00"/>
<line x1="134.0" y1="249.8" x2="134.0" y2="283.4" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="129.5" y="252.8" width="9" height="27.2" fill="#c00"/>
<line x1="149.0" y1="277.0" x2="149.0" y2="308.0" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="144.5" y="280.0" width="9" height="24.6" fill="#c00"/>
<line x1="164.0" y1="301.6" x2="164.0" y2="332.7" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="159.5" y="304.6" width="9" height="24.7" fill="#c00"/>
<line x1="179.0" y1="326.3" x2="179.0" y2="357.3" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="174.5" y="329.3" width="9" height="24.6" fill="#c00"/>
<line x1="194.0" y1="350.9" x2="194.0" y2="381.1" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="189.5" y="353.9" width="9" height="23.8" fill="#c00"/>
<line x1="209.0" y1="374.8" x2="209.0" y2="401.5" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="204.5" y="377.7" width="9" height="20.4" fill="#c00"/>
<line x1="224.0" y1="383.2" x2="224.0" y2="401.5" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="219.5" y="386.2" width="9" height="11.9" fill="#188a3e"/>
<line x1="239.0" y1="383.2" x2="239.0" y2="408.3" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="234.5" y="386.2" width="9" height="18.7" fill="#c00"/>
<line x1="254.0" y1="401.9" x2="254.0" y2="429.6" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="249.5" y="404.9" width="9" height="21.2" fill="#c00"/>
<line x1="269.0" y1="423.6" x2="269.0" y2="451.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="264.5" y="426.2" width="9" height="19.6" fill="#c00"/>
<line x1="284.0" y1="410.0" x2="284.0" y2="448.3" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="279.5" y="413.4" width="9" height="32.3" fill="#188a3e"/>
<line x1="299.0" y1="367.9" x2="299.0" y2="416.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="294.5" y="370.9" width="9" height="42.5" fill="#188a3e"/>
<line x1="314.0" y1="329.7" x2="314.0" y2="374.3" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="309.5" y="332.7" width="9" height="38.2" fill="#188a3e"/>
<line x1="329.0" y1="299.9" x2="329.0" y2="336.1" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="324.5" y="302.9" width="9" height="29.8" fill="#188a3e"/>
<line x1="344.0" y1="293.6" x2="344.0" y2="305.5" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="339.5" y="300.4" width="9" height="2.6" fill="#188a3e"/>
<line x1="359.0" y1="277.8" x2="359.0" y2="303.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="354.5" y="280.8" width="9" height="19.6" fill="#188a3e"/>
<line x1="374.0" y1="265.9" x2="374.0" y2="284.2" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="369.5" y="268.9" width="9" height="11.9" fill="#188a3e"/>
<line x1="389.0" y1="253.2" x2="389.0" y2="272.3" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="384.5" y="256.2" width="9" height="12.8" fill="#188a3e"/>
<line x1="404.0" y1="237.1" x2="404.0" y2="259.6" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="399.5" y="240.0" width="9" height="16.1" fill="#188a3e"/>
<line x1="419.0" y1="223.4" x2="419.0" y2="243.4" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="414.5" y="226.4" width="9" height="13.6" fill="#188a3e"/>
<line x1="434.0" y1="202.2" x2="434.0" y2="229.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="429.5" y="205.2" width="9" height="21.2" fill="#188a3e"/>
<line x1="449.0" y1="185.2" x2="449.0" y2="208.6" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="444.5" y="188.2" width="9" height="17.0" fill="#188a3e"/>
<line x1="464.0" y1="159.7" x2="464.0" y2="191.6" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="459.5" y="162.7" width="9" height="25.5" fill="#188a3e"/>
<line x1="479.0" y1="129.9" x2="479.0" y2="166.1" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="474.5" y="132.9" width="9" height="29.8" fill="#188a3e"/>
<line x1="494.0" y1="91.7" x2="494.0" y2="136.3" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="489.5" y="94.7" width="9" height="38.2" fill="#188a3e"/>
<line x1="509.0" y1="56.0" x2="509.0" y2="98.1" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="504.5" y="60.7" width="9" height="34.0" fill="#188a3e"/>
<line x1="24" y1="577.4" x2="626" y2="577.4" stroke="#888"/>
<rect x="39.5" y="541.6" width="9" height="35.9" fill="#c00" opacity="0.55"/>
<rect x="54.5" y="539.9" width="9" height="37.6" fill="#c00" opacity="0.55"/>
<rect x="69.5" y="542.4" width="9" height="35.0" fill="#c00" opacity="0.55"/>
<rect x="84.5" y="538.2" width="9" height="39.3" fill="#c00" opacity="0.55"/>
<rect x="99.5" y="540.7" width="9" height="36.7" fill="#c00" opacity="0.55"/>
<rect x="114.5" y="539.0" width="9" height="38.4" fill="#c00" opacity="0.55"/>
<rect x="129.5" y="543.3" width="9" height="34.1" fill="#c00" opacity="0.55"/>
<rect x="144.5" y="539.9" width="9" height="37.6" fill="#c00" opacity="0.55"/>
<rect x="159.5" y="537.3" width="9" height="40.1" fill="#c00" opacity="0.55"/>
<rect x="174.5" y="540.7" width="9" height="36.7" fill="#c00" opacity="0.55"/>
<rect x="189.5" y="543.3" width="9" height="34.1" fill="#c00" opacity="0.55"/>
<rect x="204.5" y="541.6" width="9" height="35.9" fill="#c00" opacity="0.55"/>
<rect x="219.5" y="545.0" width="9" height="32.4" fill="#188a3e" opacity="0.55"/>
<rect x="234.5" y="542.4" width="9" height="35.0" fill="#c00" opacity="0.55"/>
<rect x="249.5" y="533.0" width="9" height="44.4" fill="#c00" opacity="0.55"/>
<rect x="264.5" y="529.6" width="9" height="47.8" fill="#c00" opacity="0.55"/>
<rect x="279.5" y="534.7" width="9" height="42.7" fill="#188a3e" opacity="0.55"/>
<rect x="294.5" y="539.0" width="9" height="38.4" fill="#188a3e" opacity="0.55"/>
<rect x="309.5" y="541.6" width="9" height="35.9" fill="#188a3e" opacity="0.55"/>
<rect x="324.5" y="544.1" width="9" height="33.3" fill="#188a3e" opacity="0.55"/>
<rect x="339.5" y="551.8" width="9" height="25.6" fill="#188a3e" opacity="0.55"/>
<rect x="354.5" y="538.2" width="9" height="39.3" fill="#188a3e" opacity="0.55"/>
<rect x="369.5" y="536.4" width="9" height="41.0" fill="#188a3e" opacity="0.55"/>
<rect x="384.5" y="534.7" width="9" height="42.7" fill="#188a3e" opacity="0.55"/>
<rect x="399.5" y="533.0" width="9" height="44.4" fill="#188a3e" opacity="0.55"/>
<rect x="414.5" y="531.3" width="9" height="46.1" fill="#188a3e" opacity="0.55"/>
<rect x="429.5" y="527.9" width="9" height="49.5" fill="#188a3e" opacity="0.55"/>
<rect x="444.5" y="524.5" width="9" height="52.9" fill="#188a3e" opacity="0.55"/>
<rect x="459.5" y="521.1" width="9" height="56.3" fill="#188a3e" opacity="0.55"/>
<rect x="474.5" y="517.7" width="9" height="59.8" fill="#188a3e" opacity="0.55"/>
<rect x="489.5" y="512.5" width="9" height="64.9" fill="#188a3e" opacity="0.55"/>
<rect x="504.5" y="507.4" width="9" height="70.0" fill="#188a3e" opacity="0.55"/>
<text x="24" y="501.4" fill="#555">volum</text>
<text x="32.0" y="78.2" fill="#777" text-anchor="start" style="font-weight:bold">downtrend weekly</text>
<text x="344.0" y="325.5" fill="#00a0aa" text-anchor="middle" style="font-weight:bold">buy doji close 373.30 (RVOL 0.61)</text>
<text x="359.0" y="267.8" fill="#188a3e" text-anchor="middle" style="font-weight:bold">intrare 374.80</text>
<text x="434.0" y="184.2" fill="#1a5fb4" text-anchor="middle" style="font-weight:bold">TP1 383.74 = pivot</text>
<text x="509.0" y="24.0" fill="#1a5fb4" text-anchor="middle" style="font-weight:bold">TP2 402.00</text>
<text x="24" y="607" fill="#555">Ale cursului: close 373.30, suport 355.48, pivot 383.74, RVOL 0.61. Intrarea 374.80, SL</text>
<text x="24" y="624" fill="#555">352.00 si TP2 402.00 sunt estimari din 04_MODUL_3/01; structura barelor dintre niveluri e</text>
<text x="24" y="641" fill="#555">construita, la scara.</text>
</svg>

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -0,0 +1,327 @@
#!/usr/bin/env python3
"""Relative strength — cele 4 cadrane (matrice 2x2).
Diagramă conceptuală (NU grafic de preț): pentru `strategii/04_selectia.md`,
Nivel 1, §1.2 F4 „Relative strength în cadranul 1-2". Citește specificația
`gen/strategii/selectia_cadrane.spec.txt` și scrie
`gen/strategii/selectia_cadrane.svg`.
Nu folosește `svg_candles` / `pchart`: SVG-ul e construit ca string, doar
stdlib.
Axele (așa cum le dă spec-ul):
- vertical: histograma RS față de linia de 0 (peste / sub 0);
- orizontal: histograma RS față de MA (peste / sub MA).
Cadranele (definițiile din spec, preluate din
03_MODUL_2/06_indicatorul_de_performanta.md:31-39):
1 supraperformanță peste 0 ȘI peste MA -> LONG
2 îmbunătățire sub 0, dar peste MA -> LONG (zona de cumpărat)
3 înrăutățire peste 0, dar sub MA -> SHORT
4 subperformanță sub 0 ȘI sub MA -> SHORT / stai pe margine
Banda de jos e avertismentul despre cadranul 4 (08_ANALIZA_ZILNICA/
11_selectia_simbolului.md:493-508), plus nota mică despre eroarea de
transcriere (03_MODUL_2/06 ...:41-48) — marcată explicit ca NOTĂ, nu ca
definiție a cursului.
Fără cifre de RS inventate: desenul e pur calitativ.
Rulare: .venv/bin/python gen/strategii/selectia_cadrane.py
"""
import html
from pathlib import Path
W, H = 700, 720
BLACK = "#000000"
GRAY = "#777777"
RED = "#c00"
GREEN = "#188a3e"
BLUE = "#1a5fb4"
ORANGE = "#b06000"
GREEN_BG = "#eef7f0"
RED_BG = "#fdeeee"
# ---------------------------------------------------------------- geometrie
PLOT_X0, PLOT_X1 = 130.0, 570.0
PLOT_Y0, PLOT_Y1 = 134.0, 414.0
CX = (PLOT_X0 + PLOT_X1) / 2.0 # 350
CY = (PLOT_Y0 + PLOT_Y1) / 2.0 # 266
GUT = 4.0
CW = (PLOT_X1 - PLOT_X0 - GUT) / 2.0 # 218
CH = (PLOT_Y1 - PLOT_Y0 - GUT) / 2.0 # 138
Q3 = {"x": PLOT_X0, "y": PLOT_Y0} # peste 0, sub MA
Q1 = {"x": CX + GUT / 2, "y": PLOT_Y0} # peste 0, peste MA
Q4 = {"x": PLOT_X0, "y": CY + GUT / 2} # sub 0, sub MA
Q2 = {"x": CX + GUT / 2, "y": CY + GUT / 2} # sub 0, peste MA
for _q in (Q1, Q2, Q3, Q4):
_q["w"], _q["h"] = CW, CH
BAND = {"x": 40.0, "y": 460.0, "w": 620.0, "h": 196.0}
# ---------------------------------------------------------------- collect
TEXTS = [] # (x, y, s, size, weight, fill, anchor)
RECTS = [] # (x, y, w, h, tag)
def add(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"):
TEXTS.append((x, y, s, size, weight, fill, anchor))
return s
def est_w(s, size, weight):
factor = 0.62 if weight == "bold" else 0.56
return len(s) * size * factor
def txt(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"):
return (f'<text x="{x:.1f}" y="{y:.1f}" font-size="{size}" '
f'font-weight="{weight}" fill="{fill}" text-anchor="{anchor}">'
f'{html.escape(s, quote=False)}</text>')
def rect(x, y, w, h, tag, fill="#ffffff", stroke=GRAY, sw=1.5, dash=None):
RECTS.append((x, y, w, h, tag))
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}" '
f'fill="{fill}" stroke="{stroke}" stroke-width="{sw}"{d}/>')
def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None):
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
f'stroke="{stroke}" stroke-width="{width}"{d}/>')
def cell_text(q, dy, s, *a, **k):
add(q["x"] + q["w"] / 2.0, q["y"] + dy, s, *a, **k)
# =============================================================== CONȚINUT
p = []
# ---- titlu + subtitlu + legenda axelor
add(W / 2, 34, "Relative strength — cele 4 cadrane", 20, "bold")
add(W / 2, 58, "RS = cât de bine se mișcă simbolul față de benchmark (ex. SPY)",
13, "normal", GRAY)
add(W / 2, 80,
"Axa verticală: histograma RS față de 0 (peste / sub 0)",
13, "normal", GRAY)
add(W / 2, 98,
"Axa orizontală: histograma RS față de MA (peste / sub MA)",
13, "normal", GRAY)
# ---- etichetele coloanelor / rândurilor (axe)
add(PLOT_X0 + CW / 2 + GUT / 2, 122, "sub MA", 14, "bold", ORANGE)
add(CX + GUT / 2 + CW / 2, 122, "peste MA", 14, "bold", BLUE)
add(PLOT_X0 - 8, Q3["y"] + CH / 2, "peste 0", 14, "bold", GREEN, "end")
add(PLOT_X0 - 8, Q4["y"] + CH / 2, "sub 0", 14, "bold", RED, "end")
# ---- liniile de 0 (orizontală) și de MA (verticală), în spatele cadranelor
p.append(rect(PLOT_X0, PLOT_Y0, PLOT_X1 - PLOT_X0, PLOT_Y1 - PLOT_Y0,
"plot", fill="#ffffff", stroke=GRAY, sw=1.0))
p.append(line(PLOT_X0, CY, PLOT_X1, CY, GRAY, 1.0, "4 4"))
p.append(line(CX, PLOT_Y0, CX, PLOT_Y1, GRAY, 1.0, "4 4"))
# ---- cadrane
p.append(rect(Q3["x"], Q3["y"], CW, CH, "Q3", fill=RED_BG, stroke=RED, sw=1.6))
p.append(rect(Q1["x"], Q1["y"], CW, CH, "Q1", fill=GREEN_BG, stroke=GREEN, sw=1.6))
p.append(rect(Q4["x"], Q4["y"], CW, CH, "Q4", fill=RED_BG, stroke=RED, sw=1.6,
dash="6 4"))
p.append(rect(Q2["x"], Q2["y"], CW, CH, "Q2", fill=GREEN_BG, stroke=GREEN, sw=1.6))
# Cadran 3 — top-left: peste 0, sub MA -> SHORT
cell_text(Q3, 30, "Cadran 3", 16, "bold", BLACK)
cell_text(Q3, 52, "înrăutățire", 14, "bold", RED)
cell_text(Q3, 80, "peste 0, dar sub MA", 13, "normal", BLACK)
cell_text(Q3, 112, "SHORT", 16, "bold", RED)
# Cadran 1 — top-right: peste 0, peste MA -> LONG
cell_text(Q1, 30, "Cadran 1", 16, "bold", BLACK)
cell_text(Q1, 52, "supraperformanță", 14, "bold", GREEN)
cell_text(Q1, 80, "peste 0 și peste MA", 13, "normal", BLACK)
cell_text(Q1, 112, "LONG", 16, "bold", GREEN)
# Cadran 4 — bottom-left: sub 0, sub MA -> SHORT / stai pe margine
cell_text(Q4, 26, "Cadran 4", 16, "bold", BLACK)
cell_text(Q4, 48, "subperformanță", 14, "bold", RED)
cell_text(Q4, 74, "sub 0 și sub MA", 13, "normal", BLACK)
cell_text(Q4, 100, "SHORT", 15, "bold", RED)
cell_text(Q4, 122, "stai pe margine", 13, "normal", RED)
# Cadran 2 — bottom-right: sub 0, peste MA -> LONG (zona de cumpărat)
cell_text(Q2, 26, "Cadran 2", 16, "bold", BLACK)
cell_text(Q2, 48, "îmbunătățire", 14, "bold", GREEN)
cell_text(Q2, 74, "sub 0, dar peste MA", 13, "normal", BLACK)
cell_text(Q2, 100, "LONG", 15, "bold", GREEN)
cell_text(Q2, 122, "zona de cumpărat", 13, "normal", GREEN)
# ---- concluzia de sub matrice
add(W / 2, 442, "Long = cadranele 1-2 · Short = cadranele 3-4",
14, "bold", BLACK)
# ---- banda de avertisment (cadranul 4)
p.append(rect(BAND["x"], BAND["y"], BAND["w"], BAND["h"], "band",
fill="#fdf0e0", stroke=ORANGE, sw=1.6))
add(BAND["x"] + 20, 488, "Avertisment — cadranul 4 nu se tranzacționează pe orb",
15, "bold", RED, "start")
add(BAND["x"] + 20, 516,
"„Cadranul 4 nu se tranzacționează pe orb: aștepți trecerea în 2 sau 1,",
13, "normal", BLACK, "start")
add(BAND["x"] + 20, 536, "ori un MP4-1 / MBT-10 long ca semn de schimbare.”",
13, "normal", BLACK, "start")
add(BAND["x"] + 20, 566,
"„Un setup de bottom cu RS fără îmbunătățire are șanse mai mici.”",
13, "bold", BLACK, "start")
add(BAND["x"] + 20, 596,
"Notă: transcrierea descrierii cadranului 4 are o eroare (îl descrie ca pe",
13, "normal", GRAY, "start")
add(BAND["x"] + 20, 616,
"cadranul 3); definiția de mai sus e completarea sumarului —",
13, "normal", GRAY, "start")
add(BAND["x"] + 20, 636,
"03_MODUL_2/06_indicatorul_de_performanta.md:41-48.",
13, "normal", GRAY, "start")
# ---- surse
add(W / 2, 686,
"Surse: 08_ANALIZA_ZILNICA/11_selectia_simbolului.md:486-499, :501-508;",
13, "normal", GRAY)
add(W / 2, 704,
"03_MODUL_2/06_indicatorul_de_performanta.md:31-39.",
13, "normal", GRAY)
# =============================================================== EMITERE
head = (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" '
f'width="{W}" height="{H}" font-family="system-ui, sans-serif" role="img" '
f'aria-label="Matricea 2x2 a relative strength: cadranele 1 si 2 sunt '
f'pentru long, cadranele 3 si 4 pentru short; cadranul 4 nu se '
f'tranzactioneaza pe orb">')
body = [head, f'<rect x="0" y="0" width="{W}" height="{H}" fill="#ffffff"/>']
body += p
for x, y, s, size, weight, fill, anchor in TEXTS:
body.append(txt(x, y, s, size, weight, fill, anchor))
body.append("</svg>")
svg = "\n".join(body) + "\n"
# =============================================================== VERIFICARE
# 1. geometrie: 4 cadrane egale, fără suprapunere, în interiorul plot-ului
assert len(RECTS) == 6, f"aștept 6 dreptunghiuri, am {len(RECTS)}"
quads = [r for r in RECTS if r[4] in ("Q1", "Q2", "Q3", "Q4")]
assert len(quads) == 4, f"aștept 4 cadrane, am {len(quads)}"
for x, y, w, h, tag in quads:
assert abs(w - CW) < 1e-9 and abs(h - CH) < 1e-9, f"{tag}: dimensiune greșită"
assert PLOT_X0 - 0.5 <= x and x + w <= PLOT_X1 + 0.5, f"{tag}: iese orizontal"
assert PLOT_Y0 - 0.5 <= y and y + h <= PLOT_Y1 + 0.5, f"{tag}: iese vertical"
for i in range(len(quads)):
for j in range(i + 1, len(quads)):
x1, y1, w1, h1, t1 = quads[i]
x2, y2, w2, h2, t2 = quads[j]
assert not (x1 < x2 + w2 and x2 < x1 + w1 and
y1 < y2 + h2 and y2 < y1 + h1), \
f"cadrane suprapuse: {t1} / {t2}"
# 2. pozițiile relative: 1 sus-dreapta, 2 jos-dreapta, 3 sus-stânga, 4 jos-stânga
by_tag = {r[4]: r for r in quads}
assert by_tag["Q1"][0] > CX and by_tag["Q1"][1] < CY, "cadranul 1 nu e sus-dreapta"
assert by_tag["Q2"][0] > CX and by_tag["Q2"][1] > CY, "cadranul 2 nu e jos-dreapta"
assert by_tag["Q3"][0] < CX and by_tag["Q3"][1] < CY, "cadranul 3 nu e sus-stânga"
assert by_tag["Q4"][0] < CX and by_tag["Q4"][1] > CY, "cadranul 4 nu e jos-stânga"
# 3. definițiile din spec sunt exact cele desenate
for req in ("Cadran 1", "Cadran 2", "Cadran 3", "Cadran 4",
"supraperformanță", "îmbunătățire", "înrăutățire",
"subperformanță"):
assert any(t[2] == req for t in TEXTS), f"lipsește din desen: {req!r}"
# condiția fiecărui cadran
assert any(t[2] == "peste 0 și peste MA" for t in TEXTS)
assert any(t[2] == "sub 0, dar peste MA" for t in TEXTS)
assert any(t[2] == "peste 0, dar sub MA" for t in TEXTS)
assert any(t[2] == "sub 0 și sub MA" for t in TEXTS)
# direcția: LONG pe 1-2, SHORT pe 3-4
assert svg.count(">LONG</text>") == 2, "LONG trebuie să apară exact de 2 ori"
assert svg.count(">SHORT</text>") == 2, "SHORT trebuie să apară exact de 2 ori"
assert "stai pe margine" in svg, "lipsește «stai pe margine» din cadranul 4"
assert "zona de cumpărat" in svg, "lipsește «zona de cumpărat» din cadranul 2"
assert "Long = cadranele 1-2" in svg and "Short = cadranele 3-4" in svg
# 4. titlu, subtitlu, legenda axelor, avertismentul, nota
for req in ("Relative strength — cele 4 cadrane",
"RS = cât de bine se mișcă simbolul față de benchmark (ex. SPY)",
"Axa verticală: histograma RS față de 0 (peste / sub 0)",
"Axa orizontală: histograma RS față de MA (peste / sub MA)",
"Cadranul 4 nu se tranzacționează pe orb",
"aștepți trecerea în 2 sau 1",
"MP4-1 / MBT-10 long",
"Un setup de bottom cu RS fără îmbunătățire are șanse mai mici",
"transcrierea descrierii cadranului 4 are o eroare",
"e completarea sumarului",
"03_MODUL_2/06_indicatorul_de_performanta.md:41-48",
"11_selectia_simbolului.md:486-499",
"06_indicatorul_de_performanta.md:31-39"):
assert req in svg, f"lipsește din desen: {req!r}"
# 5. etichetele cadranului 2 și 3 care se pot încurca: verifică asocierea
t2 = [t for t in TEXTS if t[0] > CX and t[1] > CY]
assert any("îmbunătățire" == t[2] for t in t2), "cadranul 2 nu e marcat corect"
t3 = [t for t in TEXTS if t[0] < CX and t[1] < CY]
assert any("înrăutățire" == t[2] for t in t3), "cadranul 3 nu e marcat corect"
# 6. textul fiecărui cadran încape în cadranul lui
for tag, q in (("Q1", Q1), ("Q2", Q2), ("Q3", Q3), ("Q4", Q4)):
for x, y, s, size, weight, fill, anchor in TEXTS:
if q["x"] <= x <= q["x"] + q["w"] and q["y"] <= y <= q["y"] + q["h"]:
w = est_w(s, size, weight)
assert w <= q["w"] - 16, f"{tag}: text prea lat: {s!r} ({w:.0f})"
# 7. toate etichetele în panză + fără suprapuneri
boxes = []
for x, y, s, size, weight, fill, anchor in TEXTS:
assert size >= 13, f"font sub 13px: {s!r} ({size})"
w = est_w(s, size, weight)
if anchor == "end":
left = x - w
elif anchor == "start":
left = x
else:
left = x - w / 2.0
right = left + w
top, bottom = y - size * 0.78, y + size * 0.22
assert -2 <= left and right <= W + 2, f"text în afara panzei orizontal: {s!r}"
assert 0 <= top and bottom <= H, f"text în afara panzei vertical: {s!r}"
boxes.append((left, right, top, bottom, s))
for i in range(len(boxes)):
for j in range(i + 1, len(boxes)):
l1, r1, t1, b1, s1 = boxes[i]
l2, r2, t2_, b2, s2 = boxes[j]
if r1 > l2 + 2 and r2 > l1 + 2 and b1 > t2_ + 2 and b2 > t1 + 2:
raise AssertionError(f"etichete suprapuse: {s1!r} / {s2!r}")
# 8. stil: fără umbre / gradienți, fontul cerut
assert "filter=" not in svg and "gradient" not in svg, "fără umbre/gradienti"
assert 'font-family="system-ui, sans-serif"' in svg, "lipsește fontul cerut"
# 8b. SVG-ul e XML valid, cu o singură rădăcină <svg> (bug prins la render:
# un rect scris înaintea rădăcinii => „extra content at the end”)
import xml.etree.ElementTree as _ET
assert svg.lstrip().startswith("<svg "), "rădăcina nu e <svg>"
_root = _ET.fromstring(svg)
assert _root.tag.endswith("svg"), f"rădăcină neașteptată: {_root.tag}"
# 9. fără cifre de RS inventate: niciun text nu conține numere zecimale sau
# procente (axele folosesc doar „0”, ca prag, nu valori de RS)
import re
for _x, _y, _s, _size, _weight, _fill, _anchor in TEXTS:
assert not re.search(r"\d+[.,]\d+", _s), f"cifră de RS inventată: {_s!r}"
assert "%" not in _s, f"procent inventat: {_s!r}"
assert "score" not in svg and "scor" not in svg
OUT = Path(__file__).with_suffix(".svg")
OUT.write_text(svg, encoding="utf-8")
print(f"scris {OUT} ({len(svg)} bytes, {len(TEXTS)} etichete, "
f"{len(quads)} cadrane, {W}x{H}), verificare OK")

View File

@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 720" width="700" height="720" font-family="system-ui, sans-serif" role="img" aria-label="Matricea 2x2 a relative strength: cadranele 1 si 2 sunt pentru long, cadranele 3 si 4 pentru short; cadranul 4 nu se tranzactioneaza pe orb">
<rect x="0" y="0" width="700" height="720" fill="#ffffff"/>
<rect x="130.0" y="134.0" width="440.0" height="280.0" fill="#ffffff" stroke="#777777" stroke-width="1.0"/>
<line x1="130.0" y1="274.0" x2="570.0" y2="274.0" stroke="#777777" stroke-width="1.0" stroke-dasharray="4 4"/>
<line x1="350.0" y1="134.0" x2="350.0" y2="414.0" stroke="#777777" stroke-width="1.0" stroke-dasharray="4 4"/>
<rect x="130.0" y="134.0" width="218.0" height="138.0" fill="#fdeeee" stroke="#c00" stroke-width="1.6"/>
<rect x="352.0" y="134.0" width="218.0" height="138.0" fill="#eef7f0" stroke="#188a3e" stroke-width="1.6"/>
<rect x="130.0" y="276.0" width="218.0" height="138.0" fill="#fdeeee" stroke="#c00" stroke-width="1.6" stroke-dasharray="6 4"/>
<rect x="352.0" y="276.0" width="218.0" height="138.0" fill="#eef7f0" stroke="#188a3e" stroke-width="1.6"/>
<rect x="40.0" y="460.0" width="620.0" height="196.0" fill="#fdf0e0" stroke="#b06000" stroke-width="1.6"/>
<text x="350.0" y="34.0" font-size="20" font-weight="bold" fill="#000000" text-anchor="middle">Relative strength — cele 4 cadrane</text>
<text x="350.0" y="58.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">RS = cât de bine se mișcă simbolul față de benchmark (ex. SPY)</text>
<text x="350.0" y="80.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">Axa verticală: histograma RS față de 0 (peste / sub 0)</text>
<text x="350.0" y="98.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">Axa orizontală: histograma RS față de MA (peste / sub MA)</text>
<text x="241.0" y="122.0" font-size="14" font-weight="bold" fill="#b06000" text-anchor="middle">sub MA</text>
<text x="461.0" y="122.0" font-size="14" font-weight="bold" fill="#1a5fb4" text-anchor="middle">peste MA</text>
<text x="122.0" y="203.0" font-size="14" font-weight="bold" fill="#188a3e" text-anchor="end">peste 0</text>
<text x="122.0" y="345.0" font-size="14" font-weight="bold" fill="#c00" text-anchor="end">sub 0</text>
<text x="239.0" y="164.0" font-size="16" font-weight="bold" fill="#000000" text-anchor="middle">Cadran 3</text>
<text x="239.0" y="186.0" font-size="14" font-weight="bold" fill="#c00" text-anchor="middle">înrăutățire</text>
<text x="239.0" y="214.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">peste 0, dar sub MA</text>
<text x="239.0" y="246.0" font-size="16" font-weight="bold" fill="#c00" text-anchor="middle">SHORT</text>
<text x="461.0" y="164.0" font-size="16" font-weight="bold" fill="#000000" text-anchor="middle">Cadran 1</text>
<text x="461.0" y="186.0" font-size="14" font-weight="bold" fill="#188a3e" text-anchor="middle">supraperformanță</text>
<text x="461.0" y="214.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">peste 0 și peste MA</text>
<text x="461.0" y="246.0" font-size="16" font-weight="bold" fill="#188a3e" text-anchor="middle">LONG</text>
<text x="239.0" y="302.0" font-size="16" font-weight="bold" fill="#000000" text-anchor="middle">Cadran 4</text>
<text x="239.0" y="324.0" font-size="14" font-weight="bold" fill="#c00" text-anchor="middle">subperformanță</text>
<text x="239.0" y="350.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">sub 0 și sub MA</text>
<text x="239.0" y="376.0" font-size="15" font-weight="bold" fill="#c00" text-anchor="middle">SHORT</text>
<text x="239.0" y="398.0" font-size="13" font-weight="normal" fill="#c00" text-anchor="middle">stai pe margine</text>
<text x="461.0" y="302.0" font-size="16" font-weight="bold" fill="#000000" text-anchor="middle">Cadran 2</text>
<text x="461.0" y="324.0" font-size="14" font-weight="bold" fill="#188a3e" text-anchor="middle">îmbunătățire</text>
<text x="461.0" y="350.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">sub 0, dar peste MA</text>
<text x="461.0" y="376.0" font-size="15" font-weight="bold" fill="#188a3e" text-anchor="middle">LONG</text>
<text x="461.0" y="398.0" font-size="13" font-weight="normal" fill="#188a3e" text-anchor="middle">zona de cumpărat</text>
<text x="350.0" y="442.0" font-size="14" font-weight="bold" fill="#000000" text-anchor="middle">Long = cadranele 1-2 · Short = cadranele 3-4</text>
<text x="60.0" y="488.0" font-size="15" font-weight="bold" fill="#c00" text-anchor="start">Avertisment — cadranul 4 nu se tranzacționează pe orb</text>
<text x="60.0" y="516.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="start">„Cadranul 4 nu se tranzacționează pe orb: aștepți trecerea în 2 sau 1,</text>
<text x="60.0" y="536.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="start">ori un MP4-1 / MBT-10 long ca semn de schimbare.”</text>
<text x="60.0" y="566.0" font-size="13" font-weight="bold" fill="#000000" text-anchor="start">„Un setup de bottom cu RS fără îmbunătățire are șanse mai mici.”</text>
<text x="60.0" y="596.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="start">Notă: transcrierea descrierii cadranului 4 are o eroare (îl descrie ca pe</text>
<text x="60.0" y="616.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="start">cadranul 3); definiția de mai sus e completarea sumarului —</text>
<text x="60.0" y="636.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="start">03_MODUL_2/06_indicatorul_de_performanta.md:41-48.</text>
<text x="350.0" y="686.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">Surse: 08_ANALIZA_ZILNICA/11_selectia_simbolului.md:486-499, :501-508;</text>
<text x="350.0" y="704.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">03_MODUL_2/06_indicatorul_de_performanta.md:31-39.</text>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1,208 @@
"""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[^>]*>", svg)
assert m_svg, "tag <svg> lipsa"
svg = svg.replace(
m_svg.group(0),
m_svg.group(0) + f'\n <text x="24" y="16" fill="#333" style="font-weight:bold">{title}</text>',
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'<text x="24" y="(\d+)" fill="#555">' + re.escape(caption) + r"</text>\s*</svg>", svg)
assert m, "caption-ul nu a fost gasit in SVG"
y0 = int(m.group(1))
wrapped = "\n ".join(f'<text x="24" y="{y0 + 17 * i}" fill="#555">{ln}</text>'
for i, ln in enumerate(cap_lines))
svg = svg[:m.start()] + wrapped + "\n</svg>"
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'<text x="([\d.]+)" y="([\d.]+)"([^>]*)>(.*?)</text>', 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 "<svg" in svg and "\n\n" not in svg, "linie goala in SVG"
OUT.write_text(svg)
print(f"scris {OUT.name}: {len(cds)} candele, doji {cds[12].c:.2f}, "
f"gap open {cds[10].o:.2f} (RVOL {VOLS[10]/media_vol:.2f}), "
f"intrare 78.10, SL 82.00 = 3.90, TP 72.00 = 6.10 ({reward/risc:.2f}R), "
f"ultimul close {cds[-1].c:.2f}")

View File

@@ -0,0 +1,113 @@
<svg viewBox="0 0 640 433" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;font:12px sans-serif">
<text x="24" y="16" fill="#333" style="font-weight:bold">HIG — daily (Sell Doji + Bear Reversal, RVOL 200%)</text>
<line x1="24" y1="34.0" x2="626" y2="34.0" stroke="#777" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="30" y="28.0" fill="#777">rezistenta 84.50</text>
<line x1="24" y1="74.0" x2="626" y2="74.0" stroke="#c00" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="612" y="68.0" fill="#c00" text-anchor="end">SL 82.00</text>
<line x1="24" y1="136.4" x2="626" y2="136.4" stroke="#1a5fb4" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="30" y="130.4" fill="#1a5fb4">intrare short 78.10</text>
<line x1="24" y1="234.0" x2="626" y2="234.0" stroke="#188a3e" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="30" y="228.0" fill="#188a3e">TP 72.00</text>
<polyline points="329.0,107.0 344.0,110.5 359.0,114.3 374.0,119.0 389.0,124.7 404.0,129.8 419.0,136.1 434.0,143.4 449.0,151.7 464.0,160.8 479.0,170.7" fill="none" stroke="#b06000" stroke-width="1.5" stroke-dasharray="4 3"/>
<text x="485" y="165" fill="#b06000">SMA-20</text>
<line x1="44.0" y1="98.8" x2="44.0" y2="115.6" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="39.5" y="106.0" width="9" height="3.2" fill="#188a3e"/>
<line x1="59.0" y1="90.0" x2="59.0" y2="113.2" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="54.5" y="96.4" width="9" height="9.6" fill="#188a3e"/>
<line x1="74.0" y1="78.8" x2="74.0" y2="102.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="69.5" y="86.8" width="9" height="9.6" fill="#188a3e"/>
<line x1="89.0" y1="66.8" x2="89.0" y2="94.0" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="84.5" y="75.6" width="9" height="11.2" fill="#188a3e"/>
<line x1="104.0" y1="68.4" x2="104.0" y2="90.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="99.5" y="75.6" width="9" height="6.4" fill="#c00"/>
<line x1="119.0" y1="61.2" x2="119.0" y2="88.4" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="114.5" y="69.2" width="9" height="12.8" fill="#188a3e"/>
<line x1="134.0" y1="52.4" x2="134.0" y2="77.2" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="129.5" y="59.6" width="9" height="9.6" fill="#188a3e"/>
<line x1="149.0" y1="42.0" x2="149.0" y2="66.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="144.5" y="48.4" width="9" height="11.2" fill="#188a3e"/>
<line x1="164.0" y1="36.4" x2="164.0" y2="54.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="159.5" y="42.0" width="9" height="6.4" fill="#188a3e"/>
<line x1="179.0" y1="34.0" x2="179.0" y2="49.2" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="174.5" y="38.8" width="9" height="3.2" fill="#188a3e"/>
<line x1="194.0" y1="112.4" x2="194.0" y2="130.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="189.5" y="118.8" width="9" height="4.0" fill="#c00"/>
<line x1="209.0" y1="115.6" x2="209.0" y2="132.4" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="204.5" y="122.8" width="9" height="3.2" fill="#c00"/>
<line x1="224.0" y1="120.4" x2="224.0" y2="134.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="219.5" y="126.0" width="9" height="2.4" fill="#c00"/>
<line x1="239.0" y1="122.0" x2="239.0" y2="143.6" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="234.5" y="128.4" width="9" height="8.8" fill="#c00"/>
<line x1="254.0" y1="130.0" x2="254.0" y2="152.4" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="249.5" y="137.2" width="9" height="8.8" fill="#c00"/>
<line x1="269.0" y1="134.0" x2="269.0" y2="152.4" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="264.5" y="141.2" width="9" height="4.8" fill="#188a3e"/>
<line x1="284.0" y1="134.0" x2="284.0" y2="157.2" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="279.5" y="141.2" width="9" height="9.6" fill="#c00"/>
<line x1="299.0" y1="143.6" x2="299.0" y2="166.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="294.5" y="150.8" width="9" height="9.6" fill="#c00"/>
<line x1="314.0" y1="148.4" x2="314.0" y2="166.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="309.5" y="155.6" width="9" height="4.8" fill="#188a3e"/>
<line x1="329.0" y1="148.4" x2="329.0" y2="173.2" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="324.5" y="155.6" width="9" height="11.2" fill="#c00"/>
<line x1="344.0" y1="159.6" x2="344.0" y2="182.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="339.5" y="166.8" width="9" height="9.6" fill="#c00"/>
<line x1="359.0" y1="164.4" x2="359.0" y2="182.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="354.5" y="171.6" width="9" height="4.8" fill="#188a3e"/>
<line x1="374.0" y1="164.4" x2="374.0" y2="187.6" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="369.5" y="171.6" width="9" height="9.6" fill="#c00"/>
<line x1="389.0" y1="174.0" x2="389.0" y2="195.6" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="384.5" y="181.2" width="9" height="8.0" fill="#c00"/>
<line x1="404.0" y1="177.2" x2="404.0" y2="195.6" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="399.5" y="184.4" width="9" height="4.8" fill="#188a3e"/>
<line x1="419.0" y1="177.2" x2="419.0" y2="202.0" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="414.5" y="184.4" width="9" height="11.2" fill="#c00"/>
<line x1="434.0" y1="188.4" x2="434.0" y2="211.6" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="429.5" y="195.6" width="9" height="9.6" fill="#c00"/>
<line x1="449.0" y1="198.0" x2="449.0" y2="221.2" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="444.5" y="205.2" width="9" height="9.6" fill="#c00"/>
<line x1="464.0" y1="207.6" x2="464.0" y2="230.8" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="459.5" y="214.8" width="9" height="9.6" fill="#c00"/>
<line x1="479.0" y1="216.4" x2="479.0" y2="244.4" stroke="#1a1a1a" stroke-width="1.2"/>
<rect x="474.5" y="224.4" width="9" height="11.2" fill="#c00"/>
<line x1="24" y1="340.4" x2="626" y2="340.4" stroke="#888"/>
<rect x="39.5" y="309.6" width="9" height="30.8" fill="#188a3e" opacity="0.55"/>
<rect x="54.5" y="311.7" width="9" height="28.7" fill="#188a3e" opacity="0.55"/>
<rect x="69.5" y="308.9" width="9" height="31.5" fill="#188a3e" opacity="0.55"/>
<rect x="84.5" y="305.4" width="9" height="35.0" fill="#188a3e" opacity="0.55"/>
<rect x="99.5" y="310.3" width="9" height="30.1" fill="#c00" opacity="0.55"/>
<rect x="114.5" y="302.6" width="9" height="37.8" fill="#188a3e" opacity="0.55"/>
<rect x="129.5" y="304.0" width="9" height="36.4" fill="#188a3e" opacity="0.55"/>
<rect x="144.5" y="298.4" width="9" height="42.0" fill="#188a3e" opacity="0.55"/>
<rect x="159.5" y="306.8" width="9" height="33.6" fill="#188a3e" opacity="0.55"/>
<rect x="174.5" y="305.4" width="9" height="35.0" fill="#188a3e" opacity="0.55"/>
<rect x="189.5" y="270.4" width="9" height="70.0" fill="#c00" opacity="0.55"/>
<rect x="204.5" y="294.9" width="9" height="45.5" fill="#c00" opacity="0.55"/>
<rect x="219.5" y="305.4" width="9" height="35.0" fill="#c00" opacity="0.55"/>
<rect x="234.5" y="287.9" width="9" height="52.5" fill="#c00" opacity="0.55"/>
<rect x="249.5" y="304.7" width="9" height="35.7" fill="#c00" opacity="0.55"/>
<rect x="264.5" y="309.6" width="9" height="30.8" fill="#188a3e" opacity="0.55"/>
<rect x="279.5" y="302.6" width="9" height="37.8" fill="#c00" opacity="0.55"/>
<rect x="294.5" y="306.8" width="9" height="33.6" fill="#c00" opacity="0.55"/>
<rect x="309.5" y="311.0" width="9" height="29.4" fill="#188a3e" opacity="0.55"/>
<rect x="324.5" y="305.4" width="9" height="35.0" fill="#c00" opacity="0.55"/>
<rect x="339.5" y="308.9" width="9" height="31.5" fill="#c00" opacity="0.55"/>
<rect x="354.5" y="313.1" width="9" height="27.3" fill="#188a3e" opacity="0.55"/>
<rect x="369.5" y="306.1" width="9" height="34.3" fill="#c00" opacity="0.55"/>
<rect x="384.5" y="310.3" width="9" height="30.1" fill="#c00" opacity="0.55"/>
<rect x="399.5" y="314.5" width="9" height="25.9" fill="#188a3e" opacity="0.55"/>
<rect x="414.5" y="308.2" width="9" height="32.2" fill="#c00" opacity="0.55"/>
<rect x="429.5" y="311.7" width="9" height="28.7" fill="#c00" opacity="0.55"/>
<rect x="444.5" y="304.0" width="9" height="36.4" fill="#c00" opacity="0.55"/>
<rect x="459.5" y="313.8" width="9" height="26.6" fill="#c00" opacity="0.55"/>
<rect x="474.5" y="309.6" width="9" height="30.8" fill="#c00" opacity="0.55"/>
<text x="24" y="264.4" fill="#555">volum</text>
<text x="194.0" y="86.4" fill="#333" text-anchor="middle" style="font-weight:bold">gap down, open 79.20</text>
<text x="224.0" y="106.4" fill="#c00" text-anchor="middle" style="font-weight:bold">sell doji close 78.60</text>
<text x="239.0" y="189.6" fill="#188a3e" text-anchor="middle" style="font-weight:bold">intrare short 78.10</text>
<text x="194.0" y="162.8" fill="#c00" text-anchor="middle" style="font-weight:bold">RVOL 200%</text>
<text x="24" y="370" fill="#555">RVOL 200% si simbolul sunt reale (04_MODUL_3/01:196-199). Toate</text>
<text x="24" y="387" fill="#555">nivelurile de preț (79.20 / 78.60 / 78.10 / 82.00 / 72.00) sunt</text>
<text x="24" y="404" fill="#555">construite in sumarul 04_MODUL_3/01:293-296, nu date de curs;</text>
<text x="24" y="421" fill="#555">structura barelor e construita, la scara.</text>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -0,0 +1,282 @@
#!/usr/bin/env python3
"""Fluxul de selecție — palnia de filtrare de la scanner la lista scurtă.
Diagramă conceptuală (NU grafic de preț): palnie cu 8 trepte care se
îngustează de sus în jos, apoi lista scurtă; cutie separată de note laterale.
Pentru `strategii/04_selectia.md`, Nivel 1, §1.2 „Fluxul de filtrare, pas cu
pas". Citește specificația `gen/strategii/selectia_funnel.spec.txt` și scrie
`gen/strategii/selectia_funnel.svg`.
Nu folosește `svg_candles` / `pchart`: SVG-ul e construit ca string, doar
stdlib.
Fiecare treaptă are titlul, subtitlul cu pragul și trimiterea (sursa).
Toate cifrele din desen (pragurile, „max ~300 simboluri”, „top 10”) sunt
preluate textual din specificație; nu se inventează valori de RS sau de
sectoare.
Rulare: .venv/bin/python gen/strategii/selectia_funnel.py
"""
import html
import re
from pathlib import Path
W = 700
H = 1024
BLACK = "#000000"
GRAY = "#777777"
RED = "#c00"
GREEN = "#188a3e"
BLUE = "#1a5fb4"
ORANGE = "#b06000"
STEP_FILL = ("#eef3fb", "#e4edf9") # alternanță foarte fină între trepte
GREEN_BG = "#eef7f0"
NOTE_BG = "#fdf0e0"
# ---------------------------------------------------------------- geometrie
CX = W / 2.0 # 350: axa palniei
TOPW = 610.0 # lățimea gurii palniei
NARROW = 24.0 # cât se strânge pe fiecare treaptă
Y0 = 100.0
HSTEP = 76.0
LIST_W, LIST_Y, LIST_H = 410.0, 722.0, 62.0
NOTE = {"x": 40.0, "y": 802.0, "w": 620.0, "h": 200.0}
# ---------------------------------------------------------------- conținut
# (titlu, subtitlu, [linii de sursă])
STEPS = [
("Scanner — multe semnale",
"zeci sau sute de semnale",
["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:631-637"]),
("Filtre de bază",
"close > 30-40 $; volum mediu 3 luni > 1M (min 500k)",
["09_QA/28_2025-05-27.md:372-380;",
"03_MODUL_2/11_construire_scanere_sf_byd_sld.md:70-73"]),
("Fundamental",
"ATM score peste 7-8; 12-13 = spuma; F-score 6-9",
["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:199-221"]),
("Sectorul care domină",
"cele 11 sectoare S&P, sortate cadran 1 + valoarea RS, săptămânal",
["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:639-717"]),
("Doar suport / pivot, după downtrend",
"suport monthly, quarterly, anual sau pivot; proximitate ≤ 3%",
["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:719-728, 787-803"]),
("Relative strength: cadran 1-2",
"doar cadranele 1 și 2, RS pozitiv",
["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:730-739"]),
("Relative volume peste 100%",
"candela de volum / media ultimelor 10 candele",
["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:741-747"]),
("Stele (Trinity)",
"RS + Power + volum, cadran 1",
["08_ANALIZA_ZILNICA/11_selectia_simbolului.md:776-785"]),
]
LIST = ("Listă scurtă",
"câteva idei pe care le urmărești",
"08_ANALIZA_ZILNICA/11_selectia_simbolului.md:42-50")
TITLE = "Fluxul de selecție — de la scanner la lista scurtă"
SUBTITLE = ("selecția se face înainte de semnalul tehnic; "
"ea nu spune CÂND intri")
CAPTION = "Palnia se îngustează de sus în jos: 8 filtre, apoi lista scurtă."
NOTE_TITLE = "Note laterale"
NOTE1 = ["După corecție masivă: doji / hammer la suporturi mari",
"(monthly, quarterly, anual); scanner de apropiere de suport",
"până la 3% (4% e cam departe)."]
NOTE1_SRC = "08_ANALIZA_ZILNICA/11_selectia_simbolului.md:787-803"
NOTE2 = ["Radarscreen (max ~300 simboluri): repetă fluxul — sectoare top 10,",
"RS, apoi 3 stele."]
NOTE2_SRC = "08_ANALIZA_ZILNICA/11_selectia_simbolului.md:815-825"
# ---------------------------------------------------------------- colect
TEXTS = [] # (x, y, s, size, weight, fill, anchor)
SHAPES = [] # (x, y, w, h, tag) bbox-uri pentru verificare
WIDTHS = [] # (w_sus, w_jos) pe fiecare treaptă, pentru verificat conectarea
def add(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"):
TEXTS.append((x, y, s, size, weight, fill, anchor))
return s
def est_w(s, size, weight):
factor = 0.62 if weight == "bold" else 0.56
return len(s) * size * factor
def txt(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"):
return (f'<text x="{x:.1f}" y="{y:.1f}" font-size="{size}" '
f'font-weight="{weight}" fill="{fill}" text-anchor="{anchor}">'
f'{html.escape(s, quote=False)}</text>')
def rect(x, y, w, h, tag, fill="#ffffff", stroke=GRAY, sw=1.5, rx=0):
SHAPES.append((x, y, w, h, tag))
r = f' rx="{rx}"' if rx else ""
return (f'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}" '
f'fill="{fill}" stroke="{stroke}" stroke-width="{sw}"{r}/>')
def trapezoid(y, wt, wb, h, index):
"""Treaptă de palnie: muchia de sus lată wt, cea de jos wb."""
xtl, xtr = CX - wt / 2.0, CX + wt / 2.0
xbl, xbr = CX - wb / 2.0, CX + wb / 2.0
SHAPES.append((xbl, y, wb, h, f"step{index}"))
WIDTHS.append((wt, wb))
d = (f"M {xtl:.1f} {y:.1f} L {xtr:.1f} {y:.1f} "
f"L {xbr:.1f} {y + h:.1f} L {xbl:.1f} {y + h:.1f} Z")
fill = STEP_FILL[index % 2]
return (f'<path d="{d}" fill="{fill}" stroke="{BLUE}" '
f'stroke-width="1.2"/>')
# =============================================================== CONȚINUT
p = []
# ---- titlu / subtitlu / caption
add(W / 2, 34, TITLE, 20, "bold")
add(W / 2, 57, SUBTITLE, 13, "normal", GRAY)
add(W / 2, 79, CAPTION, 13, "normal", GRAY)
# ---- palnia: 8 trepte
for i, (titlu, subtitlu, surse) in enumerate(STEPS):
y = Y0 + i * HSTEP
wt = TOPW - i * NARROW
wb = TOPW - (i + 1) * NARROW
p.append(trapezoid(y, wt, wb, HSTEP, i))
add(CX, y + 25, f"{i + 1}. {titlu}", 14.5, "bold", BLACK)
add(CX, y + 45, subtitlu, 13, "normal", BLACK)
for k, src in enumerate(surse):
add(CX, y + 59 + k * 14, src, 13, "normal", GRAY)
# ---- lista scurtă (vârful palniei, cutie separată)
p.append(rect(CX - LIST_W / 2, LIST_Y, LIST_W, LIST_H, "list",
fill=GREEN_BG, stroke=GREEN, sw=1.8, rx=8))
add(CX, LIST_Y + 24, LIST[0], 16, "bold", GREEN)
add(CX, LIST_Y + 43, LIST[1], 13, "normal", BLACK)
add(CX, LIST_Y + 57, LIST[2], 13, "normal", GRAY)
# ---- cutia de note laterale
p.append(rect(NOTE["x"], NOTE["y"], NOTE["w"], NOTE["h"], "note",
fill=NOTE_BG, stroke=ORANGE, sw=1.6, rx=8))
nx = NOTE["x"] + 20
add(nx, NOTE["y"] + 28, NOTE_TITLE, 15, "bold", ORANGE, "start")
add(nx, NOTE["y"] + 56, NOTE1[0], 13, "normal", BLACK, "start")
add(nx, NOTE["y"] + 75, NOTE1[1], 13, "normal", BLACK, "start")
add(nx, NOTE["y"] + 94, NOTE1[2], 13, "normal", BLACK, "start")
add(nx, NOTE["y"] + 115, NOTE1_SRC, 13, "normal", GRAY, "start")
add(nx, NOTE["y"] + 150, NOTE2[0], 13, "normal", BLACK, "start")
add(nx, NOTE["y"] + 169, NOTE2[1], 13, "normal", BLACK, "start")
add(nx, NOTE["y"] + 190, NOTE2_SRC, 13, "normal", GRAY, "start")
# =============================================================== EMITERE
head = (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" '
f'width="{W}" height="{H}" font-family="system-ui, sans-serif" '
f'role="img" aria-label="Palnia de selectie: 8 trepte de filtrare de la '
f'scanner la lista scurta, fiecare cu pragul si sursa ei, plus note '
f'laterale despre suporturi si Radarscreen">')
body = [head, f'<rect x="0" y="0" width="{W}" height="{H}" fill="#ffffff"/>']
body += p
for x, y, s, size, weight, fill, anchor in TEXTS:
body.append(txt(x, y, s, size, weight, fill, anchor))
body.append("</svg>")
svg = "\n".join(body) + "\n"
# =============================================================== VERIFICARE
# 1. palnia are exact 8 trepte, conectate și tot mai înguste
assert len(STEPS) == 8, f"aștept 8 trepte, am {len(STEPS)}"
traps = [s for s in SHAPES if s[4].startswith("step")]
assert len(traps) == 8, f"aștept 8 trapezoide, am {len(traps)}"
assert len(WIDTHS) == 8
for i, (wt, wb) in enumerate(WIDTHS):
assert wt > wb, f"treapta {i} nu se îngustează"
if i:
assert abs(WIDTHS[i][0] - WIDTHS[i - 1][1]) < 1e-9, \
f"treapta {i} nu continuă muchia treptei {i - 1}"
assert WIDTHS[-1][1] < WIDTHS[0][0], "palnia nu se îngustează de sus în jos"
assert LIST_W < traps[-1][2], "lista scurtă nu e mai îngustă decât vârful palniei"
# 2. toate titlurile și subtitlurile din spec sunt desenate
for i, (titlu, subtitlu, surse) in enumerate(STEPS):
assert any(t[2] == f"{i + 1}. {titlu}" for t in TEXTS), \
f"lipsește titlul treptei {i + 1}: {titlu!r}"
assert any(t[2] == subtitlu for t in TEXTS), \
f"lipsește subtitlul treptei {i + 1}: {subtitlu!r}"
for src in surse:
assert src in svg, f"lipsește sursa treptei {i + 1}: {src!r}"
assert any(t[2] == LIST[0] for t in TEXTS), "lipsește «Listă scurtă»"
assert "câteva idei pe care le urmărești" in svg
# 3. titlul, subtitlul și notele din spec sunt exact cele desenate
assert TITLE in svg, "lipsește titlul din spec"
assert SUBTITLE in svg, "lipsește subtitlul din spec"
for line in NOTE1 + [NOTE1_SRC] + NOTE2 + [NOTE2_SRC]:
assert line in svg, f"lipsește din note: {line!r}"
# 4. fiecare textul încape în lățimea treptei lui (muchia de jos, cea îngustă)
for i in range(8):
ytop = Y0 + i * HSTEP
ybot = ytop + HSTEP
wb = TOPW - (i + 1) * NARROW
for x, y, s, size, weight, fill, anchor in TEXTS:
if ytop <= y <= ybot:
w = est_w(s, size, weight)
assert w <= wb - 14, f"treapta {i + 1}: text prea lat: {s!r} ({w:.0f})"
# textul din lista scurtă și din note încape în cutiile lor
for x, y, s, size, weight, fill, anchor in TEXTS:
if LIST_Y <= y <= LIST_Y + LIST_H:
assert est_w(s, size, weight) <= LIST_W - 16, f"listă: text prea lat: {s!r}"
if NOTE["y"] <= y <= NOTE["y"] + NOTE["h"]:
assert est_w(s, size, weight) <= NOTE["w"] - 40, f"notă: text prea lat: {s!r}"
# 5. toate etichetele în panză + fără suprapuneri
boxes = []
for x, y, s, size, weight, fill, anchor in TEXTS:
assert size >= 13, f"font sub 13px: {s!r}"
w = est_w(s, size, weight)
if anchor == "end":
left = x - w
elif anchor == "start":
left = x
else:
left = x - w / 2.0
right = left + w
top, bottom = y - size * 0.78, y + size * 0.22
assert -2 <= left and right <= W + 2, f"text în afara panzei orizontal: {s!r}"
assert 0 <= top and bottom <= H, f"text în afara panzei vertical: {s!r}"
boxes.append((left, right, top, bottom, s))
for i in range(len(boxes)):
for j in range(i + 1, len(boxes)):
l1, r1, t1, b1, s1 = boxes[i]
l2, r2, t2, b2, s2 = boxes[j]
if r1 > l2 + 2 and r2 > l1 + 2 and b1 > t2 + 2 and b2 > t1 + 2:
raise AssertionError(f"etichete suprapuse: {s1!r} / {s2!r}")
# 6. cutiile stau una sub alta, în ordinea palnie -> listă -> note
assert LIST_Y >= Y0 + 8 * HSTEP, "lista scurtă se suprapune cu palnia"
assert NOTE["y"] >= LIST_Y + LIST_H, "notele se suprapun cu lista scurtă"
assert NOTE["y"] + NOTE["h"] <= H, "notele ies din panză"
# 7. stil: fără umbre / gradienți, fontul cerut; fără prețuri inventate
assert "filter=" not in svg and "gradient" not in svg, "fără umbre/gradienti"
assert 'font-family="system-ui, sans-serif"' in svg, "lipsește fontul cerut"
assert "<circle" not in svg and "<line" not in svg, "doar palnie, fără axe"
# 8. singurele cifre din desen (fără referințele de sursă) sunt pragurile
# din specificație
content = [t[2] for t in TEXTS if ".md:" not in t[2]]
spec_numbers = {"1", "2", "3", "4", "5", "6", "7", "8", "9",
"30", "40", "500", "11", "12", "13", "100", "10", "300"}
nums = set(re.findall(r"\d+", " ".join(content)))
assert nums <= spec_numbers, f"cifre neașteptate în desen: {nums - spec_numbers}"
OUT = Path(__file__).with_suffix(".svg")
OUT.write_text(svg, encoding="utf-8")
print(f"scris {OUT} ({len(svg)} bytes, {len(TEXTS)} etichete, "
f"{len(traps)} trepte, {W}x{H}), verificare OK")

View File

@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 1024" width="700" height="1024" font-family="system-ui, sans-serif" role="img" aria-label="Palnia de selectie: 8 trepte de filtrare de la scanner la lista scurta, fiecare cu pragul si sursa ei, plus note laterale despre suporturi si Radarscreen">
<rect x="0" y="0" width="700" height="1024" fill="#ffffff"/>
<path d="M 45.0 100.0 L 655.0 100.0 L 643.0 176.0 L 57.0 176.0 Z" fill="#eef3fb" stroke="#1a5fb4" stroke-width="1.2"/>
<path d="M 57.0 176.0 L 643.0 176.0 L 631.0 252.0 L 69.0 252.0 Z" fill="#e4edf9" stroke="#1a5fb4" stroke-width="1.2"/>
<path d="M 69.0 252.0 L 631.0 252.0 L 619.0 328.0 L 81.0 328.0 Z" fill="#eef3fb" stroke="#1a5fb4" stroke-width="1.2"/>
<path d="M 81.0 328.0 L 619.0 328.0 L 607.0 404.0 L 93.0 404.0 Z" fill="#e4edf9" stroke="#1a5fb4" stroke-width="1.2"/>
<path d="M 93.0 404.0 L 607.0 404.0 L 595.0 480.0 L 105.0 480.0 Z" fill="#eef3fb" stroke="#1a5fb4" stroke-width="1.2"/>
<path d="M 105.0 480.0 L 595.0 480.0 L 583.0 556.0 L 117.0 556.0 Z" fill="#e4edf9" stroke="#1a5fb4" stroke-width="1.2"/>
<path d="M 117.0 556.0 L 583.0 556.0 L 571.0 632.0 L 129.0 632.0 Z" fill="#eef3fb" stroke="#1a5fb4" stroke-width="1.2"/>
<path d="M 129.0 632.0 L 571.0 632.0 L 559.0 708.0 L 141.0 708.0 Z" fill="#e4edf9" stroke="#1a5fb4" stroke-width="1.2"/>
<rect x="145.0" y="722.0" width="410.0" height="62.0" fill="#eef7f0" stroke="#188a3e" stroke-width="1.8" rx="8"/>
<rect x="40.0" y="802.0" width="620.0" height="200.0" fill="#fdf0e0" stroke="#b06000" stroke-width="1.6" rx="8"/>
<text x="350.0" y="34.0" font-size="20" font-weight="bold" fill="#000000" text-anchor="middle">Fluxul de selecție — de la scanner la lista scurtă</text>
<text x="350.0" y="57.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">selecția se face înainte de semnalul tehnic; ea nu spune CÂND intri</text>
<text x="350.0" y="79.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">Palnia se îngustează de sus în jos: 8 filtre, apoi lista scurtă.</text>
<text x="350.0" y="125.0" font-size="14.5" font-weight="bold" fill="#000000" text-anchor="middle">1. Scanner — multe semnale</text>
<text x="350.0" y="145.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">zeci sau sute de semnale</text>
<text x="350.0" y="159.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">08_ANALIZA_ZILNICA/11_selectia_simbolului.md:631-637</text>
<text x="350.0" y="201.0" font-size="14.5" font-weight="bold" fill="#000000" text-anchor="middle">2. Filtre de bază</text>
<text x="350.0" y="221.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">close &gt; 30-40 $; volum mediu 3 luni &gt; 1M (min 500k)</text>
<text x="350.0" y="235.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">09_QA/28_2025-05-27.md:372-380;</text>
<text x="350.0" y="249.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">03_MODUL_2/11_construire_scanere_sf_byd_sld.md:70-73</text>
<text x="350.0" y="277.0" font-size="14.5" font-weight="bold" fill="#000000" text-anchor="middle">3. Fundamental</text>
<text x="350.0" y="297.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">ATM score peste 7-8; 12-13 = spuma; F-score 6-9</text>
<text x="350.0" y="311.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">08_ANALIZA_ZILNICA/11_selectia_simbolului.md:199-221</text>
<text x="350.0" y="353.0" font-size="14.5" font-weight="bold" fill="#000000" text-anchor="middle">4. Sectorul care domină</text>
<text x="350.0" y="373.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">cele 11 sectoare S&amp;P, sortate cadran 1 + valoarea RS, săptămânal</text>
<text x="350.0" y="387.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">08_ANALIZA_ZILNICA/11_selectia_simbolului.md:639-717</text>
<text x="350.0" y="429.0" font-size="14.5" font-weight="bold" fill="#000000" text-anchor="middle">5. Doar suport / pivot, după downtrend</text>
<text x="350.0" y="449.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">suport monthly, quarterly, anual sau pivot; proximitate ≤ 3%</text>
<text x="350.0" y="463.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">08_ANALIZA_ZILNICA/11_selectia_simbolului.md:719-728, 787-803</text>
<text x="350.0" y="505.0" font-size="14.5" font-weight="bold" fill="#000000" text-anchor="middle">6. Relative strength: cadran 1-2</text>
<text x="350.0" y="525.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">doar cadranele 1 și 2, RS pozitiv</text>
<text x="350.0" y="539.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">08_ANALIZA_ZILNICA/11_selectia_simbolului.md:730-739</text>
<text x="350.0" y="581.0" font-size="14.5" font-weight="bold" fill="#000000" text-anchor="middle">7. Relative volume peste 100%</text>
<text x="350.0" y="601.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">candela de volum / media ultimelor 10 candele</text>
<text x="350.0" y="615.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">08_ANALIZA_ZILNICA/11_selectia_simbolului.md:741-747</text>
<text x="350.0" y="657.0" font-size="14.5" font-weight="bold" fill="#000000" text-anchor="middle">8. Stele (Trinity)</text>
<text x="350.0" y="677.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">RS + Power + volum, cadran 1</text>
<text x="350.0" y="691.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">08_ANALIZA_ZILNICA/11_selectia_simbolului.md:776-785</text>
<text x="350.0" y="746.0" font-size="16" font-weight="bold" fill="#188a3e" text-anchor="middle">Listă scurtă</text>
<text x="350.0" y="765.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">câteva idei pe care le urmărești</text>
<text x="350.0" y="779.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">08_ANALIZA_ZILNICA/11_selectia_simbolului.md:42-50</text>
<text x="60.0" y="830.0" font-size="15" font-weight="bold" fill="#b06000" text-anchor="start">Note laterale</text>
<text x="60.0" y="858.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="start">După corecție masivă: doji / hammer la suporturi mari</text>
<text x="60.0" y="877.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="start">(monthly, quarterly, anual); scanner de apropiere de suport</text>
<text x="60.0" y="896.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="start">până la 3% (4% e cam departe).</text>
<text x="60.0" y="917.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="start">08_ANALIZA_ZILNICA/11_selectia_simbolului.md:787-803</text>
<text x="60.0" y="952.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="start">Radarscreen (max ~300 simboluri): repetă fluxul — sectoare top 10,</text>
<text x="60.0" y="971.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="start">RS, apoi 3 stele.</text>
<text x="60.0" y="992.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="start">08_ANALIZA_ZILNICA/11_selectia_simbolului.md:815-825</text>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,286 @@
#!/usr/bin/env python3
"""ATM score — scala 0-14 (bară cu praguri). TIP: CONCEPT.
Citește specificația `gen/strategii/selectia_scor.spec.txt` și scrie
`gen/strategii/selectia_scor.svg`. Diagramă conceptuală (NU candele): nu
folosește `svg_candles` / `pchart`, SVG-ul e construit ca string, doar stdlib.
Ce arată (toate cifrele vin din spec):
- bara 0-14: F-score 0-9 (albastru, 9/14) + value-score 0-5 (portocaliu, 5/14);
- praguri: 7 (reperul „galben”), 7-8 („intră în analiză”), 12-13 („spuma”), 14 (max);
- benzile F-score: 0-3 slab fundamental, 4-5 mixt, 6-7 solid, 8-9 foarte puternic;
- nota: scor -1 = date indisponibile în platformă, NU scor 0;
- nota: „peste 7-8" e formularea cursului, sub 7 e deducere, nu prag declarat.
Rulare: .venv/bin/python gen/strategii/selectia_scor.py
"""
import html
from pathlib import Path
W, H = 700, 610
BLACK = "#000000"
GRAY = "#777777"
RED = "#c00"
GREEN = "#188a3e"
BLUE = "#1a5fb4"
ORANGE = "#b06000"
YELLOW = "#e0a800" # „7/14 = galben" (reperul din curs)
DARK_YELLOW = "#8a6d00" # aceeași nuanță, lizibilă ca text
BLUE_BG = "#eef2fb"
ORANGE_BG = "#fdf0e0"
GREEN_BG = "#eef7f0"
RED_BG = "#fdeeee"
GRAY_BG = "#f4f4f4"
# ---------------------------------------------------------------- geometrie bară
BAR_X0, BAR_X1 = 60.0, 640.0
BAR_Y, BAR_H = 150.0, 48.0
UNIT = (BAR_X1 - BAR_X0) / 14.0 # 41.4286 px per punct de scor
SPLIT = 9 # F-score 0-9, apoi value-score 9-14
def sx(v):
"""Poziția pe bară a scorului v (0..14)."""
return BAR_X0 + v * UNIT
X_SPLIT = sx(SPLIT)
X7, X8, X12, X13, X14 = sx(7), sx(8), sx(12), sx(13), sx(14)
# ---------------------------------------------------------------- colectoare
TEXTS = [] # (x, y, s, size, weight, fill, anchor)
RECTS = [] # (x, y, w, h, tag)
def add(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"):
TEXTS.append((float(x), float(y), s, size, weight, fill, anchor))
return s
def est_w(s, size, weight):
factor = 0.62 if weight == "bold" else 0.56
return len(s) * size * factor
def txt(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"):
return (f'<text x="{x:.1f}" y="{y:.1f}" font-size="{size}" '
f'font-weight="{weight}" fill="{fill}" text-anchor="{anchor}">'
f'{html.escape(s, quote=False)}</text>')
def rect(x, y, w, h, tag, fill="#ffffff", stroke=GRAY, sw=1.5, dash=None):
RECTS.append((float(x), float(y), float(w), float(h), tag))
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'<rect x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}" '
f'fill="{fill}" stroke="{stroke}" stroke-width="{sw}"{d}/>')
def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None):
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
f'stroke="{stroke}" stroke-width="{width}"{d}/>')
# =============================================================== CONȚINUT
p = []
# ---- titlu + subtitlu
add(W / 2, 34, "ATM score: scala 0-14", 21, "bold")
add(W / 2, 58, "ATM score = F-score (max 9) + value-score (max 5) = max 14",
13, "normal", GRAY)
# ---- etichetele celor două componente, deasupra barei
add((BAR_X0 + X_SPLIT) / 2, 112, "F-score 0-9", 15, "bold", BLUE)
add((BAR_X0 + X_SPLIT) / 2, 131, "9/14 din bară", 13, "normal", GRAY)
add((X_SPLIT + BAR_X1) / 2, 112, "value-score 0-5", 15, "bold", ORANGE)
add((X_SPLIT + BAR_X1) / 2, 131, "5/14 din bară", 13, "normal", GRAY)
# ---- bara celor 14 puncte: partea albastră (0-9) + partea portocalie (9-14)
p.append(rect(BAR_X0, BAR_Y, X_SPLIT - BAR_X0, BAR_H, "fscore",
fill=BLUE_BG, stroke=BLUE, sw=1.8))
p.append(rect(X_SPLIT, BAR_Y, BAR_X1 - X_SPLIT, BAR_H, "vscore",
fill=ORANGE_BG, stroke=ORANGE, sw=1.8))
# diviziunea la 9
p.append(line(X_SPLIT, BAR_Y, X_SPLIT, BAR_Y + BAR_H, BLACK, 1.2, "3 3"))
# reperele peste bară: banda „spuma" 12-13 și linia galbenă de la 7
p.append(rect(X12, BAR_Y - 6, X13 - X12, BAR_H + 12, "spuma",
fill="none", stroke=RED, sw=1.4, dash="5 3"))
p.append(line(X7, BAR_Y - 10, X7, BAR_Y + BAR_H + 6, YELLOW, 2.4, "4 3"))
# în interiorul barei, numele componentelor
add((BAR_X0 + X_SPLIT) / 2, BAR_Y + BAR_H / 2 + 5, "F-score", 15, "bold", BLUE)
add((X_SPLIT + BAR_X1) / 2, BAR_Y + BAR_H / 2 + 5, "value-score", 15, "bold", ORANGE)
# ---- scale: tick-uri + numere 0..14 sub bară
for i in range(15):
x = sx(i)
p.append(line(x, BAR_Y + BAR_H, x, BAR_Y + BAR_H + 7, GRAY, 1.0))
add(x, BAR_Y + BAR_H + 26, str(i), 13, "normal", BLACK)
# ---- reperul 7/14 (galben)
add(X7 - 8, 244, "7/14 = galben", 13, "bold", DARK_YELLOW, "end")
# ---- intervalul „spuma" (12-13) și maximul 14
add(X14, 244, "12-13: spuma", 13, "bold", RED, "end")
add(X14, 264, "14 = maxim", 13, "bold", BLACK, "end")
# ---- „peste 7-8: intră în analiză" (acoladă sub scală, peste intervalul 7-8)
p.append(line(X7, 234, X7, 242, GRAY, 1.2))
p.append(line(X8, 234, X8, 242, GRAY, 1.2))
p.append(line(X7, 242, X8, 242, GRAY, 1.2))
add((X7 + X8) / 2, 264, "peste 7-8: intră în analiză", 13, "bold", BLACK)
# ---- benzile F-score, aliniate sub partea albastră (0-9)
add(BAR_X0, 300, "Benzi F-score (partea albastră, 0-9)", 14, "bold",
BLACK, "start")
BANDS = [
(0, 3, "0-3", ["slab", "fundamental"], RED, RED_BG),
(3, 5, "4-5", ["mixt"], ORANGE, GRAY_BG),
(5, 7, "6-7", ["solid"], BLUE, BLUE_BG),
(7, 9, "8-9", ["foarte", "puternic"], GREEN, GREEN_BG),
]
BAND_Y, BAND_H = 314.0, 64.0
for a, b, rng, labs, col, bg in BANDS:
x0, x1 = sx(a), sx(b)
p.append(rect(x0, BAND_Y, x1 - x0, BAND_H, f"band{a}",
fill=bg, stroke=col, sw=1.6))
cx = (x0 + x1) / 2
add(cx, 336, rng, 15, "bold", col)
add(cx, 358, labs[0], 13, "normal", BLACK)
if len(labs) > 1:
add(cx, 376, labs[1], 13, "normal", BLACK)
# ---- note (scor -1 + formularea „peste 7-8")
p.append(rect(40, 402, 620, 122, "note",
fill="#f7f7f7", stroke=GRAY, sw=1.2))
add(60, 430, "Scor -1 = date indisponibile în platformă, NU scor 0.",
14, "bold", BLACK, "start")
add(60, 450, "Scorul -1 nu se citește ca 0.", 13, "normal", GRAY, "start")
add(60, 470,
"16_ANALIZA_FUNDAMENTALA/09_implementare_f_score_tradestation.md:45-46, 268-269.",
13, "normal", GRAY, "start")
add(60, 498,
"„peste 7-8” e formularea exactă a cursului; sub 7 se deduce ca excludere,",
13, "normal", BLACK, "start")
add(60, 518,
"nu e declarat ca prag separat (11_selectia_simbolului.md:199-204).",
13, "normal", GRAY, "start")
# ---- surse
add(W / 2, 556,
"Surse: 08_ANALIZA_ZILNICA/11_selectia_simbolului.md:158-164, :199-204;",
13, "normal", GRAY)
add(W / 2, 574,
"16_ANALIZA_FUNDAMENTALA/08_piotrowski_f_score.md:70-72;",
13, "normal", GRAY)
add(W / 2, 592,
"16_ANALIZA_FUNDAMENTALA/09_implementare_f_score_tradestation.md:45-46, 268-269.",
13, "normal", GRAY)
# =============================================================== EMITERE
head = (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" '
f'width="{W}" height="{H}" font-family="system-ui, sans-serif" role="img" '
f'aria-label="Scala ATM score 0-14: F-score 0-9 la care se adauga '
f'value-score 0-5; pragurile 7 (galben), 7-8 intrare in analiza, '
f'12-13 spuma, 14 maxim">')
body = [head, f'<rect x="0" y="0" width="{W}" height="{H}" fill="#ffffff"/>']
body += p
for x, y, s, size, weight, fill, anchor in TEXTS:
body.append(txt(x, y, s, size, weight, fill, anchor))
body.append("</svg>")
svg = "\n".join(body) + "\n"
# =============================================================== VERIFICARE
# 1. bara are proporțiile cerute: 9/14 albastru, 5/14 portocaliu
w_f = X_SPLIT - BAR_X0
w_v = BAR_X1 - X_SPLIT
assert abs(w_f / (BAR_X1 - BAR_X0) - 9 / 14) < 1e-9, "F-score nu e 9/14 din bară"
assert abs(w_v / (BAR_X1 - BAR_X0) - 5 / 14) < 1e-9, "value-score nu e 5/14 din bară"
assert abs(X_SPLIT - sx(9)) < 1e-9 and abs(sx(0) - BAR_X0) < 1e-9, "scara nu e 0-14"
# 2. toate cifrele/etichetele din spec sunt în desen
for req in ("ATM score: scala 0-14",
"ATM score = F-score (max 9) + value-score (max 5) = max 14",
"F-score 0-9", "9/14 din bară",
"value-score 0-5", "5/14 din bară",
"7/14 = galben", "peste 7-8: intră în analiză",
"12-13: spuma", "14 = maxim",
"0-3", "slab", "fundamental",
"4-5", "mixt", "6-7", "solid", "8-9", "foarte", "puternic",
"Scor -1 = date indisponibile în platformă, NU scor 0.",
"sub 7 se deduce ca excludere",
"nu e declarat ca prag separat"):
assert any(req in t[2] for t in TEXTS), f"lipsește din desen: {req!r}"
# 3. numerele de pe scală sunt exact 0..14
nums = [t[2] for t in TEXTS if t[5] == BLACK and t[3] == 13 and t[6] == "middle"
and t[2].isdigit() and BAR_Y < t[1] < BAR_Y + BAR_H + 40]
assert nums == [str(i) for i in range(15)], nums
# 4. pozițiile relative ale pragurilor
assert abs(sx(7) - (BAR_X0 + 7 * UNIT)) < 1e-9
assert sx(12) < sx(13) < sx(14) == BAR_X1, "intervalul spuma / maximul greșit"
# reperul 7 (galben) și intervalul spuma sunt desenate ca linii/dreptunghiuri
assert any(r[4] == "spuma" for r in RECTS), "lipsește banda spuma 12-13"
assert any(r[4] == "fscore" for r in RECTS) and any(r[4] == "vscore" for r in RECTS)
# 5. benzile F-score: aliniate exact sub partea albastră, fără suprapunere
bands = [r for r in RECTS if r[4].startswith("band")]
assert len(bands) == 4, f"aștept 4 benzi F-score, am {len(bands)}"
assert abs(bands[0][0] - BAR_X0) < 1e-9, "prima bandă nu începe la 0"
assert abs((bands[-1][0] + bands[-1][2]) - X_SPLIT) < 1e-9, "ultima bandă nu se termină la 9"
for i in range(1, len(bands)):
assert abs(bands[i][0] - (bands[i - 1][0] + bands[i - 1][2])) < 1e-9, "benzi nelipite"
for i in range(len(bands)):
for j in range(i + 1, len(bands)):
x1, y1, w1, h1, t1 = bands[i]
x2, y2, w2, h2, t2 = bands[j]
assert not (x1 < x2 + w2 and x2 < x1 + w1 and y1 < y2 + h2 and y2 < y1 + h1), \
f"benzi suprapuse: {t1}/{t2}"
# 6. numerele de pe scală sunt exact cele din benzi (0,3,5,7,9)
edges = [sx(0), sx(3), sx(5), sx(7), sx(9)]
for e, b in zip(edges, bands):
assert abs(e - b[0]) < 1e-9, "marginile benzilor nu cad pe scală"
# 7. fiecare etichetă: mărime >= 13, în panză, fără suprapuneri
boxes = []
for x, y, s, size, weight, fill, anchor in TEXTS:
assert size >= 13, f"font sub 13px: {s!r} ({size})"
w = est_w(s, size, weight)
if anchor == "end":
left = x - w
elif anchor == "start":
left = x
else:
left = x - w / 2.0
right = left + w
top, bottom = y - size * 0.78, y + size * 0.22
assert -2 <= left and right <= W + 2, f"text în afara panzei orizontal: {s!r}"
assert 0 <= top and bottom <= H, f"text în afara panzei vertical: {s!r}"
boxes.append((left, right, top, bottom, s))
for i in range(len(boxes)):
for j in range(i + 1, len(boxes)):
l1, r1, t1, b1, s1 = boxes[i]
l2, r2, t2_, b2, s2 = boxes[j]
if r1 > l2 + 2 and r2 > l1 + 2 and b1 > t2_ + 2 and b2 > t1 + 2:
raise AssertionError(f"etichete suprapuse: {s1!r} / {s2!r}")
# 8. textele din interiorul barei și benzile încap
for tag, (rx, ry, rw, rh, _) in [(r[4], r) for r in RECTS]:
if tag in ("fscore", "vscore"):
for x, y, s, size, weight, fill, anchor in TEXTS:
if rx <= x <= rx + rw and ry <= y <= ry + rh:
assert est_w(s, size, weight) <= rw - 12, f"text prea lat în {tag}: {s!r}"
# 9. stil: fără umbre/gradienți, fontul cerut
assert "filter=" not in svg and "gradient" not in svg, "fără umbre/gradienti"
assert 'font-family="system-ui, sans-serif"' in svg, "lipsește fontul cerut"
assert 'fill="#ffffff"' in svg, "fundal alb lipsă"
OUT = Path(__file__).with_suffix(".svg")
OUT.write_text(svg, encoding="utf-8")
print(f"scris {OUT} ({len(svg)} bytes, {len(TEXTS)} etichete, "
f"{len(bands)} benzi F-score, F-score {w_f:.1f}px / value-score {w_v:.1f}px, "
f"{W}x{H}), verificare OK")

View File

@@ -0,0 +1,77 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 610" width="700" height="610" font-family="system-ui, sans-serif" role="img" aria-label="Scala ATM score 0-14: F-score 0-9 la care se adauga value-score 0-5; pragurile 7 (galben), 7-8 intrare in analiza, 12-13 spuma, 14 maxim">
<rect x="0" y="0" width="700" height="610" fill="#ffffff"/>
<rect x="60.0" y="150.0" width="372.9" height="48.0" fill="#eef2fb" stroke="#1a5fb4" stroke-width="1.8"/>
<rect x="432.9" y="150.0" width="207.1" height="48.0" fill="#fdf0e0" stroke="#b06000" stroke-width="1.8"/>
<line x1="432.9" y1="150.0" x2="432.9" y2="198.0" stroke="#000000" stroke-width="1.2" stroke-dasharray="3 3"/>
<rect x="557.1" y="144.0" width="41.4" height="60.0" fill="none" stroke="#c00" stroke-width="1.4" stroke-dasharray="5 3"/>
<line x1="350.0" y1="140.0" x2="350.0" y2="204.0" stroke="#e0a800" stroke-width="2.4" stroke-dasharray="4 3"/>
<line x1="60.0" y1="198.0" x2="60.0" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="101.4" y1="198.0" x2="101.4" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="142.9" y1="198.0" x2="142.9" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="184.3" y1="198.0" x2="184.3" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="225.7" y1="198.0" x2="225.7" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="267.1" y1="198.0" x2="267.1" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="308.6" y1="198.0" x2="308.6" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="350.0" y1="198.0" x2="350.0" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="391.4" y1="198.0" x2="391.4" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="432.9" y1="198.0" x2="432.9" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="474.3" y1="198.0" x2="474.3" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="515.7" y1="198.0" x2="515.7" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="557.1" y1="198.0" x2="557.1" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="598.6" y1="198.0" x2="598.6" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="640.0" y1="198.0" x2="640.0" y2="205.0" stroke="#777777" stroke-width="1.0"/>
<line x1="350.0" y1="234.0" x2="350.0" y2="242.0" stroke="#777777" stroke-width="1.2"/>
<line x1="391.4" y1="234.0" x2="391.4" y2="242.0" stroke="#777777" stroke-width="1.2"/>
<line x1="350.0" y1="242.0" x2="391.4" y2="242.0" stroke="#777777" stroke-width="1.2"/>
<rect x="60.0" y="314.0" width="124.3" height="64.0" fill="#fdeeee" stroke="#c00" stroke-width="1.6"/>
<rect x="184.3" y="314.0" width="82.9" height="64.0" fill="#f4f4f4" stroke="#b06000" stroke-width="1.6"/>
<rect x="267.1" y="314.0" width="82.9" height="64.0" fill="#eef2fb" stroke="#1a5fb4" stroke-width="1.6"/>
<rect x="350.0" y="314.0" width="82.9" height="64.0" fill="#eef7f0" stroke="#188a3e" stroke-width="1.6"/>
<rect x="40.0" y="402.0" width="620.0" height="122.0" fill="#f7f7f7" stroke="#777777" stroke-width="1.2"/>
<text x="350.0" y="34.0" font-size="21" font-weight="bold" fill="#000000" text-anchor="middle">ATM score: scala 0-14</text>
<text x="350.0" y="58.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">ATM score = F-score (max 9) + value-score (max 5) = max 14</text>
<text x="246.4" y="112.0" font-size="15" font-weight="bold" fill="#1a5fb4" text-anchor="middle">F-score 0-9</text>
<text x="246.4" y="131.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">9/14 din bară</text>
<text x="536.4" y="112.0" font-size="15" font-weight="bold" fill="#b06000" text-anchor="middle">value-score 0-5</text>
<text x="536.4" y="131.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">5/14 din bară</text>
<text x="246.4" y="179.0" font-size="15" font-weight="bold" fill="#1a5fb4" text-anchor="middle">F-score</text>
<text x="536.4" y="179.0" font-size="15" font-weight="bold" fill="#b06000" text-anchor="middle">value-score</text>
<text x="60.0" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">0</text>
<text x="101.4" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">1</text>
<text x="142.9" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">2</text>
<text x="184.3" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">3</text>
<text x="225.7" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">4</text>
<text x="267.1" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">5</text>
<text x="308.6" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">6</text>
<text x="350.0" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">7</text>
<text x="391.4" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">8</text>
<text x="432.9" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">9</text>
<text x="474.3" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">10</text>
<text x="515.7" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">11</text>
<text x="557.1" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">12</text>
<text x="598.6" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">13</text>
<text x="640.0" y="224.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">14</text>
<text x="342.0" y="244.0" font-size="13" font-weight="bold" fill="#8a6d00" text-anchor="end">7/14 = galben</text>
<text x="640.0" y="244.0" font-size="13" font-weight="bold" fill="#c00" text-anchor="end">12-13: spuma</text>
<text x="640.0" y="264.0" font-size="13" font-weight="bold" fill="#000000" text-anchor="end">14 = maxim</text>
<text x="370.7" y="264.0" font-size="13" font-weight="bold" fill="#000000" text-anchor="middle">peste 7-8: intră în analiză</text>
<text x="60.0" y="300.0" font-size="14" font-weight="bold" fill="#000000" text-anchor="start">Benzi F-score (partea albastră, 0-9)</text>
<text x="122.1" y="336.0" font-size="15" font-weight="bold" fill="#c00" text-anchor="middle">0-3</text>
<text x="122.1" y="358.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">slab</text>
<text x="122.1" y="376.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">fundamental</text>
<text x="225.7" y="336.0" font-size="15" font-weight="bold" fill="#b06000" text-anchor="middle">4-5</text>
<text x="225.7" y="358.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">mixt</text>
<text x="308.6" y="336.0" font-size="15" font-weight="bold" fill="#1a5fb4" text-anchor="middle">6-7</text>
<text x="308.6" y="358.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">solid</text>
<text x="391.4" y="336.0" font-size="15" font-weight="bold" fill="#188a3e" text-anchor="middle">8-9</text>
<text x="391.4" y="358.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">foarte</text>
<text x="391.4" y="376.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="middle">puternic</text>
<text x="60.0" y="430.0" font-size="14" font-weight="bold" fill="#000000" text-anchor="start">Scor -1 = date indisponibile în platformă, NU scor 0.</text>
<text x="60.0" y="450.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="start">Scorul -1 nu se citește ca 0.</text>
<text x="60.0" y="470.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="start">16_ANALIZA_FUNDAMENTALA/09_implementare_f_score_tradestation.md:45-46, 268-269.</text>
<text x="60.0" y="498.0" font-size="13" font-weight="normal" fill="#000000" text-anchor="start">„peste 7-8” e formularea exactă a cursului; sub 7 se deduce ca excludere,</text>
<text x="60.0" y="518.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="start">nu e declarat ca prag separat (11_selectia_simbolului.md:199-204).</text>
<text x="350.0" y="556.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">Surse: 08_ANALIZA_ZILNICA/11_selectia_simbolului.md:158-164, :199-204;</text>
<text x="350.0" y="574.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">16_ANALIZA_FUNDAMENTALA/08_piotrowski_f_score.md:70-72;</text>
<text x="350.0" y="592.0" font-size="13" font-weight="normal" fill="#777777" text-anchor="middle">16_ANALIZA_FUNDAMENTALA/09_implementare_f_score_tradestation.md:45-46, 268-269.</text>
</svg>

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -15,7 +15,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
CAPITOLE = sorted((ROOT / "strategii").glob("0[1-8]_*.md"))
MARCAJ = re.compile(r"^<!-- DIAGRAMA: ([a-z0-9_]+) -->$", re.M)
MARCAJ = re.compile(r"^[ \t]*<!-- DIAGRAMA: ([a-z0-9_]+) -->[ \t]*$", re.M)
def slug(cale: Path) -> str:
@@ -32,7 +32,11 @@ def main() -> int:
def inlocuieste(m: re.Match) -> str:
nonlocal puse, lipsa
svg = ROOT / "gen" / "strategii" / f"{s}_{m.group(1)}.svg"
# unii agenți au pus slug-ul în cheie (mp2_exemplu), alții nu (exemplu)
gen = ROOT / "gen" / "strategii"
svg = next((c for c in (gen / f"{s}_{m.group(1)}.svg",
gen / f"{m.group(1)}.svg") if c.exists()),
gen / f"{s}_{m.group(1)}.svg")
if not svg.exists():
lipsa += 1
print(f"LIPSĂ {cap.name}: {svg.name}")