"""Trei stări de cash flow, din `summaries/08_ANALIZA_ZILNICA/11_selectia_simbolului.md` (secțiunea 1.5 Cash flow): - pozitiv și în creștere = se finanțează, stă în picioare -> îl cauți - pozitiv, dar în scădere = deteriorare -> nu-l pui pe lista de investiție - negativ = speculă pe momentum, nu investiție La ETF-uri, ETF-urile nu au scor, dar nu intră în această diagramă. SVG scris de mână (nu e grafic de preț, deci NU se folosesc svg_candles / pchart). Fără dependințe: doar stdlib. Rulare: .venv/bin/python gen/selectia/cash_flow.py Scrie `gen/selectia/cash_flow.svg`. Notă de conținut: textul nu dă praguri, procente sau cod de culoare (verde/roșu ca semafor), deci diagrama nu inventează niciunul. Toate cifrele care apar în desen sunt referințe de citat (fișier:linii) existente în text. """ from pathlib import Path from xml.sax.saxutils import escape OUT = Path(__file__).resolve().parent / "cash_flow.svg" BLACK = "#000000" GRAY = "#777777" BLUE = "#1a5fb4" W = 700 MARGIN = 30 GAP = 20 CARD_W = 200 CARD_TOP = 80 CARD_H = 216 XS = [MARGIN + i * (CARD_W + GAP) for i in range(3)] # 30, 250, 470 TITLE_Y = 34 SUB_Y = 56 # Fiecare card: header (poate fi pe două rânduri), "ce înseamnă" (2 rânduri), # acțiunea (1-2 rânduri, în caseta de jos) și sursa. CARDS = [ { "header": ["Pozitiv", "și în creștere"], "meaning": ["se finanțează,", "stă în picioare"], "action": ["Îl cauți chiar și la", "companiile pe", "pierdere"], "source": "66_18052026.txt:120-121", }, { "header": ["Pozitiv, dar", "în scădere"], "meaning": ["deși e pozitiv,", "s-a deteriorat"], "action": ["Nu-l pui pe lista", "de investiție"], "source": "72_08052026.txt:241-243", }, { "header": ["Negativ"], "meaning": ["speculă pe momentum"], "action": ["Nu investiție"], "source": "„Ce faci”, secțiunea 1.5", }, ] FOOTNOTES = [ ("Cash flow-ul pozitiv e ce ține compania în picioare — îl cauți chiar și la", BLACK), ("companiile încă pe pierdere.", BLACK), ("Pozitiv dar în scădere = deteriorare (53_09062026.txt:122-124).", GRAY), ("Surse: 66_18052026.txt:120-121; 72_08052026.txt:241-243.", GRAY), ("Secțiunea 1.5 Cash flow — summaries/08_ANALIZA_ZILNICA/11_selectia_simbolului.md.", GRAY), ] FOOT_TOP = CARD_TOP + CARD_H + 30 H = FOOT_TOP + (len(FOOTNOTES) - 1) * 20 + 30 labels = [] # (x_left, x_right, y_top, y_bottom, text) pentru verificare def est_width(text, size, bold=False): """Lățime aproximativă pentru system-ui, pentru verificarea suprapunerilor.""" factor = 0.62 if bold else 0.56 return len(text) * size * factor def txt(x, y, s, size=13, bold=False, fill=BLACK, anchor="middle"): weight = "700" if bold else "400" w = est_width(s, size, bold) if anchor == "middle": x0, x1 = x - w / 2, x + w / 2 elif anchor == "start": x0, x1 = x, x + w else: # end x0, x1 = x - w, x labels.append((x0, x1, y - size, y + 3, s)) return ( f'{escape(s)}' ) def card(idx, spec): x = XS[idx] parts = [ f'', ] # header hy = CARD_TOP + 30 for line in spec["header"]: parts.append(txt(x + CARD_W / 2, hy, line, size=16, bold=True)) hy += 21 sep_y = CARD_TOP + 64 parts.append( f'' ) # ce înseamnă parts.append(txt(x + 14, CARD_TOP + 88, "Ce înseamnă", size=13, fill=GRAY, anchor="start")) my = CARD_TOP + 108 for line in spec["meaning"]: parts.append(txt(x + 14, my, line, size=13, anchor="start")) my += 19 # caseta de acțiune box_y = CARD_TOP + CARD_H - 74 parts.append( f'' ) ay = box_y + 19 for line in spec["action"]: parts.append(txt(x + CARD_W / 2, ay, line, size=13, bold=True)) ay += 17 # sursa parts.append(txt(x + CARD_W / 2, CARD_TOP + CARD_H + 15, spec["source"], size=13, fill=GRAY)) return parts svg = [ f'', f'', txt(W / 2, TITLE_Y, "Cash flow — trei stări", size=19, bold=True), txt(W / 2, SUB_Y, "un cash flow pozitiv care scade e deteriorare, " "nu semnal de cumpărare", size=13, fill=GRAY), ] for i, spec in enumerate(CARDS): svg.extend(card(i, spec)) fy = FOOT_TOP + 10 for line, color in FOOTNOTES: svg.append(txt(W / 2, fy, line, size=13, fill=color)) fy += 20 svg.append("") # ------------------------------------------------------------------ verificare # 1. fiecare text încape în card / în panză for i, spec in enumerate(CARDS): inner = CARD_W - 28 for line in spec["header"]: assert est_width(line, 16, bold=True) <= inner, \ f"header nu incape in card {i}: {line!r}" for line in spec["meaning"] + spec["action"]: assert est_width(line, 13, line in spec["action"]) <= inner, \ f"text nu incape in card {i}: {line!r}" # 2. toate etichetele au coordonate în interiorul panzei for x0, x1, y0, y1, s in labels: assert 0 <= x0 and x1 <= W, f"eticheta iese din panza pe orizontala: {s!r}" assert 0 <= y0 and y1 <= H, f"eticheta iese din panza pe verticala: {s!r}" # 3. pe aceeași poziție / suprapuse for i in range(len(labels)): for j in range(i + 1, len(labels)): ax0, ax1, ay0, ay1, a = labels[i] bx0, bx1, by0, by1, b = labels[j] inter_w = min(ax1, bx1) - max(ax0, bx0) inter_h = min(ay1, by1) - max(ay0, by0) if inter_w > 1.5 and inter_h > 1.5: raise AssertionError(f"texte suprapuse: {a!r} / {b!r}") # 4. nu se calcă pe verticală doua rânduri de text (baseline prea apropiat) ys = sorted((round((y0 + y1) / 2), s) for _, _, y0, y1, s in labels) for k in range(len(ys) - 1): assert ys[k + 1][0] - ys[k][0] >= 14 or ys[k][0] == ys[k + 1][0], \ f"rânduri prea apropiate: {ys[k][1]!r} / {ys[k + 1][1]!r}" OUT.write_text("\n".join(svg) + "\n", encoding="utf-8") print(f"scris {OUT} ({W}x{H}), {len(CARDS)} stări, " f"{len(labels)} etichete, verificare OK")