ในฐานะนักพัฒนาที่ดูแลระบบ Quantitative Trading มากว่า 3 ปี ผมเชื่อว่า Dashboard ที่ดีคือหัวใจของการตัดสินใจลงทุน วันนี้จะมาแชร์วิธีการสร้างระบบ Monitor คริปโตแบบ Real-time ด้วย Tardis และ Grafana ที่ผมใช้จริงในการ Track พอร์ตโฟลิโอ

ทำไมต้องเป็น Tardis + Grafana

Tardis เป็นบริการที่รวบรวม Data Feed จาก Exchange หลายตัว (Binance, Bybit, OKX, Bitget) มาพร้อม WebSocket API ที่รองรับ Order Book, Trade, Ticker และ Candlestick แบบเรียลไทม์ ส่วน Grafana คือเครื่องมือ Visualize ข้อมูลที่ยืดหยุ่นที่สุดในตลาด รองรับ Data Source หลากหลาย และมี Alerting System ที่ครบวงจร

เหมาะกับใคร / ไม่เหมาะกับใคร

กลุ่มเป้าหมายความเหมาะสม
นักเทรดรายวัน (Day Trader) เหมาะมาก — ต้องการ Data ละเอียดถึง Millisecond
Quantitative Fund เหมาะมาก — Backtest และ Live Monitor ในระบบเดียว
สถาบันการเงิน เหมาะปานกลาง — ต้องการ Enterprise License เพิ่มเติม
ผู้เริ่มต้นเทรด ไม่เหมาะ — ซับซ้อนเกินไป ควรเริ่มจาก TradingView ก่อน

การติดตั้ง Docker Compose

ขั้นตอนแรกคือการตั้งค่า Environment ด้วย Docker Compose ซึ่งจะรวม Tardis Exchange Adapter, InfluxDB (สำหรับเก็บ Time Series Data) และ Grafana ไว้ใน Container เดียว

version: '3.8'

services:
  # Tardis Exchange Adapter - รวบรวม Data Feed จาก Exchange
  tardis:
    image: ghcr.io/tardis-dev/tardis-webapp:latest
    container_name: tardis-exchange
    ports:
      - "3000:3000"
    environment:
      - EXCHANGE_ADAPTERS=binance,bybit,okx,bitget
      - RUST_LOG=info
    volumes:
      - ./tardis/data:/app/data
    networks:
      - crypto-monitor

  # InfluxDB - Time Series Database
  influxdb:
    image: influxdb:2.7
    container_name: influxdb-crypto
    ports:
      - "8086:8086"
    environment:
      - DOCKER_INFLUXDB_INIT_MODE=setup
      - DOCKER_INFLUXDB_INIT_USERNAME=admin
      - DOCKER_INFLUXDB_INIT_PASSWORD=secure_password_123
      - DOCKER_INFLUXDB_INIT_ORG=quantlab
      - DOCKER_INFLUXDB_INIT_BUCKET=crypto_metrics
      - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-token
    volumes:
      - ./influxdb:/var/lib/influxdb
    networks:
      - crypto-monitor

  # Grafana - Visualization & Alerting
  grafana:
    image: grafana/grafana:latest
    container_name: grafana-dashboard
    ports:
      - "3001:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=grafana_secure_pass
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - ./grafana:/var/lib/grafana
    depends_on:
      - influxdb
    networks:
      - crypto-monitor

  # MQTT Broker - Real-time Data Pipeline
  mqtt:
    image: eclipse-mosquitto:latest
    container_name: mqtt-broker
    ports:
      - "1883:1883"
    volumes:
      - ./mosquitto/config:/mosquitto/config
      - ./mosquitto/data:/mosquitto/data
    networks:
      - crypto-monitor

networks:
  crypto-monitor:
    driver: bridge
# สร้างโครงสร้างโฟลเดอร์
mkdir -p crypto-dashboard/{tardis,influxdb,grafana,mosquitto/{config,data}}
cd crypto-dashboard

สร้าง config MQTT

cat > mosquitto/config/mosquitto.conf << 'EOF' listener 1883 allow_anonymous true persistence true persistence_location /mosquitto/data/ EOF

Start ระบบทั้งหมด

docker-compose up -d

ตรวจสอบสถานะ

docker-compose ps

สคริปต์ Python ดึงข้อมูลจาก Tardis ไป InfluxDB

หลังจากติดตั้ง Docker เสร็จ ต่อไปจะเป็นการเขียนสคริปต์ Python เพื่อดึง Data จาก Tardis WebSocket แล้วเก็บลง InfluxDB พร้อมใช้งานกับ Grafana

# requirements.txt

pip install -r requirements.txt

tardis-realtime>=1.5.0

influxdb-client>=1.40.0

