ในฐานะนักพัฒนาที่ดูแลระบบ 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 |