from __future__ import annotations from app.models import Action, SignalPayload def _parse_price_number(raw: str) -> float: text = raw.strip().replace(" ", "").replace(",", "") if text.startswith("$"): text = text[1:] return float(text) def format_sl_distance_pct(entry_raw: str, sl_raw: str, *, is_long: bool) -> str: """Percent move from entry to SL; shown as risk (negative) for both sides.""" entry = _parse_price_number(entry_raw) if entry == 0: raise ValueError("entry price is zero") sl = _parse_price_number(sl_raw) pct = (sl - entry) / entry * 100 if not is_long: pct = -pct return f"({pct:.2f}%)" def format_current_profit(entry_raw: str, current_raw: str, sl_raw: str, *, is_long: bool) -> str: """Signed profit % vs entry and RR vs original SL distance (e.g. +1.6% (RR 1:1.2)).""" entry = _parse_price_number(entry_raw) if entry == 0: raise ValueError("entry price is zero") current = _parse_price_number(current_raw) sl = _parse_price_number(sl_raw) if is_long: profit_pct = (current - entry) / entry * 100 else: profit_pct = (entry - current) / entry * 100 sl_dist = abs(entry - sl) if sl_dist == 0: raise ValueError("stop loss equals entry") move = abs(current - entry) rr = move / sl_dist if profit_pct < 0: rr = -rr return f"{profit_pct:+.1f}% (RR 1:{rr:.1f})" def format_price(raw: str) -> str: """Insert thousand spaces and prefix with $; preserve decimal precision from TV.""" text = raw.strip().replace(" ", "").replace(",", "") if text.startswith("$"): text = text[1:] negative = text.startswith("-") if negative: text = text[1:] if "." in text: whole, frac = text.split(".", 1) else: whole, frac = text, None whole = whole.lstrip("0") or "0" grouped = _group_thousands(whole) if frac is not None: formatted = f"{grouped}.{frac}" else: formatted = grouped if negative: formatted = f"-{formatted}" return f"${formatted}" def _group_thousands(digits: str) -> str: if len(digits) <= 3: return digits parts: list[str] = [] while digits: parts.append(digits[-3:]) digits = digits[:-3] return " ".join(reversed(parts)) def format_caption(signal: SignalPayload) -> str: is_long = signal.action == Action.LONG seq = signal.signal_sequence if seq == 1: label = "💚 Buy" if is_long else "💔 Sell" else: label = f"🌱 Buy Seq: {seq}" if is_long else f"🥀 Sell Seq: {seq}" if seq > 1: entry = format_price(signal.entry_price) price = format_price(signal.current_price) profit = format_current_profit( signal.entry_price, signal.current_price, signal.stop_loss_price, is_long=is_long, ) text = ( f"{signal.ticker} {label}\n" f"\n" f"Entry price: {entry}\n" f"Price: {price}\n" f"Current profit: {profit}" ) if signal.is_reversal and signal.realized_pnl_pct is not None: text += ( f"\n\nreversal, realized PnL {signal.realized_pnl_pct:+.2f}%" ) return text price = format_price(signal.entry_price) sl = format_price(signal.stop_loss_price) sl_pct = format_sl_distance_pct( signal.entry_price, signal.stop_loss_price, is_long=is_long ) tp1 = format_price(signal.take_profit_1_price) hold_remainder = not signal.take_profit_2_price and not signal.take_profit_3_price lines = [ f"{signal.ticker} {label}\n", f"Price: {price}", f"SL: {sl} {sl_pct}", "", f"TP1+BE: {tp1}" if hold_remainder else f"TP1: {tp1}", ] if signal.take_profit_2_price: lines.append(f"TP2: {format_price(signal.take_profit_2_price)}") if signal.take_profit_3_price: lines.append(f"TP3: {format_price(signal.take_profit_3_price)}") if signal.is_reversal and signal.realized_pnl_pct is not None: lines.extend( [ "", f"reversal, realized PnL {signal.realized_pnl_pct:+.2f}%", ] ) return "\n".join(lines)