python-dateutil>=2.8.2

asyncio-nats-client>=0.17.0

import asyncio import json from datetime import datetime from tardis_realtime import TardisRealtime from influxdb_client import InfluxDBClient, Point, WriteOptions from decimal import Decimal

Configuration

TARDIS_WS_URL = "wss://api.tardis-dev.com/v1/feed" INFLUX_URL = "http://localhost:8086" INFLUX_TOKEN = "my-super-secret-token" INFLUX_ORG = "quantlab" INFLUX_BUCKET = "crypto_metrics"

InfluxDB Client

influx_client = InfluxDBClient( url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG ) write_api = influx_client.write_api( write_options=WriteOptions( batch_size=500, flush_interval=1000, retry_interval=5000 ) )

Symbols ที่ต้องการ Monitor

SYMBOLS = ["BTC-USDT", "ETH-USDT", "SOL-USDT", "BNB-USDT", "XRP-USDT"] class CryptoDataCollector: def __init__(self): self.tardis = TardisRealtime() self.price_cache = {} self.volume_cache = {} async def on_ticker(self, data): """xử lý Ticker Data ราคา + Volume แบบ Real-time""" try: symbol = data.get("symbol", "") price = float(data.get("last", 0)) volume_24h = float(data.get("volume", 0)) timestamp = datetime.utcnow() # เก็บ Cache สำหรับ Calculate self.price_cache[symbol] = price self.volume_cache[symbol] = volume_24h # สร้าง InfluxDB Point point = Point("crypto_ticker") \ .tag("symbol", symbol) \ .tag("exchange", data.get("exchange", "unknown")) \ .field("price", price) \ .field("volume_24h", volume_24h) \ .field("bid", float(data.get("bid", 0))) \ .field("ask", float(data.get("ask", 0))) \ .field("spread", float(data.get("ask", 0)) - float(data.get("bid", 0))) \ .time(timestamp) write_api.write(bucket=INFLUX_BUCKET, org=INFLUX_ORG, record=point) except Exception as e: print(f"[Ticker Error] {e}") async def on_trade(self, data): """xử lý Trade Data สำหรับ Volume Analysis""" try: symbol = data.get("symbol", "") price = float(data.get("price", 0)) quantity = float(data.get("quantity", 0)) side = data.get("side", "buy") # buy or sell timestamp = datetime.utcnow() point = Point("crypto_trades") \ .tag("symbol", symbol) \ .tag("exchange", data.get("exchange", "unknown")) \ .tag("side", side) \ .field("price", price) \ .field("quantity", quantity) \ .field("value", price * quantity) \ .time(timestamp) write_api.write(bucket=INFLUX_BUCKET, org=INFLUX_ORG, record=point) except Exception as e: print(f"[Trade Error] {e}") async def on_orderbook(self, data): """xử lý Order Book Data สำหรับ Liquidity Analysis""" try: symbol = data.get("symbol", "") bids = data.get("bids", []) asks = data.get("asks", []) # Calculate Order Book Imbalance total_bid_volume = sum([float(b[1]) for b in bids[:10]]) total_ask_volume = sum([float(a[1]) for a in asks[:10]]) if total_bid_volume + total_ask_volume > 0: imbalance = (total_bid_volume - total_ask_volume) / (total_bid_volume + total_ask_volume) else: imbalance = 0 timestamp = datetime.utcnow() point = Point("crypto_orderbook") \ .tag("symbol", symbol) \ .tag("exchange", data.get("exchange", "unknown")) \ .field("bid_volume_10", total_bid_volume) \ .field("ask_volume_10", total_ask_volume) \ .field("imbalance", imbalance) \ .field("best_bid", float(bids[0][0]) if bids else 0) \ .field("best_ask", float(asks[0][0]) if asks else 0) \ .time(timestamp) write_api.write(bucket=INFLUX_BUCKET, org=INFLUX_ORG, record=point) except Exception as e: print(f"[OrderBook Error] {e}") async def start(self): """เริ่มต้น Subscribe ไปยัง Exchange ทั้งหมด""" print("🔄 เริ่มเชื่อมต่อ Tardis Exchange Feed...") # Subscribe สำหรับแต่ละ Exchange exchanges = ["binance", "bybit", "okx"] for exchange in exchanges: await self.tardis.subscribe( exchange=exchange, channels=["ticker", "trade", "orderbook"], symbols=SYMBOLS, handler=self._route_message ) print(f"✅ Subscribed สำเร็จ: {', '.join(exchanges)}") print(f"📊 Monitoring: {', '.join(SYMBOLS)}") # Keep running await asyncio.Future() async def _route_message(self, exchange, channel, data): """Route Message ไปยัง Handler ที่เหมาะสม""" if channel == "ticker": await self.on_ticker(data) elif channel == "trade": await self.on_trade(data) elif channel == "orderbook": await self.on_orderbook(data) if __name__ == "__main__": collector = CryptoDataCollector() asyncio.run(collector.start())

