"""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'")
# ------------------------------------------------------------------ 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")