DEV Community

Abednego Consejo
Abednego Consejo

Posted on

Building a TradingView Alpaca Webhook in FastAPI (and what I learned)

TradingView alerts are genuinely powerful. You can write a Pine Script strategy, backtest it, and then fire an alert the moment a condition is met. The problem is what happens after that alert fires — by default, nothing. You still have to manually place the trade.

Most people solve this in one of two ways: they pay $49+/month for a SaaS bridge, or they find a GitHub script that uses Flask, has no error handling, and breaks silently. Neither is satisfying. Here is how to build the bridge yourself in about 100 lines of Python.


The Architecture

The flow is simple:

TradingView alert → HTTP POST → FastAPI server → Alpaca order
Enter fullscreen mode Exit fullscreen mode

Four components:

  1. A FastAPI app with a single /webhook endpoint
  2. A Pydantic model that validates the incoming payload
  3. alpaca-py for order execution

4. A .env file for secrets, loaded with python-dotenv

Why FastAPI Over Flask

Flask works, but FastAPI gives you three things that matter here:

  • Async by default. Webhook handlers are I/O-bound (network calls to Alpaca). Async lets you handle bursts without blocking.
  • Automatic docs. Hit /docs and you get an interactive Swagger UI. Useful when debugging why your payload is being rejected.

- Pydantic integration. Your payload model is also your validation layer. If TradingView sends a malformed body, FastAPI returns a 422 before your handler even runs.

The Pydantic Model

from pydantic import BaseModel
from typing import Literal

class WebhookPayload(BaseModel):
    secret: str
    ticker: str
    action: Literal["buy", "sell", "close"]
    qty: float | None = None
Enter fullscreen mode Exit fullscreen mode

The secret field is the key decision. You are putting this server on the public internet, and TradingView does not support request signing. Anyone who finds your URL can POST to it. The payload secret is a shared token you set in both your .env and inside the TradingView alert message. If it does not match, you return a 403 and log the attempt.

import os

WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET")

@app.post("/webhook")
async def webhook(payload: WebhookPayload):
    if payload.secret != WEBHOOK_SECRET:
        raise HTTPException(status_code=403, detail="Invalid secret")
    ...
Enter fullscreen mode Exit fullscreen mode

Order Execution

from alpaca.trading.client import TradingClient
from alpaca.trading.requests import MarketOrderRequest
from alpaca.trading.enums import OrderSide, TimeInForce

client = TradingClient(
    os.getenv("ALPACA_API_KEY"),
    os.getenv("ALPACA_SECRET_KEY"),
    paper=True  # flip to False for live
)

async def execute_order(payload: WebhookPayload):
    if payload.action == "close":
        client.close_position(payload.ticker)
        return

    side = OrderSide.BUY if payload.action == "buy" else OrderSide.SELL
    order = MarketOrderRequest(
        symbol=payload.ticker,
        qty=payload.qty,
        side=side,
        time_in_force=TimeInForce.DAY
    )
    client.submit_order(order)
Enter fullscreen mode Exit fullscreen mode

Two decisions worth explaining:

Market orders by default. Limit orders introduce slippage decisions — what price, how long to wait, what if it doesn't fill? For automated signals, market orders execute immediately and you know you're in. If your strategy is latency-sensitive enough to care about the spread, you probably should not be using a webhook bridge anyway.

close_position vs. market sell. If you sell a fixed quantity and your position size is not exactly that quantity (because of partial fills, fractional shares, etc.), you end up with a residual position. close_position flattens whatever you actually hold. Use action: "close" for exits.


The TradingView Side

In your alert message, send JSON:

{
  "secret": "{{your_secret_here}}",
  "ticker": "{{ticker}}",
  "action": "buy",
  "qty": 1
}
Enter fullscreen mode Exit fullscreen mode

Set the webhook URL to your server's /webhook endpoint.


Security Checklist

  • Secret token in payload (covered above)
  • API keys in .env, never in source code
  • .env in .gitignore before the first commit, not after
  • Use Alpaca paper trading until you have tested this extensively

- Log every incoming request (at minimum: timestamp, ticker, action, whether it succeeded)

Deploying Cheaply

A one-file Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Enter fullscreen mode Exit fullscreen mode

Railway and Render both have free tiers that will run this. Railway's free tier gives you $5/month of compute credit — enough for a lightweight FastAPI server. Set your environment variables in the dashboard, not in the Dockerfile.


What I Learned

The webhook secret feels obvious in retrospect but it is the thing most people skip in tutorials. Silent failures are the other thing — if your order fails (market closed, insufficient buying power, invalid symbol), you want to know immediately. Log everything, even successful orders. Add an /health endpoint that returns 200 so you can ping it and confirm the server is running before TradingView fires a live alert.

I packaged this webhook server alongside a backtester and portfolio dashboard as AlgoKit — a Python trading infrastructure bundle for people who want to own their stack: algokit-dev.lemonsqueezy.com

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.