mirror of
https://github.com/artemium428/tvsignals-to-tg.git
synced 2026-09-15 17:16:21 +00:00
TP1+BE stays; the extra Fix 30% line was redundant. Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
4.3 KiB
Python
142 lines
4.3 KiB
Python
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"<b>{signal.ticker}</b> {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\n<i>reversal, realized PnL {signal.realized_pnl_pct:+.2f}%</i>"
|
|
)
|
|
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"<b>{signal.ticker}</b> {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"<i>reversal, realized PnL {signal.realized_pnl_pct:+.2f}%</i>",
|
|
]
|
|
)
|
|
return "\n".join(lines)
|