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,
)
return (
f"{signal.ticker} {label}\n"
f"\n"
f"Entry price: {entry}\n"
f"Price: {price}\n"
f"Current profit: {profit}"
)
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)
tp2 = format_price(signal.take_profit_2_price)
tp3 = format_price(signal.take_profit_3_price)
return (
f"{signal.ticker} {label}\n"
f"\n"
f"Price: {price}\n"
f"SL: {sl} {sl_pct}\n"
f"\n"
f"TP1: {tp1}\n"
f"TP2: {tp2}\n"
f"TP3: {tp3}"
)