mirror of
https://github.com/artemium428/tvsignals-to-tg.git
synced 2026-09-15 17:16:21 +00:00
Per-strategy Heryon accounts, reversal captions with previous-trade path on charts, and Telegram replies chained by ticker. Co-authored-by: Cursor <cursoragent@cursor.com>
81 lines
2.8 KiB
Python
81 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from app.binance import get_tick_size
|
|
from app.config import Settings
|
|
from app.indicators.common import IndicatorSignal, calc_size, format_px, round_to_mintick
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
LTF_FIX = {"tp1_fix": "30%", "tp2_fix": "30%", "tp3_fix": "40%"}
|
|
FVG_FIX = {"tp1_fix": "30%"}
|
|
|
|
|
|
def to_tv_perp_ticker(symbol: str) -> str:
|
|
base = symbol.strip().upper()
|
|
if base.endswith(".P"):
|
|
return base
|
|
return f"{base}.P"
|
|
|
|
|
|
def heryon_nonce(ticker: str, side: str, bar_open_ts: int) -> str:
|
|
limit_side = "long" if side == "long" else "short"
|
|
return f"{ticker}-limit-{limit_side}-{bar_open_ts}"
|
|
|
|
|
|
def build_heryon_payload(
|
|
signal: IndicatorSignal,
|
|
*,
|
|
symbol: str,
|
|
settings: Settings,
|
|
) -> dict[str, Any]:
|
|
ticker = to_tv_perp_ticker(symbol)
|
|
tick = get_tick_size(symbol)
|
|
action = "buy" if signal.side == "long" else "sell"
|
|
fixes = LTF_FIX if signal.strategy_id == "ltf" else FVG_FIX
|
|
entry = round_to_mintick(signal.entry, tick)
|
|
sl = round_to_mintick(signal.sl, tick)
|
|
size = calc_size(entry, sl, float(settings.risk_usd))
|
|
size_str = "" if size != size else str(int(round(size)))
|
|
payload: dict[str, Any] = {
|
|
"action": action,
|
|
"ticker": ticker,
|
|
"account_id": settings.geryon_account_id_for(signal.strategy_id),
|
|
"order_type": settings.geryon_order_type,
|
|
"position_size_usd": size_str,
|
|
"stop_loss": format_px(signal.sl, tick),
|
|
"tp1": format_px(signal.tp1, tick),
|
|
"tp1_fix": fixes.get("tp1_fix", ""),
|
|
"sl_to_bk": "tp1",
|
|
"nonce": heryon_nonce(ticker, signal.side, signal.bar_open_ts),
|
|
"secret": settings.geryon_secret_for(signal.strategy_id),
|
|
}
|
|
if signal.strategy_id == "ltf":
|
|
payload["tp2"] = format_px(signal.tp2, tick)
|
|
payload["tp2_fix"] = fixes.get("tp2_fix", "")
|
|
payload["tp3"] = format_px(signal.tp3, tick)
|
|
payload["tp3_fix"] = fixes.get("tp3_fix", "")
|
|
return {key: value for key, value in payload.items() if value != ""}
|
|
|
|
|
|
async def send_heryon(settings: Settings, payload: dict[str, Any]) -> None:
|
|
url = (settings.geryon_webhook_url or "").strip()
|
|
if not url:
|
|
logger.info("Heryon skipped (GERYON_WEBHOOK_URL empty): %s", payload.get("nonce"))
|
|
return
|
|
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
response = await client.post(url, json=payload)
|
|
if response.status_code >= 400:
|
|
raise RuntimeError(
|
|
f"Heryon webhook {response.status_code}: {response.text[:500]}"
|
|
)
|
|
logger.info(
|
|
"Heryon accepted nonce=%s ticker=%s action=%s",
|
|
payload.get("nonce"),
|
|
payload.get("ticker"),
|
|
payload.get("action"),
|
|
)
|