#!/usr/bin/env python3 """Ce e un doji și ce nu — patru candele cu același high 50.10 și low 49.20. Citește `gen/strategii/buy_doji_doji_forma.spec.txt` și scrie `gen/strategii/buy_doji_doji_forma.svg`. E prima imagine din capitol: cine o vede nu știe ce e un doji, deci candela A primește săgeți pe corp și pe fiecare fitil, cu explicația de ce arată așa, iar C și D sunt contraexemple. Zonă verde peste A și B („doji"), zonă roșie peste C și D („nu e doji"). Desenat direct, nu cu `Chart`: `tag`/`note` din svg_candles pun etichetele pe un singur rând sau în blocul de jos, iar aici fiecare etichetă trebuie să stea lipită sub candela ei, pe 2-3 rânduri, plus săgeți cu text pe lateral. Rulare: python3 gen/strategii/buy_doji_doji_forma.py """ from pathlib import Path OUT = Path(__file__).with_suffix(".svg") GREEN, RED, GREY, BLUE = "#188a3e", "#c00", "#555", "#1a5fb4" H, L = 50.10, 49.20 RNG = H - L # (nume, o, h, l, c, verdict, rânduri de etichetă sub candelă) CANDLES = [ ("A", 49.60, 50.10, 49.20, 49.55, "DA", ["DA: corp 0.05", "= 5,6 % din înălțime"]), ("B", 49.62, 50.10, 49.20, 49.53, "DA", ["DA, la limită:", "corp 0.09 = 10 %"]), ("C", 49.80, 50.10, 49.20, 49.50, "NU", ["NU: corp 0.30", "= 33 %"]), ("D", 50.05, 50.10, 49.20, 50.10, "NU", ["NU: corp mic (5,6 %),", "dar fără fitil sus"]), ] # --- assert-urile din spec -------------------------------------------------- ratio = {n: round(abs(c - o) / (h - l), 4) for n, o, h, l, c, *_ in CANDLES} for n, o, h, l, c, *_ in CANDLES: assert h == H and l == L, (n, h, l) # 1 assert ratio == {"A": 0.0556, "B": 0.1000, "C": 0.3333, "D": 0.0556}, ratio # 2 for n, o, h, l, c, *_ in CANDLES[:2]: # 3 assert ratio[n] <= 0.10 and min(o, c) > l and max(o, c) < h, n assert ratio["C"] > 0.10 # 4 _, oD, hD, _, cD, *_ = CANDLES[3] assert max(oD, cD) == hD # --- geometrie --------------------------------------------------------------- SCALE, TOP, W = 260, 210, 1000 XS = [350, 490, 690, 860] # centrele candelelor: A, B (doji) | C, D (nu) BODY_W = 26 def y(p): return TOP + (H - p) * SCALE oA, cA = CANDLES[0][1], CANDLES[0][4] assert y(oA) < y(cA), "open A trebuie să fie mai sus pe ecran decât close A" # 5 out = [] t = out.append def text(x, yy, s, color=GREY, anchor="start", bold=False, size=12): style = f' style="font-weight:bold;font-size:{size}px"' if bold else ( f' style="font-size:{size}px"' if size != 12 else "") t(f'{s}') def zone(x0, x1, y0, y1, label, color): t(f'') text(x0 + 6, y0 - 7, label, color, bold=True) def arrow(x1, y1, x2, y2, color, lines, anchor="start"): """Săgeată de la (x1,y1) [coada, lângă text] la (x2,y2) [vârf, pe țintă].""" d = max(((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5, 1e-9) ux, uy = (x2 - x1) / d, (y2 - y1) / d bx, by = x2 - 8 * ux, y2 - 8 * uy t(f'') t(f'') dx = -6 if anchor == "end" else (0 if anchor == "middle" else 6) # la săgeata verticală textul stă deasupra cozii, altfel intră peste ea y0 = (y1 - 10 - 14 * (len(lines) - 1) if anchor == "middle" else y1 + 4 - 14 * (len(lines) - 1) / 2) for k, s in enumerate(lines): text(x1 + dx, y0 + 14 * k, s, color, anchor, bold=(k == 0)) text(24, 30, "Ce e un doji și ce nu — aceeași bară, 50.10 sus și 49.20 jos", "#333", bold=True, size=14) # zonele: A+B sunt doji, C+D nu label_y0 = y(L) + 28 z_bot = label_y0 + 16 * 2 - 4 zone(XS[0] - 58, XS[1] + 62, y(H) - 30, z_bot, "doji", GREEN) zone(XS[2] - 62, XS[3] + 74, y(H) - 30, z_bot, "nu e doji", RED) # high și low comune: linii subțiri pe toată lățimea for p in (H, L): t(f'') text(24, y(H) - 6, "cât de sus a urcat prețul: 50.10") text(24, y(L) + 14, "cât de jos a coborât: 49.20") for (n, o, h, l, c, verdict, lines), x in zip(CANDLES, XS): color = GREEN if c >= o else RED vcol = GREEN if verdict == "DA" else RED t(f'') top, bot = y(max(o, c)), y(min(o, c)) t(f'') for k, s in enumerate(lines): text(x, label_y0 + 16 * k, s, vcol, "middle", bold=(k == 0)) # --- săgețile pe A: corp, fitil sus, fitil jos ------------------------------- xA = XS[0] arrow(xA - 60, y(49.92), xA - 3, y(49.85), BLUE, anchor="end", lines=["fitilul de sus", "cumpărătorii au împins, dar n-au ținut"]) arrow(xA - 60, y(49.575), xA - 15, y(49.575), BLUE, anchor="end", lines=["corpul: aproape o linie", "open și close aproape egale — nimeni", "n-a câștigat ziua"]) arrow(xA - 60, y(49.30), xA - 3, y(49.375), BLUE, anchor="end", lines=["fitilul de jos", "vânzătorii au împins, dar n-au ținut"]) # prețurile de open/close, în dreapta candelei A text(xA + 20, y(oA) - 2, "open 49.60") text(xA + 20, y(cA) + 10, "close 49.55") # --- săgețile pe contraexemple ---------------------------------------------- xC, xD = XS[2], XS[3] arrow(xC + 34, y(H) - 112, xC + 14, y(49.78), RED, lines=["corp prea gros (33 %)", "aici una din tabere a câștigat —", "nu mai e indecizie"]) arrow(xD, y(H) - 34, xD, y(H) - 4, RED, anchor="middle", lines=["fără fitil sus", "regula cere fitil în ambele părți"]) cap_y = z_bot + 30 text(24, cap_y, f"Doji = corpul e cel mult 10 % din înălțimea totală a barei " f"(aici înălțimea e {RNG:.2f}) și are fitil și sus, și jos.") height = cap_y + 16 svg = (f'\n ' + "\n ".join(out) + "\n\n") OUT.write_text(svg, encoding="utf-8") print(f"scris {OUT} ({W}x{height:.0f})")