"""Buy Doji — intrarea în trei tranșe, pe același trade ca restul capitolului. Citește `gen/strategii/buy_doji_transe.spec.txt`. Barele vin din `buy_doji_ansamblu.serie()`: doji 50.10/49.25, confirmare cu close 50.40, stop 49.20. Nivelurile de 1/3 și 1/2 se calculează din corpul candelei de confirmare (open 49.90 → close 50.40), nu se scriu de mână. Lecția: împărțind intrarea în trei, prețul mediu scade de la 50.40 la 50.26, deci riscul pe acțiune de la 1.20 la 1.06 — dacă prețul revine. Bara de după confirmare coboară la 50.10 și umple ambele ordine limită. """ import re import sys from pathlib import Path sys.path.insert(0, "/workspace/atm-curs-dl") sys.path.insert(0, str(Path(__file__).parent)) from svg_candles import Chart, Level, Zone, Arrow # noqa: E402 from buy_doji_ansamblu import serie, DOJI, CONF, DOJI_L, ENTRY, SL, TP1, R # noqa: E402 OUT = Path(__file__).with_suffix(".svg") S = 12 # prima bară arătată (zoom pe fereastra de intrare) GREEN, RED, ORANGE, BLUE, GREY = "#188a3e", "#c00", "#b06000", "#1a5fb4", "#555" W, SCALE = 1320, 240.0 cds = serie()[S:20] # până la bara care atinge ținta d, cf = DOJI - S, CONF - S # doji, confirmare (indici locali) op, T1 = cds[cf].o, cds[cf].c # corpul candelei de confirmare corp = round(T1 - op, 2) T2, T3 = round(T1 - corp / 3, 2), round(T1 - corp / 2, 2) MED = round((T1 + T2 + T3) / 3, 2) RISC_MED, RISC_CLOSE = round(MED - SL, 2), round(T1 - SL, 2) TP_MED = round(MED + RISC_MED, 2) # ținta de 1R socotită de la prețul mediu retest = cds[cf + 1] # --- aserțiuni ------------------------------------------------------------------ assert (op, T1) == (49.90, ENTRY) and corp == 0.50, (op, T1, corp) assert (T2, T3) == (50.23, 50.15), (T2, T3) assert MED == 50.26 and RISC_MED == 1.06 and RISC_CLOSE == R == 1.20 assert retest.l <= T3 < T2, "bara de după confirmare nu umple ambele ordine" assert round(retest.l, 2) == 50.10, retest.l assert min(c.l for c in cds[cf - 3:cf + 1]) == DOJI_L and round(DOJI_L - 0.05, 2) == SL assert round(T1 + RISC_CLOSE, 2) == TP1 # ținta de 1R vine mai devreme dacă prețul mediu de intrare e mai bun assert TP_MED == 51.32 == round(MED + RISC_MED, 2) and TP_MED < TP1 assert next(i for i in range(cf + 1, len(cds)) if cds[i].h >= TP1) == len(cds) - 1 # măsurat pe toată candela (nu doar pe corp) 1/2 iese tot T3; 1/3 iese 50.28 hi, lo = cds[cf].h, cds[cf].l assert round(hi - (hi - lo) / 3, 2) == 50.28 and round((hi + lo) / 2, 2) == T3 print(f"transe: T1 {T1:.2f} T2 {T2:.2f} T3 {T3:.2f} medie {MED:.2f} " f"risc {RISC_MED:.2f} (față de {RISC_CLOSE:.2f}) retest low {retest.l:.2f}") def dy_sub(price, rand): """Săgeata pleacă din nivel și coboară sub linia stopului, pe rândul `rand`.""" return (price - SL) * SCALE + rand ch = Chart( candles=cds, levels=[Level(TP1, "ținta 1: 51.60 = 1R față de close", GREEN, "2 2"), Level(TP_MED, f"ținta 1: {TP_MED:.2f} = 1R față de prețul mediu {MED:.2f}", BLUE, "2 2"), Level(ENTRY, "intrarea (close-ul de confirmare): 50.40", GREEN, "4 3"), Level(SL, "stopul: 49.20", RED, "4 3", "left")], zones=[Zone(cf, cf, "", GREEN, p_top=T1, p_bot=op)], arrows=[ Arrow(cf - 1, "media mai bună coboară ținta:\nieși la 51.32, nu la 51.60", where="above", price=TP_MED, color=BLUE, dx=150), Arrow(cf + 1, "retestul: prețul revine la 50.10\nși umple amândouă ordinele limită", where="below", price=retest.l, color=BLUE, dx=-150, dy=20), ], scale=SCALE, gap=30, body_w=10, left=56, width=W, title="Intrarea în trei tranșe — același trade (doji 50.10 / 49.25)", ) svg = ch.render() OFF = (float(re.search(r']*stroke="#1a1a1a"', svg).group(1)) - ch._cx(0)) def Y(p): return ch._y(p) + OFF def x(i): return ch._cx(i) + OFF_X CW, LH = 6.2, 14 # lățime caracter bold 12px, înălțime rând def sageata(x1, y1, x2, y2, col, randuri): """Săgeată desenată de mână: coada la (x1,y1) lângă text, vârful la (x2,y2).""" 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 out = [f'', f''] y0 = y1 + 4 - LH * (len(randuri) - 1) / 2 for k, t in enumerate(randuri): out.append(f'{t}') return out ex = [] # paranteza corpului, în stânga candelei de confirmare, cu diviziunile 1/3 și 1/2 xb = x(cf) - 17 ex.append(f'') # fără etichetă lângă paranteză: oriunde lângă candele s-ar suprapune peste ele # sau peste liniile săgeților. O numește legenda de sub grafic. # Fiecare tranșă are linia ei de nivel, prelungită la dreapta pe o lungime # diferită, cu prețul scris la capăt. Săgeata tranșei se termină pe capătul # liniei ei: trei puncte de sosire distanțate și pe orizontală, și pe verticală # (altfel, la aceeași candelă, cele trei vârfuri ies într-un ghem de 10 px). XT1, XT2, XT3 = x(len(cds) - 1) + 230, x(len(cds) - 1) + 150, x(len(cds) - 1) + 70 TRANSE = ((T1, GREEN, XT1, "", ["1/3 la close 50.40", "ca să nu ratezi trade-ul dacă pleacă fără tine"]), (T2, ORANGE, XT2, ' stroke-dasharray="3 3"', ["1/3 cu ordin limită la 50.23 (o treime din corp)", "mai ieftin, dacă prețul revine"]), (T3, ORANGE, XT3, ' stroke-dasharray="3 3"', ["1/3 cu ordin limită la 50.15 (jumătatea corpului)", "și mai ieftin"])) XTAIL = x(len(cds) - 1) + 114 # coada tuturor celor trei săgeți, sub stop for k, (p_, col, xend, dash, randuri) in enumerate(TRANSE): if p_ != T1: # T1 e chiar nivelul de intrare, deja desenat ex.append(f'') # prețul deasupra liniei, nu pe ea ex.append(f'{p_:.2f}') ex += sageata(XTAIL, Y(SL) + 46 + 40 * k, xend, Y(p_), col, randuri) # unde se umple fiecare tranșă: T1 la close, T2 și T3 pe low-ul barei următoare for i, p_, col in ((cf, T1, GREEN), (cf + 1, T2, ORANGE), (cf + 1, T3, ORANGE)): ex.append(f'') # cele două paranteze de risc: tot la close vs cu tranșe. Pleacă amândouă de la # un nivel desenat (intrarea 50.40, respectiv media 50.26) și coboară la stop. # Stau una lângă alta, cu textele la dreapta amândurora, la înălțimi diferite — # altfel textul uneia trece peste paranteza celeilalte. XTXT = x(len(cds) - 1) + 664 for xp, hi_p, ty, txt, col in ( (x(len(cds) - 1) + 560, T1, Y(T1) + 50, f"risc {RISC_CLOSE:.2f} pe acțiune (tot la close, de la intrarea {T1:.2f})", RED), (x(len(cds) - 1) + 640, MED, Y(SL) - 50, f"risc {RISC_MED:.2f} pe acțiune (cu tranșe, de la prețul mediu {MED:.2f})", BLUE)): y1, y2 = Y(hi_p), Y(SL) ex.append(f'') ex.append(f'') ex.append(f'{txt}') # nivelul prețului mediu: segment scurt, ca paranteza albastră să plece de pe o linie ex.append(f'') ex.append(f'' f'prețul mediu al celor trei tranșe: {MED:.2f}') body = svg.rsplit("", 1)[0].rstrip() svg = "\n ".join([body, *ex]) + "\n" vw, vh = (float(v) for v in re.search(r'viewBox="0 0 ([\d.]+) ([\d.]+)"', svg).groups()) # textele desenate manual trec sub pânza calculată de Chart: crește înălțimea y_max = max(float(m) for m in re.findall(r']*\sy="([\d.]+)"', svg)) vh = max(vh, y_max + 34) CAPTION = ("Paranteza de pe candela de confirmare e corpul ei (49.90 → 50.40): pe el se măsoară " "1/3 și 1/2. Stopul e unul singur. Exemplu construit.") svg = svg.replace("", f' {CAPTION}\n') svg = re.sub(r'viewBox="0 0 [\d.]+ [\d.]+"', f'viewBox="0 0 {vw:.0f} {vh:.0f}"', svg, count=1) for tx, attrs, txt in re.findall(r']*)>(.*?)', svg): ty = float(re.search(r'y="([-\d.]+)"', attrs).group(1)) w = len(txt) * CW x0 = float(tx) - (w if 'text-anchor="end"' in attrs else w / 2 if 'middle' in attrs else 0) assert 0 <= x0 and x0 + w <= vw, f"text în afara pânzei pe orizontală: {txt!r}" assert 0 <= ty <= vh, f"text în afara pânzei pe verticală: {txt!r}" assert "\n\n" not in svg, "linie goală în SVG" # cele trei săgeți de tranșă trebuie să sosească la înălțimi și la x-uri distincte tips = [(XT1, Y(T1)), (XT2, Y(T2)), (XT3, Y(T3))] for (ax, ay), (bx_, by_) in ((tips[0], tips[1]), (tips[1], tips[2]), (tips[0], tips[2])): assert abs(ax - bx_) >= 60 and abs(ay - by_) >= 18, \ f"vârfuri de săgeată prea apropiate: {(ax, ay)} vs {(bx_, by_)}" OUT.write_text(svg, encoding="utf-8") print(f"scris {OUT} ({vw:.0f}x{vh:.0f})")