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>
139 lines
3.7 KiB
Python
139 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import secrets
|
|
from contextlib import asynccontextmanager
|
|
from typing import Any
|
|
|
|
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import ValidationError
|
|
|
|
from app.config import Settings, get_settings
|
|
from app.models import SignalPayload
|
|
from app.pipeline import deliver_telegram
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_app: FastAPI):
|
|
settings = get_settings()
|
|
task: asyncio.Task[None] | None = None
|
|
if settings.scanner_enabled:
|
|
from app.scanner import run_scanner
|
|
|
|
task = asyncio.create_task(run_scanner(settings))
|
|
logger.info(
|
|
"Scanner enabled watchlist=%s poll=%ss",
|
|
settings.watchlist_path,
|
|
settings.scanner_poll_seconds,
|
|
)
|
|
yield
|
|
if task is not None:
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
logger.info("Scanner stopped")
|
|
|
|
|
|
app = FastAPI(
|
|
title="TV Signals → Telegram",
|
|
version="1.0.0",
|
|
docs_url=None,
|
|
redoc_url=None,
|
|
openapi_url=None,
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
|
|
def _parse_signal_body(raw: bytes) -> SignalPayload:
|
|
"""Parse JSON from raw body (works for application/json and text/plain)."""
|
|
try:
|
|
text = raw.decode("utf-8").strip()
|
|
except UnicodeDecodeError as exc:
|
|
raise HTTPException(status_code=422, detail="Body must be UTF-8 text") from exc
|
|
|
|
if not text:
|
|
raise HTTPException(status_code=422, detail="Empty body")
|
|
|
|
try:
|
|
data: Any = json.loads(text)
|
|
except json.JSONDecodeError as exc:
|
|
raise HTTPException(status_code=422, detail=f"Invalid JSON: {exc}") from exc
|
|
|
|
try:
|
|
return SignalPayload.model_validate(data)
|
|
except ValidationError as exc:
|
|
raise HTTPException(status_code=422, detail=json.loads(exc.json())) from exc
|
|
|
|
|
|
@app.api_route("/health", methods=["GET", "HEAD"])
|
|
async def health() -> dict[str, str]:
|
|
return {"status": "ok"}
|
|
|
|
|
|
async def _handle_webhook(
|
|
token: str,
|
|
request: Request,
|
|
thread_id: int | None,
|
|
background_tasks: BackgroundTasks,
|
|
) -> JSONResponse:
|
|
settings = get_settings()
|
|
if not secrets.compare_digest(token, settings.webhook_secret):
|
|
raise HTTPException(status_code=404, detail="Not Found")
|
|
|
|
message_thread_id = (
|
|
thread_id if thread_id is not None else settings.telegram_message_thread_id
|
|
)
|
|
|
|
signal = _parse_signal_body(await request.body())
|
|
logger.info(
|
|
"Signal accepted: %s %s seq=%s tf=%s thread=%s",
|
|
signal.ticker,
|
|
signal.action.value,
|
|
signal.signal_sequence,
|
|
signal.visual_timeframe,
|
|
message_thread_id,
|
|
)
|
|
|
|
background_tasks.add_task(
|
|
deliver_telegram,
|
|
settings,
|
|
signal,
|
|
message_thread_id,
|
|
)
|
|
return JSONResponse({"ok": True, "accepted": True}, status_code=200)
|
|
|
|
|
|
@app.post("/h/{token}")
|
|
async def webhook(
|
|
token: str,
|
|
request: Request,
|
|
background_tasks: BackgroundTasks,
|
|
) -> JSONResponse:
|
|
return await _handle_webhook(
|
|
token, request, thread_id=None, background_tasks=background_tasks
|
|
)
|
|
|
|
|
|
@app.post("/h/{token}/{thread_id}")
|
|
async def webhook_with_thread(
|
|
token: str,
|
|
thread_id: int,
|
|
request: Request,
|
|
background_tasks: BackgroundTasks,
|
|
) -> JSONResponse:
|
|
if thread_id < 1:
|
|
raise HTTPException(status_code=422, detail="thread_id must be >= 1")
|
|
return await _handle_webhook(
|
|
token, request, thread_id=thread_id, background_tasks=background_tasks
|
|
)
|