"""Buy Doji — trendul dinaintea dojiului: 9 candele vs. 3 candele (exemplu construit). Citește specificația `gen/strategii/buy_doji_trend.spec.txt`. Două panouri stivuite, aceeași scară și dojiul pe aceeași verticală, ca să se compare direct: sus downtrend de 9 candele (ideal), jos range + 3 candele (acceptat, semnal mai slab, scannerul cu Since Bear ≥ 8 nu-l găsește). Candelele trendului sunt numerotate sub bară, ca numărătoarea să se vadă. """ import re import sys; sys.path.insert(0, "/workspace/atm-curs-dl") from svg_candles import Arrow, Candle, Chart, Mark, Zone OUT = "/workspace/atm-curs-dl/gen/strategii/buy_doji_trend.svg" DOJI = (49.65, 50.10, 49.25, 49.60) CONF = (49.60, 50.55, 49.45, 50.40) def series(start, closes): """Fitile implicite 0.15 peste/sub corp; open = close anterior.""" bars, o = [], start for c in closes: bars.append((o, max(o, c) + 0.15, min(o, c) - 0.15, c)) o = c return bars + [DOJI, CONF] L = series(55.50, [55.00, 54.30, 54.60, 53.60, 52.90, 53.10, 52.00, 51.20, 50.60, 49.65]) R = series(51.40, [51.70, 51.20, 51.60, 51.10, 51.50, 51.30, 51.70, 51.40, 50.90, 50.30, 49.65]) # --- assert-urile din spec --------------------------------------------------- for bars in (L, R): # 1 for i, (o, h, l, c) in enumerate(bars): assert l <= min(o, c) and h >= max(o, c), f"bara {i}" if i: assert abs(o - bars[i - 1][3]) < 1e-9, f"open[{i}] != close[{i-1}]" assert L[10] == R[11] == DOJI # 2 assert abs(DOJI[0] - DOJI[3]) <= 0.10 * (DOJI[1] - DOJI[2]) cL = [b[3] for b in L]; cR = [b[3] for b in R] assert cL[9] < cL[0] - 5 and cL[9] == min(cL[0:10]) # 3 assert sum(cL[i] < cL[i - 1] for i in range(1, 10)) >= 6 assert max(cR[0:8]) - min(cR[0:8]) <= 0.60 + 1e-9 # 4 assert cR[8] > cR[9] > cR[10] assert L[11][3] > DOJI[1] and R[12][3] > DOJI[1] and L[11][3] == 50.40 # 5 GREY, GREEN, BLUE, ORANGE, RED = "#555", "#188a3e", "#1a5fb4", "#b06000", "#c00" W, GAP = 540, 18 common: dict = dict(scale=30.0, gap=GAP, body_w=10, width=W) def numbers(ch, idx, color): """Numără candelele trendului: 1, 2, 3... sub fiecare bară.""" return [f'{n}' for n, i in enumerate(idx, 1)] def render_keep_offsets(ch): """`Chart.render()` restaurează top/left după desen, deci `_y`/`_cx` de după el dau coordonate deplasate față de body-ul randat (titlul și etichetele zonelor mută graficul în jos). `_render()` lasă offset-urile puse, exact ce ne trebuie ca extra-urile desenate de mână să pice la locul lor. Se cheamă o singură dată per Chart.""" return ch._render() def fits(ch, svg): """Chart mută graficul dacă un text ar ieși din pânză — aici mutarea ar dezalinia cele două panouri, deci verificăm că nu s-a întâmplat.""" assert f'viewBox="0 0 {ch.width} ' in svg, \ f"Chart a lărgit pânza ({svg[:60]}): textul unei săgeți iese din {ch.width}px" def body(svg): return svg.split(">", 1)[1].rsplit("", 1)[0].strip() # --- panoul de sus: 9 candele ------------------------------------------------ top = Chart(candles=[Candle(*b) for b in L], left=44 + GAP, # dojiul pe aceeași verticală marks=[Mark(11, "confirmare 50.40", GREEN, "above")], zones=[Zone(1, 9, "9 candele de scădere — ideal", GREEN)], arrows=[Arrow(10, "doji la capătul scăderii\nvânzătorii s-au terminat", where="below", color=ORANGE, dx=70)], top=54, # titlul se desenează de mână: cel din Chart ar cădea peste eticheta zonei **common) svg_top = render_keep_offsets(top) fits(top, svg_top) ext_top = numbers(top, range(1, 10), RED) + [ 'Trendul dinaintea dojiului: ' '9 candele vs. 3 candele — exemplu construit'] y_cap1 = max(top._y(DOJI[2]) + 50, top._arrow_geom(top.arrows[0])["box"][3] + 24) ext_top.append(f'' f'DA — ideal (8-10 candele)') # --- panoul de jos: range + 3 candele ---------------------------------------- y_sep = y_cap1 + 16 bot = Chart(candles=[Candle(*b) for b in R], left=44, marks=[Mark(11, "doji", ORANGE, "below", dy=22), Mark(12, "confirmare 50.40", GREEN, "above")], zones=[Zone(8, 10, "doar 3 candele — merge, dar mai slab", ORANGE)], arrows=[Arrow(3, "înainte: preț pe loc,\nnu scădere — de aceea\nsemnalul e mai slab", where="above", color=GREY)], top=int(y_sep + 118), **common) # loc pentru titlul panoului și textul săgeții svg_bot = render_keep_offsets(bot) fits(bot, svg_bot) ext_bot = (numbers(bot, range(8, 11), RED) + [f'' f'Același doji, după un range']) y_cap2 = bot._y(DOJI[2]) + 50 ext_bot += [f'' f'ACCEPTAT, semnal mai slab', f'' f'scannerul cu Since Bear ≥ 8 nu-l vede'] H = y_cap2 + 30 # dojiul e aceeași bară, la aceeași scară, în același loc pe orizontală assert abs(top._cx(10) - bot._cx(11)) < 1e-9 and top.scale == bot.scale svg = "\n ".join([ f'', body(svg_top), *ext_top, f'', body(svg_bot), *ext_bot, ]) + "\n" assert "\n\n" not in svg, "linie goală în SVG" with open(OUT, "w") as f: f.write(svg) print(f"scris {OUT} ({W}x{H:.0f})")