สร้าง Grafana Dashboard JSON

หลังจากมี Data ใน InfluxDB แล้ว ต่อไปจะเป็นการสร้าง Dashboard ที่แสดงราคา, Volume, Order Book Imbalance และ Alert เมื่อราคาผันผวนผิดปกติ

{
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": {
          "type": "grafana",
          "uid": "-- Grafana --"
        },
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "graphTooltip": 1,
  "id": 1,
  "links": [],
  "liveNow": false,
  "panels": [
    {
      "datasource": {
        "type": "influxdb",
        "uid": "crypto_influxdb"
      },
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 2,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "never",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "line"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              }
            ]
          },
          "unit": "currencyUSD"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 16,
        "x": 0,
        "y": 0
      },
      "id": 1,
      "options": {
        "legend": {
          "calcs": ["last", "max", "min"],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "multi",
          "sort": "desc"
        }
      },
      "targets": [
        {
          "datasource": {
            "type": "influxdb",
            "uid": "crypto_influxdb"
          },
          "query": "from(bucket: \"crypto_metrics\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"crypto_ticker\")\n  |> filter(fn: (r) => r.symbol == \"BTC-USDT\")\n  |> aggregateWindow(every: v.windowPeriod, fn: last, createEmpty: false)\n  |> yield(name: \"last\")",
          "refId": "A"
        }
      ],
      "title": "BTC/USDT Real-time Price",
      "type": "timeseries"
    },
    {
      "datasource": {
        "type": "influxdb",
        "uid": "crypto_influxdb"
      },
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "thresholds"
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "red",
                "value": null
              },
              {
                "color": "yellow",
                "value": 0.3
              },
              {
                "color": "green",
                "value": 0.6
              }
            ]
          },
          "unit": "percentunit"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 8,
        "x": 16,
        "y": 0
      },
      "id": 2,
      "options": {
        "orientation": "auto",
        "reduceOptions": {
          "calcs": ["lastNotNull"],
          "fields": "",
          "values": false
        },
        "showThresholdLabels": false,
        "showThresholdMarkers": true
      },
      "targets": [
        {
          "datasource": {
            "type": "influxdb",
            "uid": "crypto_influxdb"
          },
          "query": "from(bucket: \"crypto_metrics\")\n  |> range(start: -5m)\n  |> filter(fn: (r) => r._measurement == \"crypto_orderbook\")\n  |> filter(fn: (r) => r.symbol == \"BTC-USDT\")\n  |> last()\n  |> yield(name: \"imbalance\")",
          "refId": "A"
        }
      ],
      "title": "Order Book Imbalance",
      "type": "gauge"
    },
    {
      "datasource": {
        "type": "influxdb",
        "uid": "crypto_influxdb"
      },
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "fillOpacity": 80,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineWidth": 1,
            "scaleDistribution": {
              "type": "linear"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              }
            ]
          },
          "unit": "short"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 0,
        "y": 8
      },
      "id": 3,
      "options": {
        "barRadius": 0,
        "barWidth": 0.97,
        "groupWidth": 0.7,
        "legend": {
          "calcs": [],
          "displayMode": "list",
          "placement": "bottom",
          "showLegend": true
        },
        "orientation": "auto",
        "showValue": "auto",
        "stacking": "none",
        "tooltip": {
          "mode": "single",
          "sort": "none"
        },
        "xTickLabelRotation": 0,
        "xTickLabelSpacing": 0
      },
      "targets": [
        {
          "datasource": {
            "type": "influxdb",
            "uid": "crypto_influxdb"
          },
          "query": "from(bucket: \"crypto_metrics\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"crypto_ticker\")\n  |> filter(fn: (r) => r._field == \"volume_24h\")\n  |> last()",
          "refId": "A"
        }
      ],
      "title": "24h Volume by Symbol",
      "type": "barchart"
    },
    {
      "datasource": {
        "type": "influxdb",
        "uid": "crypto_influxdb"
      },
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "thresholds"
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              }
            ]
          },
          "unit": "currencyUSD"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 4,
        "w": 6,
        "x": 12,
        "y": 8
      },
      "id": 4,
      "options": {
        "colorMode": "value",
        "graphMode": "area",
        "justifyMode": "auto",
        "orientation": "auto",
        "reduceOptions": {
          "calcs": ["lastNotNull"],
          "fields": "",
          "values": false
        },
        "textMode": "auto"
      },
      "targets": [
        {
          "query": "from(bucket: \"crypto_metrics\") |> range(start: -1m) |> filter(fn: (r) => r._measurement == \"crypto_ticker\" and r.symbol == \"BTC-USDT\") |> last()",
          "refId": "A"
        }
      ],
      "title": "BTC Price",
      "type": "stat"
    },
    {
      "datasource": {
        "type": "influxdb",
        "uid": "crypto_influxdb"
      },
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "thresholds"
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              }
            ]
          },
          "unit": "currencyUSD"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 4,
        "w": 6,
        "x": 18,
        "y": 8
      },
      "id": 5,
      "targets": [
        {
          "query": "from(bucket: \"crypto_metrics\") |> range(start: -1m) |> filter(fn: (r) => r._measurement == \"crypto_ticker\" and r.symbol == \"ETH-USDT\") |> last()",
          "refId": "A"
        }
      ],
      "title": "ETH Price",
      "type": "stat"
    }
  ],
  "refresh": "5s",
  "schemaVersion": 38,
  "style": "dark",
  "tags": ["crypto", "quant", "trading"],
  "templating": {
    "list": []
  },
  "time": {
    "from": "now-1h",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "browser",
  "title": "Crypto Quant Dashboard",
  "uid": "crypto-quant-001",
  "version": 1,
  "weekStart": ""
}

