From f1033a916c35501659d443a73d174246a3a037b4 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sun, 20 Sep 2026 12:43:12 +0000 Subject: [PATCH] selectia: diagramele refacute dupa sablon (textul urmeaza) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agentul a atins pragul de context dupa diagrame; capitolul 04_selectia e inca in formatul vechi si se reia separat. Ce s-a schimbat: ansamblul nu mai arata ULTA mergand la TP2 — ULTA pica chiar filtrul de selectie (RVOL 0.61 si 5.01 % de suport), deci nu putea fi exemplul-vitrina; acum arata doar ce citeste selectia, cu granita "aici se termina selectia" marcata. Funnel-ul trece de la 8 trepte la 6, aliniat cu pasii nivelului 2, si fiecare treapta spune de ce. Cadranele primesc mini-histograme RS, ca sa se recunoasca dintr-un desen, nu dintr-o propozitie. Trimiterile fisier:linie sunt scoase de pe desene. Noi: univers (pragurile de pret si volum), sectoare (dominantul din cele 11), proximitate (banda de 3 % peste suportul monthly, cu observatia ca nu e pragul de 1-2 % din Buy Doji — masoara altceva). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ --- gen/strategii/atmi_aplus.svg | 119 --------- .../{atmi_aplus.py => atmi_corectie.py} | 0 ..._aplus.spec.txt => atmi_corectie.spec.txt} | 0 gen/strategii/selectia_ansamblu.py | 174 ++++++------ gen/strategii/selectia_ansamblu.svg | 247 +++++++++--------- gen/strategii/selectia_cadrane.py | 129 +++++---- gen/strategii/selectia_cadrane.svg | 125 ++++++--- gen/strategii/selectia_exemplu.py | 63 +++-- gen/strategii/selectia_exemplu.svg | 235 +++++++++-------- gen/strategii/selectia_funnel.py | 129 +++++---- gen/strategii/selectia_funnel.svg | 89 +++---- gen/strategii/selectia_proximitate.py | 171 ++++++++++++ gen/strategii/selectia_proximitate.svg | 121 +++++++++ gen/strategii/selectia_scor.py | 18 +- gen/strategii/selectia_scor.svg | 11 +- gen/strategii/selectia_sectoare.py | 153 +++++++++++ gen/strategii/selectia_sectoare.svg | 50 ++++ gen/strategii/selectia_univers.py | 194 ++++++++++++++ gen/strategii/selectia_univers.svg | 49 ++++ 19 files changed, 1407 insertions(+), 670 deletions(-) delete mode 100644 gen/strategii/atmi_aplus.svg rename gen/strategii/{atmi_aplus.py => atmi_corectie.py} (100%) rename gen/strategii/{atmi_aplus.spec.txt => atmi_corectie.spec.txt} (100%) create mode 100644 gen/strategii/selectia_proximitate.py create mode 100644 gen/strategii/selectia_proximitate.svg create mode 100644 gen/strategii/selectia_sectoare.py create mode 100644 gen/strategii/selectia_sectoare.svg create mode 100644 gen/strategii/selectia_univers.py create mode 100644 gen/strategii/selectia_univers.svg diff --git a/gen/strategii/atmi_aplus.svg b/gen/strategii/atmi_aplus.svg deleted file mode 100644 index 07e53ee..0000000 --- a/gen/strategii/atmi_aplus.svg +++ /dev/null @@ -1,119 +0,0 @@ - - - SPY — daily: aceeași corecție, 4.0 % (DA) vs. 3.9 % (NU) - - 100.00 — swing high - - 96.00 = 4.0 % — DA, intri - - TP +10 % = 105.60 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - volum - BIM - candelă verde → trigger - Regula e pe procent, nu pe cât de dramatică arată linia: calea NU pare o cădere mare, dar e 3.9 %. - Calea DA (candele): 4.0 % la 96.00. Calea NU (punctat): 3.9 % („3.93 %” în curs) — fără intrare. - - - - 96.10 = 3.9 % — NU, sub prag - zonă quarterly - calea DA (candelele) - calea NU (continuă în jos) - \ No newline at end of file diff --git a/gen/strategii/atmi_aplus.py b/gen/strategii/atmi_corectie.py similarity index 100% rename from gen/strategii/atmi_aplus.py rename to gen/strategii/atmi_corectie.py diff --git a/gen/strategii/atmi_aplus.spec.txt b/gen/strategii/atmi_corectie.spec.txt similarity index 100% rename from gen/strategii/atmi_aplus.spec.txt rename to gen/strategii/atmi_corectie.spec.txt diff --git a/gen/strategii/selectia_ansamblu.py b/gen/strategii/selectia_ansamblu.py index 3feb9ce..8b39fa5 100644 --- a/gen/strategii/selectia_ansamblu.py +++ b/gen/strategii/selectia_ansamblu.py @@ -1,10 +1,16 @@ """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ă. +Rolul diagramei în capitol: arată **unde se termină selecția**. Filtrul citește +contextul (downtrend, suportul monthly 355.48, pivotul monthly 383.74, buy doji +la 373.30, RVOL 0.61) și se oprește la „simbolul intră pe listă". Intrarea, +stopul și ținta nu apar aici, pentru că nu aparțin selecției — sunt ale +capitolului 1 (Buy Doji). Partea de după linia verticală e desenată gri tocmai +ca să se vadă că nu mai e treaba filtrului. + +Cifrele cursului: close 373.30, suport monthly 355.48, pivot monthly 383.74, +RVOL 0.61 (`04_MODUL_3/01_filtre_scaner_bull_bear_reversal_pp.md`). Procentele +de 5.01 % și 2.72 % se recalculează din ele, cu assert. Structura barelor e +construită, la scară, prin `svg_candles` (from_closes). Rulează: python3 gen/strategii/selectia_ansamblu.py @@ -15,18 +21,20 @@ 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 +from svg_candles import Chart, Level, Arrow, Zone, 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" +SUPORT, PIVOT, DOJI_CLOSE = 355.48, 383.74, 373.30 +DOJI = 20 # indexul candelei de buy doji + # ---------------------------------------------------------------- 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 +# bare 0-11 : downtrend vizibil de la ~395 la ~362 (~0.8 %/bară) +# bare 12-19 : baza, minimul atinge suportul monthly 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 +# bare 21-31 : ce urmează — nu mai e treaba selecției, desenat gri 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 @@ -38,92 +46,106 @@ CLOSES = [ 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[0] = (0.40, 0.50) # prima bară, deschide dintr-un vârf 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 +WICKS[DOJI] = (1.45, 1.45) # doji: corp 0.30 din 3.20 înălțime = 9 % assert len(WICKS) == 32, len(WICKS) -# barele 25-31 in crestere; bara 20 (doji) sub media seriei (RVOL 0.61) +# bara 20 (doji) sub media seriei: exact ce înseamnă 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 + 46, # 21 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 +assert abs(VOLS[DOJI] / media_vol - 0.61) < 0.02, VOLS[DOJI] / 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" +# barele de după doji sunt gri: nu mai sunt în sarcina filtrului +for cd in cds[DOJI + 1:]: + cd.color = "#bbb" + +# ---------------------------------------------------------------- assert-uri de conținut +assert abs(cds[DOJI].c - DOJI_CLOSE) < 1e-9, "doji-ul trebuie să închidă la 373.30" +corp = abs(cds[DOJI].o - cds[DOJI].c) +inaltime = cds[DOJI].h - cds[DOJI].l +assert corp / inaltime <= 0.10, f"bara {DOJI} nu e doji: corp {corp / inaltime:.0%} din înălțime" +assert abs(min(c.l for c in cds[12:DOJI + 1]) - SUPORT) < 0.05, "suportul 355.48 nu e atins" +# de la intrarea în bază și până la doji, prețul stă sub pivot: de asta pivotul +# e rezistența cea mai apropiată deasupra doji-ului +assert max(c.h for c in cds[12:DOJI + 1]) < PIVOT, "pivotul nu are voie atins în bază" for a, b in zip(cds, cds[1:]): - assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial intre bare" + assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial între 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" +# procentele din etichete se calculează, nu se scriu de mână +pct_suport = (DOJI_CLOSE - SUPORT) / SUPORT * 100 # 5.01 % +pct_pivot = (PIVOT - DOJI_CLOSE) / PIVOT * 100 # 2.72 % +assert abs(pct_suport - 5.01) < 0.01, pct_suport +assert abs(pct_pivot - 2.72) < 0.01, pct_pivot +rvol = VOLS[DOJI] / media_vol -# ---------------------------------------------------------------- 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), + Level(SUPORT, f"suport monthly {SUPORT:.2f}", BLUE, label_at="left"), + Level(PIVOT, f"pivot monthly {PIVOT:.2f}", 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), +zones = [ + Zone(0, 11, "1. downtrend — de asta apare în scaner", RED), + Zone(12, DOJI, "", BLUE, p_bot=SUPORT - 0.6), ] -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.") +arrows = [ + Arrow(15, f"2. suportul monthly {SUPORT:.2f} a mai întors prețul o dată:\n" + f"acolo stau ordine de cumpărare, deci coborârea are unde să se oprească", + where="below", price=SUPORT, color=BLUE, dx=-30, dy=34), + Arrow(DOJI, f"3. buy doji, close {DOJI_CLOSE:.2f} — aici se oprește selecția\n" + f"{pct_suport:.2f} % peste suport, {pct_pivot:.2f} % sub pivot: trece proximitatea față de pivot\n" + f"volumul zilei = {rvol:.2f} din medie (RVOL {rvol:.2f}), sub pragul de 1.00: minus, nu veto", + where="above", color=TEAL, dx=-250, dy=-66), + Arrow(27, "trigger, stop și țintă sunt\nale capitolului Buy Doji", + where="right", color=GRAY, dx=16, dy=-40), +] + +caption = ("Ale cursului: close 373.30, suport monthly 355.48, pivot monthly 383.74, RVOL 0.61. " + "Procentele sunt calculate din ele. Structura barelor e construită, la scară. " + "Intrarea, stopul și ținta nu apar pe desen: nu aparțin selecției.") ch = Chart( - candles=cds, levels=levels, marks=marks, + candles=cds, levels=levels, zones=zones, arrows=arrows, 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)", + title="ULTA — daily: ce citește selecția și unde se oprește", 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" +# ---------------------------------------------------------------- linia verticală de predare +# offsetul intern al randării, citit din prima linie de Level desenată +m_off = re.search(r']*fill="' + RED + r'"', svg) +assert m_dx, "nu găsesc dreptunghiul primei zone, din care citesc offsetul pe x" +DX = float(m_dx.group(1)) - (ch._cx(0) - ch.body_w / 2 - 5) +x_sep = ch._cx(DOJI) + DX + ch.gap / 2 +y_top = ch._y(max(c.h for c in cds)) + OFF + 4 +y_bot = ch._y(min(c.l for c in cds)) + OFF + 16 +sep = (f'' + f'aici se termină selecția' + f'aici începe capitolul 1') +svg = svg.replace("", sep + "") -# ---------------------------------------------------------------- finisaj text -# caption-ul pe un singur rand depaseste latimea panzei (se taie la viewBox): -# il rupem in linii si crestem inaltimea desenului. +# ---------------------------------------------------------------- caption pe mai multe rânduri def _wrap(text: str, max_chars: int) -> list[str]: lines, cur = [], "" for w in text.split(): @@ -138,25 +160,27 @@ def _wrap(text: str, max_chars: int) -> list[str]: cap_lines = _wrap(caption, 92) -m = re.search(r'' + re.escape(caption) + r"\s*", svg) -assert m, "caption-ul nu a fost gasit in SVG" +m = re.search(r'' + re.escape(caption) + r"", svg) +assert m, "caption-ul nu a fost găsit în SVG" y0 = int(m.group(1)) wrapped = "\n ".join(f'{ln}' for i, ln in enumerate(cap_lines)) -svg = svg[:m.start()] + wrapped + "\n" +svg = svg[:m.start()] + wrapped + svg[m.end():] 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) + f'viewBox="0 0 {vm.group(1)} {int(vm.group(2)) + 17 * (len(cap_lines) - 1) + 24}"', 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'(SMA-20)', - lambda mm: f'{mm.group(1)}{int(mm.group(2)) - 12}{mm.group(3)}', svg) -assert n == 1, f"eticheta SMA-20: {n} potriviri" +# ---------------------------------------------------------------- nicio cifră inventată în etichete +desenate = {f"{SUPORT:.2f}", f"{PIVOT:.2f}", f"{DOJI_CLOSE:.2f}"} +for txt in re.findall(r"]*>([^<]*)", svg): + for nr in re.findall(r"\d+\.\d\d", txt): + assert nr in desenate | {f"{pct_suport:.2f}", f"{pct_pivot:.2f}", + f"{rvol:.2f}", "1.00"}, \ + f"eticheta numește {nr}, care nu e un nivel desenat: {txt!r}" -assert " - ULTA — daily (pivoți monthly) - - SL 352.00 - - suport monthly 355.48 - - intrare 374.80 - - pivot / TP1 383.74 - - TP2 402.00 - - SMA-20 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - volum - downtrend weekly - buy doji close 373.30 (RVOL 0.61) - intrare 374.80 - TP1 383.74 = pivot - TP2 402.00 - 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. - \ No newline at end of file + + ULTA — daily: ce citește selecția și unde se oprește + + 1. downtrend — de asta apare în scaner + + + suport monthly 355.48 + + pivot monthly 383.74 + + SMA-20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + + + 2. suportul monthly 355.48 a mai întors prețul o dată: + acolo stau ordine de cumpărare, deci coborârea are unde să se oprească + + + 3. buy doji, close 373.30 — aici se oprește selecția + 5.01 % peste suport, 2.72 % sub pivot: trece proximitatea față de pivot + volumul zilei = 0.61 din medie (RVOL 0.61), sub pragul de 1.00: minus, nu veto + + + trigger, stop și țintă sunt + ale capitolului Buy Doji + Ale cursului: close 373.30, suport monthly 355.48, pivot monthly 383.74, RVOL 0.61. + Procentele sunt calculate din ele. Structura barelor e construită, la scară. Intrarea, + stopul și ținta nu apar pe desen: nu aparțin selecției. +aici se termină selecțiaaici începe capitolul 1 \ No newline at end of file diff --git a/gen/strategii/selectia_cadrane.py b/gen/strategii/selectia_cadrane.py index e19bd84..8b6ac6d 100644 --- a/gen/strategii/selectia_cadrane.py +++ b/gen/strategii/selectia_cadrane.py @@ -32,7 +32,7 @@ Rulare: .venv/bin/python gen/strategii/selectia_cadrane.py import html from pathlib import Path -W, H = 700, 720 +W, H = 700, 812 BLACK = "#000000" GRAY = "#777777" @@ -46,7 +46,7 @@ RED_BG = "#fdeeee" # ---------------------------------------------------------------- geometrie PLOT_X0, PLOT_X1 = 130.0, 570.0 -PLOT_Y0, PLOT_Y1 = 134.0, 414.0 +PLOT_Y0, PLOT_Y1 = 134.0, 502.0 CX = (PLOT_X0 + PLOT_X1) / 2.0 # 350 CY = (PLOT_Y0 + PLOT_Y1) / 2.0 # 266 @@ -61,7 +61,7 @@ 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} +BAND = {"x": 40.0, "y": 556.0, "w": 620.0, "h": 196.0} # ---------------------------------------------------------------- collect TEXTS = [] # (x, y, s, size, weight, fill, anchor) @@ -97,6 +97,39 @@ def line(x1, y1, x2, y2, stroke=GRAY, width=1.0, dash=None): f'stroke="{stroke}" stroke-width="{width}"{d}/>') +def ma4(vals): + """Media mobilă pe 4, ca în indicatorul cursului: histogramă + MA peste ea.""" + return [sum(vals[max(0, i - 3):i + 1]) / len(vals[max(0, i - 3):i + 1]) + for i in range(len(vals))] + + +def sketch(q, dy, vals, color): + """Mini-histogramă RS cu linia de 0 și media mobilă peste ea, în celula q. + + Ultima bară e cea care decide cadranul, așa că e desenată plină, iar + poziția ei față de 0 și față de MA e verificată prin assert de mai jos. + """ + w, h = 158.0, 46.0 + x0 = q["x"] + (q["w"] - w) / 2.0 + y0 = q["y"] + dy + yz = y0 + h / 2.0 # linia de 0, la mijloc + unit = (h / 2.0 - 3) / max(abs(v) for v in vals) + bw = w / (len(vals) * 1.6) + out = [f'', + line(x0, yz, x0 + w, yz, BLACK, 1.0)] + xs = [x0 + w * (i + 0.5) / len(vals) for i in range(len(vals))] + for i, v in enumerate(vals): + top = yz - v * unit if v > 0 else yz + out.append(f'') + pts = " ".join(f"{xs[i]:.1f},{yz - m * unit:.1f}" for i, m in enumerate(ma4(vals))) + out.append(f'') + return out + + def cell_text(q, dy, s, *a, **k): add(q["x"] + q["w"] / 2.0, q["y"] + dy, s, *a, **k) @@ -134,67 +167,76 @@ 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)) +# seriile mini-histogramelor: ultima bară decide cadranul +H3 = [6, 6, 5, 5, 4, 3, 3, 2, 1] # peste 0, dar sub MA +H1 = [-1, 0, 1, 2, 2, 3, 4, 5, 6] # peste 0 și peste MA +H4 = [1, 0, -1, -2, -2, -3, -4, -5, -6] # sub 0 și sub MA +H2 = [-6, -6, -5, -5, -4, -3, -3, -2, -1] # sub 0, dar peste MA + # 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) +cell_text(Q3, 26, "Cadran 3", 16, "bold", BLACK) +cell_text(Q3, 48, "înrăutățire", 14, "bold", RED) +cell_text(Q3, 70, "peste 0, dar sub MA", 13, "normal", BLACK) +cell_text(Q3, 94, "SHORT", 16, "bold", RED) +p += sketch(Q3, 106, H3, 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) +cell_text(Q1, 26, "Cadran 1", 16, "bold", BLACK) +cell_text(Q1, 48, "supraperformanță", 14, "bold", GREEN) +cell_text(Q1, 70, "peste 0 și peste MA", 13, "normal", BLACK) +cell_text(Q1, 94, "LONG", 16, "bold", GREEN) +p += sketch(Q1, 106, H1, 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) +cell_text(Q4, 24, "Cadran 4", 16, "bold", BLACK) +cell_text(Q4, 46, "subperformanță", 14, "bold", RED) +cell_text(Q4, 68, "sub 0 și sub MA", 13, "normal", BLACK) +cell_text(Q4, 90, "SHORT — stai pe margine", 14, "bold", RED) +p += sketch(Q4, 102, H4, 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) +cell_text(Q2, 24, "Cadran 2", 16, "bold", BLACK) +cell_text(Q2, 46, "îmbunătățire", 14, "bold", GREEN) +cell_text(Q2, 68, "sub 0, dar peste MA", 13, "normal", BLACK) +cell_text(Q2, 90, "LONG — zona de cumpărat", 14, "bold", GREEN) +p += sketch(Q2, 102, H2, GREEN) + +# fiecare schiță trebuie să arate exact cadranul pe care îl ilustrează +for _nume, _h, _peste0, _pesteMA in (("3", H3, True, False), ("1", H1, True, True), + ("4", H4, False, False), ("2", H2, False, True)): + _last, _ma = _h[-1], ma4(_h)[-1] + assert (_last > 0) == _peste0, f"schița cadranului {_nume}: ultima bară față de 0" + assert (_last > _ma) == _pesteMA, f"schița cadranului {_nume}: ultima bară față de MA" # ---- concluzia de sub matrice -add(W / 2, 442, "Long = cadranele 1-2 · Short = cadranele 3-4", +add(W / 2, 530, "Long = cadranele 1-2 · Short = cadranele 3-4", 14, "bold", BLACK) +add(W / 2, 548, "în fiecare cadran: histograma RS (barele), linia de 0 și media mobilă (punctat portocaliu)", + 13, "normal", GRAY) # ---- 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", +add(BAND["x"] + 20, 584, "Avertisment — cadranul 4 nu se tranzacționează pe orb", 15, "bold", RED, "start") -add(BAND["x"] + 20, 516, +add(BAND["x"] + 20, 612, "„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.”", +add(BAND["x"] + 20, 632, "ori un MP4-1 / MBT-10 long ca semn de schimbare.”", 13, "normal", BLACK, "start") -add(BAND["x"] + 20, 566, +add(BAND["x"] + 20, 662, "„Un setup de bottom cu RS fără îmbunătățire are șanse mai mici.”", 13, "bold", BLACK, "start") -add(BAND["x"] + 20, 596, +add(BAND["x"] + 20, 692, "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 —", +add(BAND["x"] + 20, 712, + "cadranul 3, care e altceva). Definiția din tabloul de mai sus e cea", 13, "normal", GRAY, "start") -add(BAND["x"] + 20, 636, - "03_MODUL_2/06_indicatorul_de_performanta.md:41-48.", +add(BAND["x"] + 20, 732, + "corectă și e singura folosită în tot capitolul.", 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'LONG") == 2, "LONG trebuie să apară exact de 2 ori" -assert svg.count(">SHORT") == 2, "SHORT trebuie să apară exact de 2 ori" +assert sum(t[2].startswith("LONG") for t in TEXTS) == 2, "LONG trebuie să apară de 2 ori" +assert sum(t[2].startswith("SHORT") for t in TEXTS) == 2, "SHORT trebuie să apară 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 @@ -260,10 +302,7 @@ for req in ("Relative strength — cele 4 cadrane", "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"): + "histograma RS (barele), linia de 0 și media mobilă"): assert req in svg, f"lipsește din desen: {req!r}" # 5. etichetele cadranului 2 și 3 care se pot încurca: verifică asocierea diff --git a/gen/strategii/selectia_cadrane.svg b/gen/strategii/selectia_cadrane.svg index f6e56ae..267de92 100644 --- a/gen/strategii/selectia_cadrane.svg +++ b/gen/strategii/selectia_cadrane.svg @@ -1,47 +1,92 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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) sub MA peste MA -peste 0 -sub 0 -Cadran 3 -înrăutățire -peste 0, dar sub MA -SHORT -Cadran 1 -supraperformanță -peste 0 și peste MA -LONG -Cadran 4 -subperformanță -sub 0 și sub MA -SHORT -stai pe margine -Cadran 2 -îmbunătățire -sub 0, dar peste MA -LONG -zona de cumpărat -Long = cadranele 1-2 · Short = cadranele 3-4 -Avertisment — cadranul 4 nu se tranzacționează pe orb -„Cadranul 4 nu se tranzacționează pe orb: aștepți trecerea în 2 sau 1, -ori un MP4-1 / MBT-10 long ca semn de schimbare.” -„Un setup de bottom cu RS fără îmbunătățire are șanse mai mici.” -Notă: transcrierea descrierii cadranului 4 are o eroare (îl descrie ca pe -cadranul 3); definiția de mai sus e completarea sumarului — -03_MODUL_2/06_indicatorul_de_performanta.md:41-48. -Surse: 08_ANALIZA_ZILNICA/11_selectia_simbolului.md:486-499, :501-508; -03_MODUL_2/06_indicatorul_de_performanta.md:31-39. +peste 0 +sub 0 +Cadran 3 +înrăutățire +peste 0, dar sub MA +SHORT +Cadran 1 +supraperformanță +peste 0 și peste MA +LONG +Cadran 4 +subperformanță +sub 0 și sub MA +SHORT — stai pe margine +Cadran 2 +îmbunătățire +sub 0, dar peste MA +LONG — zona de cumpărat +Long = cadranele 1-2 · Short = cadranele 3-4 +în fiecare cadran: histograma RS (barele), linia de 0 și media mobilă (punctat portocaliu) +Avertisment — cadranul 4 nu se tranzacționează pe orb +„Cadranul 4 nu se tranzacționează pe orb: aștepți trecerea în 2 sau 1, +ori un MP4-1 / MBT-10 long ca semn de schimbare.” +„Un setup de bottom cu RS fără îmbunătățire are șanse mai mici.” +Notă: transcrierea descrierii cadranului 4 are o eroare (îl descrie ca pe +cadranul 3, care e altceva). Definiția din tabloul de mai sus e cea +corectă și e singura folosită în tot capitolul. diff --git a/gen/strategii/selectia_exemplu.py b/gen/strategii/selectia_exemplu.py index c8eea80..711714c 100644 --- a/gen/strategii/selectia_exemplu.py +++ b/gen/strategii/selectia_exemplu.py @@ -15,7 +15,7 @@ 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 +from svg_candles import Candle, Chart, Level, Arrow, Zone, MA, from_closes # noqa: E402 OUT = Path(__file__).resolve().parent / "selectia_exemplu.svg" @@ -107,26 +107,33 @@ levels = [ 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), +zones = [ + Zone(10, 13, "ce l-a scos în față din cele 16 rezultate", RED, p_top=80.4), +] + +arrows = [ + Arrow(10, f"gap down: deschide la {cds[10].o:.2f}, mult sub\n" + f"închiderea de ieri — vânzare, nu pauză", + where="above", color=RED, dx=-160, dy=32), + Arrow(12, "sell doji close 78.60: vânzătorii și cumpărătorii la egalitate,\nîncă nu e semnal", + where="below", price=77.6, color="#333", dx=-120, dy=70), + Arrow(13, "aici ar intra strategia de intrare, nu selecția:\nînchidere sub low-ul doji-ului", + where="right", price=78.10, color=GREEN, dx=40, dy=110), ] 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.") +caption = ("Ale cursului: simbolul HIG, semnalul Sell Doji + Bear Reversal și RVOL-ul de 200 %, " + "cel mai mare din cele 16 rezultate ale scanerului. Nivelurile de preț " + "(79.20 / 78.60 / 78.10 / 82.00 / 72.00) și structura barelor sunt construite, la scară: " + "cursul nu dă cifre pentru HIG.") ch = Chart( - candles=cds, levels=levels, marks=marks, + candles=cds, levels=levels, zones=zones, arrows=arrows, 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) + title=title, + caption=caption, ) svg = ch.render() @@ -137,14 +144,28 @@ 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) -assert m_svg, "tag lipsa" -svg = svg.replace( - m_svg.group(0), - m_svg.group(0) + f'\n {title}', - 1, -) +# ---------------------------------------------------------------- RVOL, pe panoul de volum +# RVOL e o afirmație despre volum, deci eticheta stă pe panoul de volum, nu în +# cel de preț: linia întreruptă e media seriei, iar bara 10 e de două ori cât ea. +m_vb = re.search(r'', svg) +assert m_vb, "nu gasesc linia de baza a panoului de volum" +vol_base = float(m_vb.group(1)) +vmax = max(VOLS) +y_media = vol_base - media_vol / vmax * 70.0 # vol_h = 70 +y_bara = vol_base - VOLS[10] / vmax * 70.0 +assert abs((vol_base - y_bara) / (vol_base - y_media) - 2.0) < 1e-9, \ + "bara de RVOL nu e de doua ori media, desenata" +m_w = re.search(r'viewBox="0 0 (\d+) ', svg) +assert m_w +w_svg = int(m_w.group(1)) +vol_note = (f'' + f'' + f'media volumului pe serie' + f'' + f'ziua gap-ului: de {VOLS[10] / media_vol:.0f} ori media (RVOL 200 %) — ' + f'se vinde dublu față de o zi normală') +svg = svg.replace(m_vb.group(0), m_vb.group(0) + vol_note, 1) # ---------------------------------------------------------------- caption pe randuri def _wrap(text: str, max_chars: int) -> list[str]: diff --git a/gen/strategii/selectia_exemplu.svg b/gen/strategii/selectia_exemplu.svg index 711c529..b66faa5 100644 --- a/gen/strategii/selectia_exemplu.svg +++ b/gen/strategii/selectia_exemplu.svg @@ -1,113 +1,124 @@ - - HIG — daily (Sell Doji + Bear Reversal, RVOL 200%) - - rezistenta 84.50 - - SL 82.00 - - intrare short 78.10 - - TP 72.00 - - SMA-20 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - volum - gap down, open 79.20 - sell doji close 78.60 - intrare short 78.10 - RVOL 200% - 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. + + HIG — daily (Sell Doji + Bear Reversal, RVOL 200%) + + ce l-a scos în față din cele 16 rezultate + + rezistenta 84.50 + + SL 82.00 + + intrare short 78.10 + + TP 72.00 + + SMA-20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + media volumului pe serieziua gap-ului: de 2 ori media (RVOL 200 %) — se vinde dublu față de o zi normală + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + + + gap down: deschide la 79.20, mult sub + închiderea de ieri — vânzare, nu pauză + + + sell doji close 78.60: vânzătorii și cumpărătorii la egalitate, + încă nu e semnal + + + aici ar intra strategia de intrare, nu selecția: + închidere sub low-ul doji-ului + Ale cursului: simbolul HIG, semnalul Sell Doji + Bear Reversal și + RVOL-ul de 200 %, cel mai mare din cele 16 rezultate ale scanerului. + Nivelurile de preț (79.20 / 78.60 / 78.10 / 82.00 / 72.00) și + structura barelor sunt construite, la scară: cursul nu dă cifre pentru + HIG. \ No newline at end of file diff --git a/gen/strategii/selectia_funnel.py b/gen/strategii/selectia_funnel.py index e964f9e..4f752e4 100644 --- a/gen/strategii/selectia_funnel.py +++ b/gen/strategii/selectia_funnel.py @@ -1,19 +1,20 @@ #!/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`. +Diagramă conceptuală (NU grafic de preț): palnie cu 6 trepte care se +îngustează de sus în jos, apoi lista scurtă; cutie separată cu ce rămâne pe +nivelul 3. Pentru `summaries/17_STRATEGII/04_selectia.md`, nivelul 1 — harta +celor șase pași din nivelul 2. Scrie `gen/strategii/selectia_funnel.svg`. + +Fiecare treaptă spune pragul ȘI de ce există pragul; trimiterile la material +nu stau pe desen, ci în `summaries/17_STRATEGII/fise/04_selectia_surse.md`. 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. +Fiecare treaptă are titlul, pragul și motivul pragului. Toate cifrele din +desen (pragurile, „max ~300 de simboluri”, „top 10”) vin din material; nu se +inventează valori de RS sau de sectoare. Rulare: .venv/bin/python gen/strategii/selectia_funnel.py """ @@ -22,7 +23,7 @@ import re from pathlib import Path W = 700 -H = 1024 +H = 840 BLACK = "#000000" GRAY = "#777777" @@ -38,60 +39,53 @@ 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ă +NARROW = 30.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} +LIST_W, LIST_Y, LIST_H = 410.0, 566.0, 62.0 +NOTE = {"x": 40.0, "y": 648.0, "w": 620.0, "h": 176.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"]), + ("Universul și lichiditatea", + "close peste 30-40 $; volum mediu ≥ 500.000 de acțiuni, ideal peste 1 milion", + ["de ce: pe un simbol subțire, ordinul tău mută singur prețul"]), + ("Scorul fundamental", + "ATM score peste 7-8 (12-13 = spuma); F-score 6-9", + ["de ce: instituționalii cumpără bilanț, nu desen"]), ("Sectorul care domină", - "cele 11 sectoare S&P, sortate cadran 1 + valoarea RS", - ["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%", + "numeri din ce sector vin cele mai multe semnale", + ["de ce: multe semnale în același sector = bani care intră acolo"]), + ("Suport sau pivot, după downtrend", + "proximitate până la 3 % (4 % „e cam departe”)", + ["de ce: la un nivel, cumpărătorii au motiv să apară"]), + ("Relative strength: cadranul 1 sau 2", + "simbolul se mișcă mai bine decât indicele de referință", + ["de ce: cumperi ce urcă mai tare decât piața"]), + ("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"]), + ["de ce: confirmă că azi chiar participă lumea, nu doar tu"]), ] LIST = ("Listă scurtă", "câteva idei pe care le urmărești", - "08_ANALIZA_ZILNICA/11_selectia_simbolului.md:42-50") + "nicio poziție deschisă: trigger-ul e al altui capitol") 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ă." +CAPTION = "Palnia se îngustează de sus în jos: 6 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" +NOTE_TITLE = "Ce nu intră în cei șase pași (rafinări, nivelul 3)" +NOTE1 = ["Sortarea finală după 3 stele (Trinity: relative strength, Power și", + "volum, toate în cadranul 1) și departajarea între doi candidați", + "rămași: trend, apoi volum, apoi fundamental."] +NOTE1_SRC = "un scor de 7 nu e motiv să-l iei, dacă ai altul mai bun" +NOTE2 = ["Radarscreen (maximum ~300 de simboluri: cele 11 sectoare × top 10):", + "același flux, rulat pe o listă fixă, nu pe tot scanerul."] +NOTE2_SRC = "după o corecție mare, cauți doji sau hammer la suporturile mari" # ---------------------------------------------------------------- colect TEXTS = [] # (x, y, s, size, weight, fill, anchor) @@ -143,7 +137,7 @@ 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 +# ---- palnia: 6 trepte for i, (titlu, subtitlu, surse) in enumerate(STEPS): y = Y0 + i * HSTEP wt = TOPW - i * NARROW @@ -152,7 +146,7 @@ for i, (titlu, subtitlu, surse) in enumerate(STEPS): 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) + add(CX, y + 60 + k * 15, 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", @@ -165,21 +159,21 @@ add(CX, LIST_Y + 57, LIST[2], 13, "normal", GRAY) 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") +add(nx, NOTE["y"] + 26, NOTE_TITLE, 15, "bold", ORANGE, "start") +add(nx, NOTE["y"] + 50, NOTE1[0], 13, "normal", BLACK, "start") +add(nx, NOTE["y"] + 68, NOTE1[1], 13, "normal", BLACK, "start") +add(nx, NOTE["y"] + 86, NOTE1[2], 13, "normal", BLACK, "start") +add(nx, NOTE["y"] + 104, NOTE1_SRC, 13, "normal", GRAY, "start") +add(nx, NOTE["y"] + 130, NOTE2[0], 13, "normal", BLACK, "start") +add(nx, NOTE["y"] + 148, NOTE2[1], 13, "normal", BLACK, "start") +add(nx, NOTE["y"] + 166, NOTE2_SRC, 13, "normal", GRAY, "start") # =============================================================== EMITERE head = (f'') + f'role="img" aria-label="Palnia de selectie: 6 trepte de filtrare de la ' + f'scanner la lista scurta, fiecare cu pragul ei si cu motivul pentru ' + f'care exista pragul, plus rafinarile de la nivelul 3">') body = [head, f''] body += p for x, y, s, size, weight, fill, anchor in TEXTS: @@ -189,10 +183,10 @@ 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)}" +assert len(STEPS) == 6, f"aștept 6 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 +assert len(traps) == 6, f"aștept 6 trapezoide, am {len(traps)}" +assert len(WIDTHS) == 6 for i, (wt, wb) in enumerate(WIDTHS): assert wt > wb, f"treapta {i} nu se îngustează" if i: @@ -208,7 +202,8 @@ for i, (titlu, subtitlu, surse) in enumerate(STEPS): 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 src in svg, f"lipsește «de ce»-ul treptei {i + 1}: {src!r}" + assert surse[0].startswith("de ce:"), f"treapta {i + 1} nu spune de ce" 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 @@ -219,7 +214,7 @@ 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): +for i in range(len(STEPS)): ytop = Y0 + i * HSTEP ybot = ytop + HSTEP wb = TOPW - (i + 1) * NARROW @@ -259,7 +254,7 @@ for i in range(len(boxes)): 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 LIST_Y >= Y0 + len(STEPS) * 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ă" @@ -270,9 +265,9 @@ assert " - - - - - - - - - - - + + + + + + + + + + Fluxul de selecție — de la scanner la lista scurtă selecția se face înainte de semnalul tehnic; ea nu spune CÂND intri -Palnia se îngustează de sus în jos: 8 filtre, apoi lista scurtă. -1. Scanner — multe semnale -zeci sau sute de semnale -08_ANALIZA_ZILNICA/11_selectia_simbolului.md:631-637 -2. 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 -3. Fundamental -ATM score peste 7-8; 12-13 = spuma; F-score 6-9 -08_ANALIZA_ZILNICA/11_selectia_simbolului.md:199-221 -4. Sectorul care domină -cele 11 sectoare S&P, sortate cadran 1 + valoarea RS -08_ANALIZA_ZILNICA/11_selectia_simbolului.md:639-717 -5. Doar suport / pivot, după downtrend -suport monthly, quarterly, anual sau pivot; proximitate ≤ 3% -08_ANALIZA_ZILNICA/11_selectia_simbolului.md:719-728, 787-803 -6. Relative strength: cadran 1-2 -doar cadranele 1 și 2, RS pozitiv -08_ANALIZA_ZILNICA/11_selectia_simbolului.md:730-739 -7. Relative volume peste 100% -candela de volum / media ultimelor 10 candele -08_ANALIZA_ZILNICA/11_selectia_simbolului.md:741-747 -8. Stele (Trinity) -RS + Power + volum, cadran 1 -08_ANALIZA_ZILNICA/11_selectia_simbolului.md:776-785 -Listă scurtă -câteva idei pe care le urmărești -08_ANALIZA_ZILNICA/11_selectia_simbolului.md:42-50 -Note laterale -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). -08_ANALIZA_ZILNICA/11_selectia_simbolului.md:787-803 -Radarscreen (max ~300 simboluri): repetă fluxul — sectoare top 10, -RS, apoi 3 stele. -08_ANALIZA_ZILNICA/11_selectia_simbolului.md:815-825 +Palnia se îngustează de sus în jos: 6 filtre, apoi lista scurtă. +1. Universul și lichiditatea +close peste 30-40 $; volum mediu ≥ 500.000 de acțiuni, ideal peste 1 milion +de ce: pe un simbol subțire, ordinul tău mută singur prețul +2. Scorul fundamental +ATM score peste 7-8 (12-13 = spuma); F-score 6-9 +de ce: instituționalii cumpără bilanț, nu desen +3. Sectorul care domină +numeri din ce sector vin cele mai multe semnale +de ce: multe semnale în același sector = bani care intră acolo +4. Suport sau pivot, după downtrend +proximitate până la 3 % (4 % „e cam departe”) +de ce: la un nivel, cumpărătorii au motiv să apară +5. Relative strength: cadranul 1 sau 2 +simbolul se mișcă mai bine decât indicele de referință +de ce: cumperi ce urcă mai tare decât piața +6. Relative volume peste 100 % +candela de volum / media ultimelor 10 candele +de ce: confirmă că azi chiar participă lumea, nu doar tu +Listă scurtă +câteva idei pe care le urmărești +nicio poziție deschisă: trigger-ul e al altui capitol +Ce nu intră în cei șase pași (rafinări, nivelul 3) +Sortarea finală după 3 stele (Trinity: relative strength, Power și +volum, toate în cadranul 1) și departajarea între doi candidați +rămași: trend, apoi volum, apoi fundamental. +un scor de 7 nu e motiv să-l iei, dacă ai altul mai bun +Radarscreen (maximum ~300 de simboluri: cele 11 sectoare × top 10): +același flux, rulat pe o listă fixă, nu pe tot scanerul. +după o corecție mare, cauți doji sau hammer la suporturile mari diff --git a/gen/strategii/selectia_proximitate.py b/gen/strategii/selectia_proximitate.py new file mode 100644 index 0000000..c6fea79 --- /dev/null +++ b/gen/strategii/selectia_proximitate.py @@ -0,0 +1,171 @@ +"""Pasul 4 — cât de aproape de suport e prețul (TIP: PRET). + +Pentru `summaries/17_STRATEGII/04_selectia.md`, pasul 4. Un singur grafic +daily, cu suportul monthly și cu banda de 3 % de deasupra lui, desenată ca +dreptunghi: înăuntru simbolul e „la nivel", în afară e „în aer". Două momente +din aceeași serie, unul în fiecare parte, ca să se vadă diferența pe care +ochiul n-o prinde. + +Pragul de 3 % (cu 4 % „cam departe") e al cursului. Seria e construită, la +scară; toate procentele din etichete se calculează din ea, cu assert. + +Atenție la diferența față de capitolul Buy Doji: acolo pragul de 1-2 % se +măsoară pentru low-ul doji-ului. Aici măsori închiderea simbolului față de +suport, ca filtru de selecție — două măsurători diferite, praguri diferite. + +Rulează: + python3 gen/strategii/selectia_proximitate.py +""" +import re +import sys +from pathlib import Path + +sys.path.insert(0, "/workspace/atm-curs-dl") +from svg_candles import Chart, Level, Arrow, Zone, MA, from_closes # noqa: E402 + +OUT = Path(__file__).resolve().parent / "selectia_proximitate.svg" + +GRAY, RED, GREEN, BLUE, ORANGE = "#777", "#c00", "#188a3e", "#1a5fb4", "#b06000" + +SUPORT = 48.00 +PRAG = 0.03 # 3 % — pragul cursului +BANDA_SUS = SUPORT * (1 + PRAG) # 49.44 + +DEPARTE, APROAPE = 10, 30 # barele comentate + +CLOSES = [ + 56.00, 55.60, 55.10, 54.70, 54.20, # 0-4 + 53.80, 53.30, 52.90, 52.40, 52.00, # 5-9 + 51.60, 51.20, 50.90, 50.50, 50.20, # 10-14 + 49.90, 50.30, 50.00, 49.70, 49.40, # 15-19 + 49.10, 49.50, 49.20, 48.90, 49.20, # 20-24 + 48.90, 48.70, 49.00, 48.70, 48.50, # 25-29 + 48.80, 49.10, # 30-31 +] +assert len(CLOSES) == 32, len(CLOSES) + +WICKS = [(0.22, 0.24)] * 32 +WICKS[0] = (0.30, 0.30) +WICKS[29] = (0.20, 0.50) # low exact pe suport: 48.50 - 0.50 = 48.00 +assert len(WICKS) == 32 + +VOLS = [46, 44, 48, 45, 43, 47, 44, 42, 45, 41, + 40, 42, 39, 41, 38, 44, 36, 38, 35, 37, + 34, 40, 33, 36, 32, 35, 31, 34, 33, 48, + 44, 46] +assert len(VOLS) == 32 + +cds = from_closes(CLOSES, wicks=WICKS, vols=VOLS, start=56.30) + +# ---------------------------------------------------------------- assert-uri de conținut +for a, b in zip(cds, cds[1:]): + assert abs(a.c - b.o) < 1e-9, "serie cu gol artificial între bare" +assert abs(min(c.l for c in cds) - SUPORT) < 1e-9, "suportul nu e atins de niciun low" + +pct_departe = (cds[DEPARTE].c - SUPORT) / SUPORT * 100 +pct_aproape = (cds[APROAPE].c - SUPORT) / SUPORT * 100 +assert pct_departe > PRAG * 100, "bara «departe» trebuie să fie peste prag" +assert pct_aproape < PRAG * 100, "bara «aproape» trebuie să fie sub prag" +assert abs(pct_departe - 7.50) < 0.01, pct_departe +assert abs(pct_aproape - 1.67) < 0.01, pct_aproape +# banda acoperă exact ce trebuie: bara «aproape» înăuntru, bara «departe» afară +assert SUPORT <= cds[APROAPE].c <= BANDA_SUS, "bara «aproape» nu e în bandă" +assert cds[DEPARTE].c > BANDA_SUS, "bara «departe» nu e în afara benzii" +# numărul din textul zonei == numărul de bare pe care le acoperă +in_banda = [i for i, c in enumerate(cds) if SUPORT <= c.c <= BANDA_SUS] +assert in_banda and in_banda[0] == 19, in_banda[:3] + +levels = [ + Level(SUPORT, f"suport monthly {SUPORT:.2f}", BLUE, label_at="left"), + Level(BANDA_SUS, f"suport + 3 % = {BANDA_SUS:.2f}", BLUE, dash="5 3"), +] + +zones = [ + # eticheta benzii o pun manual, după randare: banda ține toată lățimea, iar + # eticheta automată ar urca deasupra primei candele, peste titlu + Zone(0, 31, "", BLUE, p_top=BANDA_SUS, p_bot=SUPORT), +] + +arrows = [ + Arrow(DEPARTE, f"close {cds[DEPARTE].c:.2f} = {pct_departe:.2f} % peste suport\n" + f"prea departe: până acolo prețul mai are de căzut,\n" + f"deci nivelul nu-l poate opri acum", + where="above", color=RED, dx=-60, dy=-4), + Arrow(APROAPE, f"close {cds[APROAPE].c:.2f} = {pct_aproape:.2f} % peste suport\n" + f"la nivel: dacă mai scade puțin, dă de ordinele\n" + f"de cumpărare care au mai întors prețul de acolo", + where="below", color=GREEN, dx=-320, dy=42), +] + +caption = ("Pragul de 3 % (cu 4 % „cam departe”) e al cursului; seria și prețurile sunt construite, " + "la scară, iar procentele sunt calculate din serie. În capitolul Buy Doji, pragul de " + "1-2 % măsoară altceva: low-ul doji-ului față de nivel, nu închiderea simbolului.") + +ch = Chart( + candles=cds, levels=levels, zones=zones, arrows=arrows, + mas=[MA(20, ORANGE, dash="4 3", text="SMA-20")], + scale=26.0, gap=15, body_w=9, left=44, width=660, vol_h=60, + vol_label="volum", + title="Cât de aproape de suport e prețul — daily, exemplu construit", + caption=caption, +) +svg = ch.render() + +# eticheta benzii, pusă în interiorul ei, în stânga +m_band = re.findall(r' y_banda_sus, "banda e desenată invers" +svg = svg.replace("", + f'banda de 3 %: înăuntru simbolul e „la nivel”' + "") + +# „SMA-20" cade peste eticheta nivelului de bandă: o ridic pe rândul liber +svg, n = re.subn(r'(SMA-20)', + lambda mm: f'{mm.group(1)}{float(mm.group(2)) - 16:.1f}{mm.group(3)}', svg) +assert n == 1, f"eticheta SMA-20: {n} potriviri" + +# liniile nu au voie să cadă pe marginea cadrului +m_sup = re.findall(r' 0.06 * h_svg, "linia suportului e lipită de marginea de sus" + +# nicio etichetă nu numește un preț care nu e desenat +desenate = {f"{SUPORT:.2f}", f"{BANDA_SUS:.2f}", f"{cds[DEPARTE].c:.2f}", f"{cds[APROAPE].c:.2f}", + f"{pct_departe:.2f}", f"{pct_aproape:.2f}"} +for t in re.findall(r"]*>([^<]*)", svg): + for nr in re.findall(r"\d+\.\d\d", t): + assert nr in desenate, f"eticheta numește {nr}, care nu e desenat: {t!r}" + + +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, 96) +m = re.search(r'' + re.escape(caption) + r"", svg) +assert m, "caption-ul nu a fost găsit în SVG" +y0 = int(m.group(1)) +wrapped = "\n ".join(f'{ln}' + for i, ln in enumerate(cap_lines)) +svg = svg[:m.start()] + wrapped + svg[m.end():] +svg = svg.replace(m_vb.group(0), + f'viewBox="0 0 {m_vb.group(1)} {h_svg + 17 * (len(cap_lines) - 1)}"', 1) + +assert " + Cât de aproape de suport e prețul — daily, exemplu construit + + + suport monthly 48.00 + + suport + 3 % = 49.44 + + SMA-20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + volum + + + close 51.60 = 7.50 % peste suport + prea departe: până acolo prețul mai are de căzut, + deci nivelul nu-l poate opri acum + + + close 48.80 = 1.67 % peste suport + la nivel: dacă mai scade puțin, dă de ordinele + de cumpărare care au mai întors prețul de acolo + Pragul de 3 % (cu 4 % „cam departe”) e al cursului; seria și prețurile sunt construite, la + scară, iar procentele sunt calculate din serie. În capitolul Buy Doji, pragul de 1-2 % măsoară + altceva: low-ul doji-ului față de nivel, nu închiderea simbolului. +banda de 3 %: înăuntru simbolul e „la nivel” \ No newline at end of file diff --git a/gen/strategii/selectia_scor.py b/gen/strategii/selectia_scor.py index f6551ac..3d8d583 100644 --- a/gen/strategii/selectia_scor.py +++ b/gen/strategii/selectia_scor.py @@ -18,7 +18,7 @@ Rulare: .venv/bin/python gen/strategii/selectia_scor.py import html from pathlib import Path -W, H = 700, 610 +W, H = 700, 546 BLACK = "#000000" GRAY = "#777777" @@ -111,7 +111,8 @@ p.append(rect(X12, BAR_Y - 6, X13 - X12, BAR_H + 12, "spuma", 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) +# eticheta stă în stânga casetei punctate 12-13, altfel se suprapune peste ea +add((X_SPLIT + X12) / 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): @@ -159,7 +160,7 @@ 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.", + "Îl vezi în coloana de scor a scanerului; dai refresh și îl recitești.", 13, "normal", GRAY, "start") add(60, 498, "„peste 7-8” e formularea exactă a cursului. Sub 7 NU e excludere:", @@ -168,17 +169,6 @@ add(60, 518, "la 4/14 și 6/14 continuă analiza; scorul departajează candidații.", 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' - + + @@ -35,7 +35,7 @@ value-score 0-5 5/14 din bară F-score -value-score +value-score 0 1 2 @@ -68,10 +68,7 @@ puternic Scor -1 = date indisponibile în platformă, NU scor 0. Scorul -1 nu se citește ca 0. -16_ANALIZA_FUNDAMENTALA/09_implementare_f_score_tradestation.md:45-46, 268-269. +Îl vezi în coloana de scor a scanerului; dai refresh și îl recitești. „peste 7-8” e formularea exactă a cursului. Sub 7 NU e excludere: la 4/14 și 6/14 continuă analiza; scorul departajează candidații. -Surse: 08_ANALIZA_ZILNICA/11_selectia_simbolului.md:158-164, :199-204; -16_ANALIZA_FUNDAMENTALA/08_piotrowski_f_score.md:70-72; -16_ANALIZA_FUNDAMENTALA/09_implementare_f_score_tradestation.md:45-46, 268-269. diff --git a/gen/strategii/selectia_sectoare.py b/gen/strategii/selectia_sectoare.py new file mode 100644 index 0000000..395f652 --- /dev/null +++ b/gen/strategii/selectia_sectoare.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Pasul 3 — sectorul care domină: numeri semnalele pe sectoare. + +Diagramă conceptuală (NU grafic de preț), pentru +`summaries/17_STRATEGII/04_selectia.md`, pasul 3. Cele 11 sectoare S&P, cu +câte semnale a dat scanerul în fiecare într-o singură zi; sectorul cu cele mai +multe e evidențiat, pentru că acolo intră banii. + +Ce vine din material: cele 11 sectoare S&P și ETF-urile lor, regula „uită-te +ce predomină, în ce sectoare" și sortarea sectoarelor după cadranul 1 plus +valoarea relative strength. Ce e al meu: **numărul de semnale din fiecare +sector**. Cursul nu dă o zi cu cifre pe sectoare, așa că le-am construit; ele +ilustrează operațiunea (numărarea), nu o zi reală de piață. Scrie asta și în +desen, ca să nu fie luate drept date. + +Nu folosește `svg_candles`: SVG-ul e construit ca string, doar stdlib. + +Rulare: python3 gen/strategii/selectia_sectoare.py +""" +import html +from pathlib import Path + +W, H = 700, 620 + +BLACK, GRAY, RED, GREEN, BLUE, ORANGE = "#000000", "#777777", "#c00", "#188a3e", "#1a5fb4", "#b06000" +BLUE_BG = "#eef3fb" + +# (sector, ETF, semnale — numărul e construit de mine) +SECTOARE = [ + ("Sănătate", "XLV", 9), + ("Energie", "XLE", 6), + ("Financiar", "XLF", 5), + ("Real estate", "XLRE", 4), + ("Tehnologie", "XLK", 3), + ("Industrial", "XLI", 3), + ("Consum de bază", "XLP", 2), + ("Consum discreționar", "XLY", 2), + ("Utilități", "XLU", 1), + ("Materiale", "XLB", 1), + ("Comunicații", "XLC", 1), +] + +X_NUME = 40.0 +X0, X1 = 250.0, 600.0 # zona barelor +Y0, ROW = 132.0, 30.0 + +TEXTS = [] + + +def add(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"): + TEXTS.append((x, y, s, size, weight, fill, anchor)) + + +def est_w(s, size, weight): + return len(s) * size * (0.62 if weight == "bold" else 0.56) + + +def txt(x, y, s, size, weight, fill, anchor): + return (f'{html.escape(s, quote=False)}') + + +p = [] +MAXS = max(s for *_, s in SECTOARE) +assert SECTOARE[0][2] == MAXS, "prima linie trebuie să fie sectorul dominant" +assert sum(1 for *_, s in SECTOARE if s == MAXS) == 1, "dominantul nu e unic" +assert len(SECTOARE) == 11, "S&P are 11 sectoare" +assert all(a[2] >= b[2] for a, b in zip(SECTOARE, SECTOARE[1:])), "lista nu e sortată descrescător" + +add(W / 2, 34, "Pasul 3 — din ce sector vin cele mai multe semnale", 20, "bold") +add(W / 2, 58, "cele 11 sectoare S&P, cu semnalele scanerului dintr-o singură zi", + 13, "normal", GRAY) +add(W / 2, 78, "de ce: mai multe semnale în același sector = cineva mută bani acolo", + 13, "normal", GRAY) +add(X_NUME, 110, "sector (ETF)", 13, "bold", GRAY, "start") +add(X0, 110, "semnale în ziua asta", 13, "bold", GRAY, "start") + +# dreptunghiul de evidențiere peste sectorul dominant +p.append(f'') + +for i, (nume, etf, n) in enumerate(SECTOARE): + y = Y0 + i * ROW + dominant = n == MAXS + col = BLUE if dominant else GRAY + add(X_NUME, y, f"{nume} ({etf})", 13, "bold" if dominant else "normal", col, "start") + w = n / MAXS * (X1 - X0) + p.append(f'') + add(X0 + w + 8, y, str(n), 13, "bold" if dominant else "normal", col, "start") + +# săgeata care spune de ce +Y_ARR = Y0 - 6 +p.append(f'') +add(X1 + 80, Y_ARR - 30, "aici te uiți", 13, "bold", BLUE, "end") + +Y_NOTE = Y0 + len(SECTOARE) * ROW + 26 +p.append(f'') +add(60, Y_NOTE + 26, "Ce faci cu asta", 15, "bold", ORANGE, "start") +add(60, Y_NOTE + 48, "Păstrezi simbolurile din sectorul dominant și le duci mai departe la pasul 4;", + 13, "normal", BLACK, "start") +add(60, Y_NOTE + 66, "restul le lași pentru azi. Sectoarele se mai sortează și după cadranul 1 plus", + 13, "normal", BLACK, "start") +add(60, Y_NOTE + 84, "valoarea relative strength — aceleași cadrane ca la pasul 5.", + 13, "normal", BLACK, "start") + +add(W / 2, H - 14, "Numărul de semnale pe sector e construit de mine: cursul dă regula, nu o zi cu cifre.", + 13, "normal", GRAY) + +head = (f'') +body = [head, f''] + p +body += [txt(*t) for t in TEXTS] +body.append("") +svg = "\n".join(body) + "\n" + +# =============================================================== VERIFICARE +# lungimea barei e proporțională cu numărul din etichetă +for i, (nume, etf, n) in enumerate(SECTOARE): + w = n / MAXS * (X1 - X0) + assert abs(w / (X1 - X0) - n / MAXS) < 1e-9, f"{nume}: bara nu e la scară" + assert X0 + w <= X1 + 1e-9, f"{nume}: bara iese din zona ei" + +# etichete în pânză, 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) + left = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x) + assert -2 <= left and left + w <= W + 2, f"text în afara pânzei: {s!r}" + assert 0 <= y - size * 0.78 and y + size * 0.22 <= H, f"text în afara pânzei: {s!r}" + boxes.append((left, left + w, y - size * 0.78, y + size * 0.22, 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}") + +# numele de sector nu intră peste zona barelor +for x, y, s, size, weight, fill, anchor in TEXTS: + if anchor == "start" and x == X_NUME and y >= Y0: + assert x + est_w(s, size, weight) < X0 - 6, f"numele iese peste bare: {s!r}" + +assert "\n\n" not in svg, "linie goală în SVG" +OUT = Path(__file__).with_suffix(".svg") +OUT.write_text(svg, encoding="utf-8") +print(f"scris {OUT.name} ({len(svg)} bytes, {len(TEXTS)} etichete, " + f"{len(SECTOARE)} sectoare, dominant {SECTOARE[0][0]} cu {MAXS}, {W}x{H}), verificare OK") diff --git a/gen/strategii/selectia_sectoare.svg b/gen/strategii/selectia_sectoare.svg new file mode 100644 index 0000000..a4e1fcb --- /dev/null +++ b/gen/strategii/selectia_sectoare.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + +Pasul 3 — din ce sector vin cele mai multe semnale +cele 11 sectoare S&P, cu semnalele scanerului dintr-o singură zi +de ce: mai multe semnale în același sector = cineva mută bani acolo +sector (ETF) +semnale în ziua asta +Sănătate (XLV) +9 +Energie (XLE) +6 +Financiar (XLF) +5 +Real estate (XLRE) +4 +Tehnologie (XLK) +3 +Industrial (XLI) +3 +Consum de bază (XLP) +2 +Consum discreționar (XLY) +2 +Utilități (XLU) +1 +Materiale (XLB) +1 +Comunicații (XLC) +1 +aici te uiți +Ce faci cu asta +Păstrezi simbolurile din sectorul dominant și le duci mai departe la pasul 4; +restul le lași pentru azi. Sectoarele se mai sortează și după cadranul 1 plus +valoarea relative strength — aceleași cadrane ca la pasul 5. +Numărul de semnale pe sector e construit de mine: cursul dă regula, nu o zi cu cifre. + diff --git a/gen/strategii/selectia_univers.py b/gen/strategii/selectia_univers.py new file mode 100644 index 0000000..a22242d --- /dev/null +++ b/gen/strategii/selectia_univers.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Pasul 1 — universul și lichiditatea: cele două praguri, desenate ca o zonă. + +Diagramă conceptuală (NU grafic de preț), pentru +`summaries/17_STRATEGII/04_selectia.md`, pasul 1. Pe orizontală prețul de +închidere, pe verticală volumul mediu zilnic. Pragurile cursului (close peste +30-40 $, volum mediu de cel puțin 500.000 de acțiuni, ideal peste un milion) +taie planul în patru; dreptunghiul verde e singurul colț din care ai voie să +iei simboluri. + +Cele trei simboluri sunt anonime (A, B, C) și cu cifre construite de mine: +cursul dă pragurile, nu exemple numerice. Le pun ca să se vadă că un simbol +poate pica pe oricare din cele două praguri, separat. + +Nu folosește `svg_candles`: SVG-ul e construit ca string, doar stdlib. + +Rulare: python3 gen/strategii/selectia_univers.py +""" +import html +from pathlib import Path + +W, H = 700, 604 + +BLACK, GRAY, RED, GREEN, BLUE, ORANGE = "#000000", "#777777", "#c00", "#188a3e", "#1a5fb4", "#b06000" +GREEN_BG = "#eef7f0" + +# ---------------------------------------------------------------- praguri (cursul) +PRET_MIN, PRET_IDEAL = 30.0, 40.0 # close peste 30-40 $ +VOL_MIN, VOL_IDEAL = 0.5, 1.0 # milioane de acțiuni pe zi + +# ---------------------------------------------------------------- simboluri (construite) +# (nume, preț, volum mediu în milioane, verdict) +SIMB = [ + ("A", 58.0, 2.4, "trece"), + ("B", 24.0, 3.1, "cade pe preț"), + ("C", 72.0, 0.18, "cade pe volum"), +] + +# ---------------------------------------------------------------- geometrie +X0, X1 = 110.0, 630.0 +Y0, Y1 = 130.0, 420.0 +PMAX, VMAX = 100.0, 3.5 + + +def px(p): + return X0 + p / PMAX * (X1 - X0) + + +def py(v): + return Y1 - v / VMAX * (Y1 - Y0) + + +TEXTS = [] + + +def add(x, y, s, size=13, weight="normal", fill=BLACK, anchor="middle"): + TEXTS.append((x, y, s, size, weight, fill, anchor)) + + +def est_w(s, size, weight): + return len(s) * size * (0.62 if weight == "bold" else 0.56) + + +def txt(x, y, s, size, weight, fill, anchor): + return (f'{html.escape(s, quote=False)}') + + +def line(x1, y1, x2, y2, stroke=GRAY, w=1.0, dash=None): + d = f' stroke-dasharray="{dash}"' if dash else "" + return (f'') + + +p = [] + +add(W / 2, 34, "Pasul 1 — cine intră în universul de scanare", 20, "bold") +add(W / 2, 58, "pe orizontală prețul de închidere, pe verticală volumul mediu zilnic", + 13, "normal", GRAY) +add(W / 2, 78, "de ce pragurile: pe un simbol subțire, ordinul tău mută singur prețul", + 13, "normal", GRAY) + +# zona verde: singurul colț bun +p.append(f'') +add(px(PRET_MIN) + 10, Y0 + 22, "zona din care iei simboluri", 14, "bold", GREEN, "start") + +# cadrul și axele +p.append(line(X0, Y0, X0, Y1, GRAY, 1.2)) +p.append(line(X0, Y1, X1, Y1, GRAY, 1.2)) + +# pragurile +p.append(line(px(PRET_MIN), Y0, px(PRET_MIN), Y1, GREEN, 1.8)) +p.append(line(px(PRET_IDEAL), Y0, px(PRET_IDEAL), Y1, GREEN, 1.2, "5 3")) +p.append(line(X0, py(VOL_MIN), X1, py(VOL_MIN), GREEN, 1.8)) +p.append(line(X0, py(VOL_IDEAL), X1, py(VOL_IDEAL), GREEN, 1.2, "5 3")) + +add(px(PRET_MIN), Y1 + 38, "30 $", 13, "bold", GREEN) +add(px(PRET_IDEAL), Y1 + 56, "40 $ (ideal)", 13, "normal", GREEN) +add(X0 - 10, py(VOL_MIN) + 4, "500.000", 13, "bold", GREEN, "end") +add(X0 - 10, py(VOL_IDEAL) + 4, "1 milion", 13, "normal", GREEN, "end") + +# repere de axă +for v in (0, 1, 2, 3): + p.append(line(X0 - 5, py(v), X0, py(v), GRAY, 1.0)) +add(X0 - 62, py(0) + 4, "0", 13, "normal", GRAY, "end") +add(X0 - 62, py(2) + 4, "2 mil.", 13, "normal", GRAY, "end") +add(X0 - 62, py(3) + 4, "3 mil.", 13, "normal", GRAY, "end") +for pr in (0, 50, 100): + p.append(line(px(pr), Y1, px(pr), Y1 + 5, GRAY, 1.0)) + add(px(pr), Y1 + 20, f"{pr} $", 13, "normal", GRAY) + +add(X1, Y1 + 56, "prețul de închidere", 13, "normal", GRAY, "end") +add(X0 - 48, Y0 - 12, "volum mediu zilnic", 13, "normal", GRAY, "start") + +# cele trei simboluri +for nume, pret, vol, verdict in SIMB: + x, y = px(pret), py(vol) + col = GREEN if verdict == "trece" else RED + p.append(f'') + add(x, y - 14, f"Simbol {nume}", 13, "bold", col) + add(x, y + 26, f"{pret:.0f} $ · {vol:.2f} mil.", 13, "normal", BLACK) + add(x, y + 44, verdict, 13, "bold", col) + +# caseta cu de ce +NOTE_Y = 504.0 +p.append(f'') +add(60, NOTE_Y + 26, "De ce contează amândouă, nu doar una", 15, "bold", ORANGE, "start") +add(60, NOTE_Y + 48, + "Simbolul B se tranzacționează mult, dar e prea ieftin; simbolul C e scump, dar", + 13, "normal", BLACK, "start") +add(60, NOTE_Y + 66, + "aproape nimeni nu-l cumpără: acolo ordinul tău e cel care mișcă prețul.", + 13, "normal", BLACK, "start") + +head = (f'') +body = [head, f''] + p +body += [txt(*t) for t in TEXTS] +body.append("") +svg = "\n".join(body) + "\n" + +# =============================================================== VERIFICARE +# 1. verdictul fiecărui simbol iese din cifrele lui, nu e scris de mână +for nume, pret, vol, verdict in SIMB: + trece = pret >= PRET_MIN and vol >= VOL_MIN + assert trece == (verdict == "trece"), f"Simbol {nume}: verdictul nu iese din cifre" + if verdict == "cade pe preț": + assert pret < PRET_MIN and vol >= VOL_MIN, f"Simbol {nume} nu cade doar pe preț" + if verdict == "cade pe volum": + assert vol < VOL_MIN and pret >= PRET_MIN, f"Simbol {nume} nu cade doar pe volum" +assert sum(1 for *_, v in SIMB if v == "trece") == 1, "aștept exact un simbol care trece" + +# 2. zona verde e exact colțul „peste ambele praguri" +assert px(PRET_MIN) > X0 and py(VOL_MIN) < Y1, "zona verde nu e un colț al planului" +for nume, pret, vol, verdict in SIMB: + inauntru = px(PRET_MIN) <= px(pret) <= X1 and Y0 <= py(vol) <= py(VOL_MIN) + assert inauntru == (verdict == "trece"), f"Simbol {nume}: punctul nu e desenat în zona lui" + +# 3. toate punctele încap în cadru +for nume, pret, vol, _ in SIMB: + assert X0 < px(pret) < X1, f"Simbol {nume}: prețul iese din cadru" + assert Y0 < py(vol) < Y1, f"Simbol {nume}: volumul iese din cadru" + +# 4. liniile de prag nu cad pe marginea cadrului +for x in (px(PRET_MIN), px(PRET_IDEAL)): + assert x - X0 > 20 and X1 - x > 20, "linie de prag lipită de marginea cadrului" +for y in (py(VOL_MIN), py(VOL_IDEAL)): + assert y - Y0 > 20 and Y1 - y > 20, "linie de prag lipită de marginea cadrului" + +# 5. etichete în pânză și 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) + left = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x) + assert -2 <= left and left + w <= W + 2, f"text în afara pânzei: {s!r}" + assert 0 <= y - size * 0.78 and y + size * 0.22 <= H, f"text în afara pânzei: {s!r}" + boxes.append((left, left + w, y - size * 0.78, y + size * 0.22, 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}") + +assert "\n\n" not in svg, "linie goală în SVG" +OUT = Path(__file__).with_suffix(".svg") +OUT.write_text(svg, encoding="utf-8") +print(f"scris {OUT.name} ({len(svg)} bytes, {len(TEXTS)} etichete, " + f"{len(SIMB)} simboluri, {W}x{H}), verificare OK") diff --git a/gen/strategii/selectia_univers.svg b/gen/strategii/selectia_univers.svg new file mode 100644 index 0000000..4abd947 --- /dev/null +++ b/gen/strategii/selectia_univers.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + +Pasul 1 — cine intră în universul de scanare +pe orizontală prețul de închidere, pe verticală volumul mediu zilnic +de ce pragurile: pe un simbol subțire, ordinul tău mută singur prețul +zona din care iei simboluri +30 $ +40 $ (ideal) +500.000 +1 milion +0 +2 mil. +3 mil. +0 $ +50 $ +100 $ +prețul de închidere +volum mediu zilnic +Simbol A +58 $ · 2.40 mil. +trece +Simbol B +24 $ · 3.10 mil. +cade pe preț +Simbol C +72 $ · 0.18 mil. +cade pe volum +De ce contează amândouă, nu doar una +Simbolul B se tranzacționează mult, dar e prea ieftin; simbolul C e scump, dar +aproape nimeni nu-l cumpără: acolo ordinul tău e cel care mișcă prețul. +