Files
2026-07-26 22:56:35 +08:00

26 KiB

BTC 15-Minute Live Trading Bot - Complete Logic Documentation

Suite: PolyBullLabs — polymakret-5min-15min-1hour-arbitrage-bot · @terauss · README · CONFIG.md

Table of Contents

  1. System Overview
  2. Market Structure
  3. Data Acquisition Layer
  4. Indicator Calculations (Formulas)
  5. Signal Generation Engine
  6. Order Execution Pipeline
  7. Hedge Mechanism
  8. Position Lifecycle and PnL Accounting
  9. Drawdown Tracking
  10. Chainlink BTC/USD Oracle Integration
  11. Auto-Redemption System
  12. Configuration Reference
  13. Fault Tolerance and Recovery
  14. File and Log Architecture

1. System Overview

The bot trades Polymarket BTC Up/Down 15-minute binary markets. Each market resolves to either "UP" (BTC price rose) or "DOWN" (BTC price fell) over a 15-minute window aligned to epoch boundaries (multiples of 900 seconds).

Architecture

                         LiveTradingBot
  +----------------------------------------------------------+
  |                                                          |
  |  +----------+  +--------------+  +------------------+   |
  |  | Market   |  |  WebSocket   |  |  Chainlink RTDS  |   |
  |  | Finder   |  |  Client      |  |  Price Client    |   |
  |  | (HTTP)   |  |  (wss://)    |  |  (wss://)        |   |
  |  +----+-----+  +------+-------+  +--------+---------+   |
  |       |               |                    |             |
  |       v               v                    v             |
  |  +-------------------------------------------------+    |
  |  |            MarketState (shared)                  |    |
  |  |  up_token, down_token, btc_price, end_time      |    |
  |  +------------------------+------------------------+    |
  |                           |                              |
  |       +-------------------+-------------------+          |
  |       v                   v                   v          |
  |  +----------+  +---------------+  +------------+        |
  |  |Dashboard |  |  Signal       |  |  Order     |        |
  |  | (Rich)   |  |  Generator    |  |  Executor  |        |
  |  +----------+  +-------+-------+  +------+-----+        |
  |                         |                |               |
  |                         v                v               |
  |              +----------------+  +--------------+        |
  |              | TradingStats   |  | HedgeManager |        |
  |              | (Position/PnL) |  | (GTD orders) |        |
  |              +----------------+  +--------------+        |
  |                                                          |
  |  +--------------+  +------------------+                  |
  |  | AutoRedeemer |  | TelegramNotifier |                  |
  |  | (background) |  | (alerts/charts)  |                  |
  |  +--------------+  +------------------+                  |
  +----------------------------------------------------------+

Main Loop (simplified)

while running:
    market = find_active_btc_15m_market()      # HTTP -> Gamma API
    subscribe_websocket(market.tokens)          # wss:// -> live prices

    while market.is_active:
        update_indicators()                     # every 250ms
        signal = evaluate_strategy()            # check all conditions

        if signal == BUY:
            execute_entry(signal)               # FAK order
            place_hedge()                       # GTD order (opposite token)

        track_drawdown()                        # update min_price_seen

        if time_left <= 10s:
            close_position()                    # record PnL
            break

    wait_for_next_market()                      # ~5-30 seconds

2. Market Structure

Polymarket BTC Up/Down 15-Min Markets

Each market is a binary outcome contract:

  • UP token: Pays $1.00 if BTC price is higher at market end vs. start. Otherwise $0.
  • DOWN token: Pays $1.00 if BTC price is lower at market end vs. start. Otherwise $0.

Tokens trade between $0.01 and $0.99. At any time:

P_UP + P_DOWN ~ 1.00

Market Timing

Markets are aligned to 15-minute epoch boundaries:

T_start = floor(T_now / 900) * 900
T_end   = T_start + 900

Market slug format: btc-updown-15m-{T_start}

Example: btc-updown-15m-1770831900 starts at Unix timestamp 1770831900.

Market Discovery

The bot searches the Gamma API for active markets using offsets from the current 15-minute window:

for offset in [0, 900, -900, 1800]:
    target_ts = current_window + offset
    slug = f"btc-updown-15m-{target_ts}"
    # Query: GET /markets?slug={slug}&active=true&closed=false

3. Data Acquisition Layer

3.1 Market Data WebSocket

URL: wss://ws-subscriptions-clob.polymarket.com/ws/market

Subscribes to both UP and DOWN token IDs. Processes three event types:

last_trade_price - Trade Execution

Each trade is stored as Trade(timestamp, price, size, side) in a deque per token.

Tracked aggregates:

  • trade_count: Total number of trades
  • volume_total: Total contract volume
  • volume_buy: Volume from buy-side
  • volume_sell: Volume from sell-side

price_change - Best Bid/Ask Updates

Updates best_bid and best_ask for each token.

book - Order Book Snapshots

Parses bids and asks arrays, extracts top-of-book:

  • best_bid, best_bid_size
  • best_ask, best_ask_size

Spread:

Spread = P_ask - P_bid

3.2 User WebSocket (Order Tracking)

URL: Polymarket User Channel (authenticated via API credentials)

Tracks order lifecycle: PLACEMENT -> MATCHED -> MINED -> CONFIRMED

Used for:

  • Fill confirmation after entry orders
  • Hedge fill detection
  • Timeout recovery (checking if order filled despite timeout)

URL: wss://ws-live-data.polymarket.com (Polymarket RTDS)

Topic: crypto_prices_chainlink filtered for btc/usd symbol.

See Section 10 for full details.


4. Indicator Calculations (Formulas)

All indicators are calculated from the live trade stream. Each token (UP and DOWN) has its own independent indicator set.

4.1 VWAP (Volume-Weighted Average Price)

VWAP over a configurable time window W (default 30 seconds):

                 SUM(P_i * V_i)   for all trades where (T_now - T_i) <= W
VWAP_W  =  -------------------------
                 SUM(V_i)

Where:

  • P_i = price of trade i
  • V_i = size (volume) of trade i
  • trades(W) = set of trades within the last W seconds

Returns 0.0 if no trades in window.

4.2 Deviation from VWAP

Percentage deviation of the current last-trade price from VWAP:

            P_last - VWAP
D  =  ---------------------- * 100%
              VWAP

Where:

  • D > 0: Price is above VWAP (bullish pressure)
  • D < 0: Price is below VWAP (bearish pressure)
  • D = 0: Price equals VWAP

4.3 Momentum

Momentum compares the current price to the average price W seconds ago (default 60s), using a band of +/-1.5 seconds to smooth:

P_ago  =  mean({P_i  where  T_now - W - d  <=  T_i  <=  T_now - W + d})

              P_last - P_ago
M  =  ------------------------- * 100%
              P_ago

Where:

  • W = momentum_window_sec (default 60)
  • d = averaging band (1.5 seconds)
  • Returns None if no trades exist in the band window

Interpretation:

  • M > 0: Price has risen over the window (positive momentum)
  • M < 0: Price has fallen (negative momentum)

4.4 Z-Score

Statistical z-score of the current price relative to recent trade prices over a 5-second window:

        P_last - mean(prices_5s)
z  =  ----------------------------
           stdev(prices_5s)

Where:

  • mean(prices_5s) = arithmetic mean of all trade prices in last 5 seconds
  • stdev(prices_5s) = standard deviation, with minimum floor of 0.001

Interpretation:

  • z > 2: Price significantly above recent mean (overbought short-term)
  • z < -2: Price significantly below recent mean (oversold short-term)

4.5 Win Rate Lookup

Historical win rates are stored in data/win_rate.csv as a 10x15 matrix:

Price Range min_0 min_1 ... min_14
0.50-0.54 52.2% 50.2% ... 52.7%
0.75-0.79 71.1% 76.4% ... 75.0%
0.85-0.89 68.0% 73.0% ... 93.3%
0.95-0.99 63.2% 68.2% ... 100%

Time bin calculation:

bin = floor(14 - T_remaining / 60)

Where bin is in [0, 14], with bin 0 = first minute, bin 14 = last minute.

Lookup: Given favorite token price P_fav and time bin, the table returns the historical win probability.


5. Signal Generation Engine

The signal generator runs inside Dashboard.create_strategy_panel(), evaluated every 250ms (4 Hz refresh).

5.1 Favorite Token Selection

The "favorite" is the token with the higher last-trade price:

            | UP     if P_UP > P_DOWN
favorite =  |
            | DOWN   otherwise

The favorite's indicators are used for signal evaluation:

  • P_fav = favorite price
  • D_fav = favorite deviation from VWAP
  • M_fav = favorite momentum

5.2 Entry Conditions (ALL must be true)

# Condition Formula Config Parameter
1 Price in range P_min <= P_fav <= P_max min_price, max_price
2 Sufficient time elapsed T_elapsed >= T_min_elapsed min_elapsed_sec
3 Deviation in range D_min < D_fav < D_max min/max_deviation_pct
4 Positive momentum M_fav > 0 -
5 Not too close to end T_remaining > T_no_entry no_entry_before_end_sec

Where:

  • T_elapsed = 900 - T_remaining
  • T_remaining = T_end - T_now

5.3 Signal States

if T_remaining <= T_no_entry:
    -> NO ENTRY (cutoff reached, no further entry this market)

elif ALL 5 conditions TRUE:
    -> BUY {UP|DOWN} (signal triggers execute_entry)

elif P_fav >= 0.70 AND T_elapsed >= T_min_elapsed:
    if M_fav <= 0:      -> ALMOST (need Mom>0%)
    if D_fav >= D_max:   -> ALMOST (Dev too high)
    else:                -> ALMOST (need dev)

else:
    -> WAIT (with specific reason: elapsed/price/dev/mom)

5.4 Signal Flow

Dashboard.create_strategy_panel()
    |
    +-- Evaluates conditions every 250ms
    +-- If BUY: sets self.last_signal = "BUY_UP" or "BUY_DOWN"
    |
    v
Main loop (run_session)
    |
    +-- Reads self.dashboard.last_signal
    +-- Clears signal (one-shot)
    +-- Creates asyncio task: _safe_execute_entry(signal)
    |
    v
execute_entry("BUY_UP" or "BUY_DOWN")

Important: Only ONE entry per market. can_enter() returns False once a position is recorded OR entry is blocked.


6. Order Execution Pipeline

6.1 Pre-Execution Guards

Before placing any order, three guards are checked:

  1. Position check: stats.can_enter() - no existing position, not closed this market, not blocked
  2. Time cutoff: T_remaining > T_no_entry
  3. Token data available: Both UP and DOWN tokens must have data

6.2 Order Configuration

Parameter Value Description
bet_amount_usd 50 USD to risk per trade
price_offset 0.02 Added to best bid for aggressive fill
order_type FAK Fill-And-Kill (immediate or cancel)
max_retries 3 Retry count on failure
retry_delay_ms 300 Delay between retries
fill_timeout_ms 1000 Max wait for fill confirmation
min_contracts 5 Polymarket minimum
max_entry_price 0.88 Hard price ceiling

6.3 Contract Calculation

contracts = floor(bet_amount_usd / P_entry)

Where P_entry = min(P_best_ask, P_max_entry)

The order is placed at:

P_order = P_best_bid + price_offset

6.4 FAK Order Flow

1. Fetch best_bid from orderbook
2. Calculate: P_order = best_bid + price_offset
3. Validate: P_order <= max_entry_price
4. Place FAK BUY order
5. Wait fill_timeout_ms for fill confirmation via WebSocket
6. If filled: record position -> place hedge -> done
7. If timeout: enter recovery mode (see Section 13)
8. If rejected: retry up to max_retries

6.5 Signal Logging

At the moment of execution, a comprehensive snapshot is logged to signals.log:

  • Timestamp, market slug, signal direction, token
  • Elapsed/remaining time, time bin
  • For each token (UP and DOWN):
    • LAST, BID, ASK prices
    • VWAP, Deviation, Z-Score, Momentum
    • Trade count, Total/Buy/Sell volume
  • Win rate lookup value
  • Strategy config parameters
  • Chainlink BTC/USD price, anchor, and deviation

7. Hedge Mechanism

7.1 Purpose

After buying the favorite token (e.g., UP at $0.85), the bot places a hedge order on the opposite token (DOWN) at a very low price ($0.02).

If the trade loses (UP resolves to $0), the hedge may fill, providing the opposite token at $0.02 which resolves to $1.00 -- a $0.98 profit per contract that partially offsets the loss.

7.2 Hedge PnL Math

Without hedge (unhedged loss):

PnL_loss = -C * P_entry

Where C = contracts, P_entry = entry price.

With hedge (if hedge fills before resolution):

PnL_hedged_loss = -C * P_entry  +  C_hedge * (1.00 - P_hedge)

With P_hedge = 0.02:

PnL_hedged_loss = -C * P_entry  +  C_hedge * 0.98

7.3 Hedge Order Type

  • GTD (Good-Till-Date): Limit order that stays on the book until expiry
  • Placed at hedge_price ($0.02) on the opposite token
  • Expires in 1 hour (market resolves in <=15 minutes)
  • Only fills if opposite token price drops to $0.02 (i.e., our side is winning strongly)

7.4 Hedge Fill Tracking

The User WebSocket monitors for fill events matching the hedge order ID:

on_trade(data):
    if data.order_id == hedge_order_id AND status == "MATCHED":
        hedge_mgr.on_hedge_fill(size, price)
        if hedge_mgr.is_fully_hedged:
            stats.record_hedge(contracts, price)

8. Position Lifecycle and PnL Accounting

8.1 Position States

  NO POSITION                    OPEN POSITION
  +----------+   execute_entry   +--------------+
  | can_enter | --------------->  |   LONG UP    |
  |  = true   |                  |   or DOWN    |
  +----------+                   |              |
                                 | entry_price  |
                                 | contracts    |
                                 | hedged?      |
                                 +------+-------+
                                        |
                         check_market_end (T_left <= 10s)
                                        |
                                        v
                                 +--------------+
                                 |  CLOSED      |
                                 |  TradeRecord |
                                 |  (PnL, DD)   |
                                 +--------------+

8.2 PnL Calculation

At market end (10 seconds before expiry), the bot reads the final token price.

Win condition: P_final >= 0.70

Win PnL (token resolves to ~$1.00):

PnL_win = C * 1.00 - C * P_entry = C * (1 - P_entry)

Loss PnL (token resolves to ~$0.00):

PnL_loss = 0 - C * P_entry = -C * P_entry

Examples with C = 64 contracts:

Entry Price Win PnL Loss PnL
$0.75 +$16.00 -$48.00
$0.81 +$12.16 -$51.84
$0.88 +$7.68 -$56.32

8.3 Win Rate and Session Statistics

Win Rate = W / (W + L) * 100%

Total PnL = SUM(PnL_i)  for all trades i = 1..N

Avg Win  = SUM(PnL_w) / count(wins)

Avg Loss = SUM(PnL_l) / count(losses)

8.4 Break-Even Win Rate

For a given entry price P, the minimum win rate needed to break even:

WR_breakeven = P / 1.00 = P
Entry Price Break-Even WR
$0.75 75%
$0.80 80%
$0.85 85%
$0.88 88%

This is why the win rate CSV is critical -- the bot only enters when historical win rate exceeds the break-even threshold for the given price and time bin.


9. Drawdown Tracking

9.1 Per-Trade Drawdown

After entry, the bot tracks the minimum price seen every 250ms:

P_min = min(P_min, P_current)    # updated every 250ms

Initialized at entry: P_min = P_entry

At position close, drawdown is calculated:

Absolute drawdown:

DD_abs = max(0,  P_entry - P_min)

Percentage drawdown:

DD_pct = (DD_abs / P_entry) * 100%

Dollar drawdown (total exposure):

DD_usd = DD_abs * C

9.2 Logging

At market end, logged to signals.log:

Max Drawdown: -0.0500 (-6.17%)
Max DD ($): -$3.20 (min price: 0.7600)

9.3 Live Dashboard

While position is open, the dashboard shows real-time drawdown:

LONG UP @ 0.810 (64 contracts)
   Unrealized: +$3.84 (price: 0.870)
   Max DD: -$1.92 (-3.7%) (low: 0.780)

10.1 Purpose

The Chainlink price feed provides the actual BTC/USD price used by Polymarket to resolve markets. The bot tracks this independently for:

  1. Dashboard display: Shows real-time BTC price and deviation from market start
  2. Signal logging: Records BTC deviation at the moment of each trade entry
  3. Analysis: Understanding how BTC price movement correlates with market outcomes

10.2 Connection

URL:     wss://ws-live-data.polymarket.com
Topic:   crypto_prices_chainlink
Symbol:  btc/usd (filtered in code)

10.3 Anchor Price and Deviation

At each 15-minute boundary, the anchor price is captured as the first tick of the new window:

Window = floor(T_chainlink / 900) * 900

When Window changes (new 15-minute period), the first tick's price becomes the anchor:

P_anchor = price of first tick where Window(T_tick) != Window_previous

BTC Deviation:

Delta_abs = P_current - P_anchor

Delta_pct = (Delta_abs / P_anchor) * 100%

10.4 Calibration Logging

For calibration purposes, every tick within [-15s, +5s] of a 15-minute boundary is logged:

BTC_TICK 16:59:59.000 (local 17:00:00.653) $69,481.26 [-1.000s before 17:00:00]
BTC_TICK 17:00:00.000 (local 17:00:01.578) $69,483.32 [+0.000s after 17:00:00]

Fields:

  • Chainlink timestamp: From the oracle data (millisecond precision)
  • Local timestamp: Server clock time when message was processed
  • Price: BTC/USD price from Chainlink
  • Offset: Seconds before/after the 15-minute boundary

10.5 Watchdog

If no Chainlink messages are received for 30 seconds, the watchdog forces a WebSocket reconnection:

if time.time() - last_msg_time > DATA_TIMEOUT:  # 30 seconds
    ws.close()  # Triggers reconnection in connect() loop

11. Auto-Redemption System

11.1 Purpose

After a market resolves, winning positions must be redeemed on-chain to collect the $1.00 payout per contract.

11.2 Flow

Every 180 seconds:
    1. Fetch all positions from Polymarket Data API
    2. Categorize: active, pending, redeemable
    3. For each redeemable position:
       a. Check oracle resolution (payoutDenominator)
       b. Submit redemption transaction on Polygon
       c. Wait for confirmation

11.3 Implementation Details

  • Runs as a background asyncio task
  • File lock prevents concurrent redemptions
  • Supports both EOA (direct) and Gnosis Safe (proxy) wallets
  • Blockchain transactions require POL (MATIC) for gas fees
  • Runs in a dedicated thread pool to avoid blocking the main event loop

12. Configuration Reference

Strategy Parameters

Parameter config.json Dataclass Default Description
min_price 0.75 0.65 Min favorite token price to enter
max_price 0.88 0.91 Max favorite token price to enter
min_elapsed_sec 500 480 Min seconds since market start
min_deviation_pct 0 5.0 Min VWAP deviation (%)
max_deviation_pct 100 100.0 Max VWAP deviation (%)
no_entry_before_end 335 90 Min seconds remaining for entry
momentum_window_sec 60 120 Momentum lookback window
vwap_window_sec 30 30 VWAP calculation window

Note

: "config.json" = active value. "Dataclass Default" = fallback if field is missing from JSON.

Timing Constraints Visualization

Market: 900 seconds (15 minutes)

0s ----------- 500s ---- 565s ----------- 900s
|               |         |                |
|   NO ENTRY    |  ENTRY  |   NO ENTRY     |
|   (too early) | WINDOW  |  (too late)    |
|               |         |                |
<-min_elapsed-> |         |                |
                |         <---335s cutoff-->|
                |         |                |
                <-- 65s -->
                  allowed

Entry is allowed when:

  • T_elapsed >= 500 seconds AND
  • T_remaining > 335 seconds

This creates a 65-second entry window (from 500s to 565s elapsed).


13. Fault Tolerance and Recovery

13.1 Order Timeout Recovery

When a FAK order times out (no fill confirmation within fill_timeout_ms):

1. Check User WebSocket for recent fills on the token
2. Wait up to ws_recovery_timeout_sec (10s)
3. If fills found:
   -> RECOVERY: Record position from WS fill data
   -> Place hedge as normal
4. If no fills found:
   -> Block entry for rest of market (prevent duplicates)
   -> Log: "Network timeout - no fill detected"

13.2 Entry Blocking

After any failed entry attempt, stats.block_entry() prevents further attempts on the same market. This avoids:

  • Duplicate orders from timeout+retry
  • Repeated failures hitting rate limits

Reset on new market: entry_blocked = False

13.3 WebSocket Reconnection

Market Data WebSocket: On ConnectionClosed, reconnects after 2 seconds. On any other exception, reconnects after 5 seconds.

Chainlink RTDS WebSocket: Same reconnection logic plus a 30-second watchdog that detects silent disconnections (TCP alive but no data flowing).

13.4 Config Validation

At startup, validate_config() checks:

  • Private key exists and starts with "0x"
  • API credentials are set
  • min_price < max_price
  • max_entry_price <= max_price
  • max_deviation_pct > min_deviation_pct

Bot refuses to start if any validation fails.


14. File and Log Architecture

Directory Structure

btc_15m_live/
+-- main.py                    # Main bot (2000+ lines, all core logic)
+-- config.json                # Runtime configuration
+-- .env                       # Secrets (API keys, private key)
+-- chart_pnl.py               # PnL chart generator
+-- PROJECT_LOGIC.md           # This document
+-- data/
|   +-- win_rate.csv           # Historical win rate matrix (10x15)
+-- logs/
|   +-- bot.log                # Main application log
|   +-- signals.log            # Trade signal snapshots
|   +-- orders.log             # Order execution details
|   +-- trading_log.json       # Trade history (JSON persistence)
|   +-- api_activity.json      # API call log
|   +-- pnl_chart.png          # Generated PnL chart
|   +-- equity_chart.png       # Equity curve chart
+-- src/
    +-- config_loader.py       # Configuration loading & validation
    +-- order_executor.py      # FAK order execution with retry
    +-- hedge_manager.py       # GTD hedge order management
    +-- market_finder.py       # Gamma API market discovery
    +-- position_tracker.py    # Position & PnL tracking
    +-- auto_redeemer.py       # On-chain position redemption
    +-- telegram_notifier.py   # Telegram alerts & charts
    +-- user_websocket.py      # User channel WebSocket
    +-- websocket_client.py    # Market data WebSocket
    +-- signal_generator.py    # (Legacy, unused)
    +-- realtime_dashboard.py  # (Legacy, unused)

Log Contents

Log File Contents
bot.log All events: connections, market changes, errors, BTC ticks, anchors
signals.log Full indicator snapshot at each trade + market end with PnL and DD
orders.log Detailed order execution: prices, retries, fills, rejections
trading_log.json Persistent trade array with entry/exit, PnL, drawdown, win/loss

trading_log.json Structure

{
  "trades": [
    {
      "market_slug": "btc-updown-15m-1770831900",
      "token_name": "UP",
      "entry_price": 0.81,
      "exit_price": 0.03,
      "contracts": 64,
      "pnl": -51.84,
      "won": false,
      "timestamp": 1770832790.165,
      "max_drawdown_abs": 0.05,
      "max_drawdown_pct": 6.17
    }
  ],
  "markets_seen": 27
}