ตั้งค่า Alert Rules สำหรับการแจ้งเตือน

การ Monitor แบบ Passive ไม่เพียงพอ ต้องมี Alert System ที่แจ้งเตือนเมื่อเกิดสถานการณ์ผิดปกติ เช่น ราคาร่วงหรือขึ้นผิดปกติ, Volume ผิดปกติ, หรือ Spread กว้างเกินไป

# Grafana Alert Rule - Price Spike Detection

POST /api/v1/provisioning/alert-rules

{ "uid": "price-spike-alert", "title": "Price Spike Alert", "condition": "C", "data": [ { "refId": "A", "relativeTimeRange": { "from": 300, "to": 0 }, "datasourceUid": "crypto_influxdb", "model": { "bucketAggs": [], "expr": "from(bucket: \"crypto_metrics\") |> range(start: -5m) |> filter(fn: (r) => r._measurement == \"crypto_ticker\" and r.symbol == \"BTC-USDT\") |> difference()", "intervalMs": 1000, "maxDataPoints": 43200, "refId": "A" } }, { "refId": "B", "relativeTimeRange": { "from": 300, "to": 0 }, "datasourceUid": "__expr__", "model": { "conditions": [ { "evaluator": { "params": [0.02], "type": "gt" }, "operator": { "type": "and" }, "query": { "params": ["A"] }, "reducer": { "params": [], "type": "last" }, "type": "query" } ], "datasource": { "type": "__expr__", "uid": "__expr__" }, "expression": "A", "intervalMs": 1000, "maxDataPoints": 43200, "reducer": "last", "refId": "B", "type": "reduce" } }, { "refId": "C", "relativeTimeRange": { "from": 0, "to": 0 }, "datasourceUid": "__expr__", "model": { "conditions": [ { "evaluator": { "params": [0], "type": "gt" }, "operator": { "type": "and" }, "query": { "params": ["B"] }, "reducer": { "params": [], "type": "avg" }, "type": "query" } ], "datasource": { "type": "__expr__", "uid": "__expr__" }, "expression": "B", "intervalMs": 1000, "maxDataPoints": 43200, "refId": "C", "type": "threshold" } } ], "noDataState": "NoData", "execErrState": "Error", "for": "1m", "annotations": { "description": "BTC/USDT ราคาเปลี่ยนแปลงเกิน 2% ภายใน 5 นาที", "summary": "Price Spike Detected on BTC/USDT" }, "labels": { "severity": "critical", "team": "quant" }, "isPaused": false }

Alert Contact Point - Telegram Integration

{ "name": "telegram-alerts", "type": "telegram", "settings": { "botToken": "YOUR_TELEGRAM_BOT_TOKEN", "chatId": "YOUR_CHAT_ID", "message": "{{ if eq .Status \"firing\" }}🔥 สถานะ: กำลังดำเนินการ\n{{ else }}✅ สถานะ: กลับสู่ปกติ\n{{ end }}\n\n📊 Alert: {{ .Labels.alertname }}\n💰 Symbol: {{ .Labels.symbol }}\n📈 ราคา: {{ .Values.A.Value }}\n⏰ เวลา: {{ .StartsAt }}" } }

ราคาและ ROI

รายการราคาประมาณหมายเหตุ
Tardis Basic $99/เดือน 3 Exchange,

🔥 ลอง HolySheep AI

เกตเวย์ AI API โดยตรง รองรับ Claude, GPT-5, Gemini, DeepSeek — หนึ่งคีย์ ไม่ต้อง VPN

👉 สมัครฟรี →