项目概述
本项目是一个模块化量化交易框架,支持 OKX 现货和合约市场,核心特性:
- 5 种策略:EMA 金叉/死叉、布林带突破、MACD、RSI 反转、策略组合投票
- ATR 动态止损:根据波动率自适应止损幅度,避免止损过紧或过松
- PySide6 GUI:实时行情条、持仓面板、策略状态、收益曲线
环境依赖
pip install ccxt pandas numpy PySide6 pyqtgraph
核心结构
quant/
├── main.py # 主入口,启动 GUI + 策略引擎
├── engine.py # 交易引擎(下单/平仓/持仓管理)
├── strategies/
│ ├── base.py # 策略基类
│ ├── ema_cross.py
│ ├── boll.py
│ ├── macd.py
│ ├── rsi_rev.py
│ └── combo.py # 组合投票策略
├── indicators.py # 技术指标计算
└── dashboard.py # PySide6 仪表盘
策略基类
from abc import ABC, abstractmethod
import pandas as pd
class BaseStrategy(ABC):
name: str = "base"
weight: float = 1.0 # 组合策略权重
def __init__(self, symbol: str, params: dict = None):
self.symbol = symbol
self.params = params or {}
@abstractmethod
def signal(self, df: pd.DataFrame) -> int:
# 返回信号:1=做多, -1=做空, 0=观望
...
EMA 金叉策略
class EMACrossStrategy(BaseStrategy):
name = "ema_cross"
def __init__(self, symbol, params=None):
super().__init__(symbol, params)
self.fast = self.params.get("fast", 9)
self.slow = self.params.get("slow", 21)
def signal(self, df: pd.DataFrame) -> int:
df["ema_fast"] = df["close"].ewm(span=self.fast).mean()
df["ema_slow"] = df["close"].ewm(span=self.slow).mean()
last = df.iloc[-1]
prev = df.iloc[-2]
if prev["ema_fast"] < prev["ema_slow"] and last["ema_fast"] > last["ema_slow"]:
return 1 # 金叉做多
if prev["ema_fast"] > prev["ema_slow"] and last["ema_fast"] < last["ema_slow"]:
return -1 # 死叉做空
return 0
ATR 动态止损
import numpy as np
def atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
high, low, close = df["high"], df["low"], df["close"]
tr = pd.concat([
high - low,
(high - close.shift()).abs(),
(low - close.shift()).abs()
], axis=1).max(axis=1)
return tr.ewm(span=period).mean()
def calc_stop_loss(entry: float, side: str, df: pd.DataFrame,
atr_mult: float = 2.0) -> float:
# 根据 ATR 动态计算止损价
atr_val = atr(df).iloc[-1]
if side == "long":
return entry - atr_val * atr_mult
else:
return entry + atr_val * atr_mult
布林带策略
class BollStrategy(BaseStrategy):
name = "boll"
def signal(self, df: pd.DataFrame) -> int:
period = self.params.get("period", 20)
std_mult = self.params.get("std", 2.0)
mid = df["close"].rolling(period).mean()
std = df["close"].rolling(period).std()
upper = mid + std_mult * std
lower = mid - std_mult * std
last_close = df["close"].iloc[-1]
last_lower = lower.iloc[-1]
last_upper = upper.iloc[-1]
if last_close <= last_lower:
return 1 # 触下轨,做多
if last_close >= last_upper:
return -1 # 触上轨,做空
return 0
组合投票策略
class ComboStrategy(BaseStrategy):
name = "combo"
def __init__(self, symbol, strategies: list):
super().__init__(symbol)
self.strategies = strategies # [BaseStrategy, ...]
def signal(self, df: pd.DataFrame) -> int:
votes = sum(s.signal(df) * s.weight for s in self.strategies)
total_weight = sum(s.weight for s in self.strategies)
ratio = votes / total_weight
if ratio >= 0.5:
return 1
if ratio <= -0.5:
return -1
return 0
ccxt 交易引擎
import ccxt
class OKXEngine:
def __init__(self, api_key: str, secret: str, passphrase: str, testnet: bool = True):
self.ex = ccxt.okx({
"apiKey": api_key,
"secret": secret,
"password": passphrase,
"sandbox": testnet,
})
def fetch_ohlcv(self, symbol: str, timeframe: str = "1h", limit: int = 200):
import pandas as pd
data = self.ex.fetch_ohlcv(symbol, timeframe, limit=limit)
df = pd.DataFrame(data, columns=["ts", "open", "high", "low", "close", "vol"])
df["ts"] = pd.to_datetime(df["ts"], unit="ms")
return df.set_index("ts")
def market_buy(self, symbol: str, usdt_amount: float):
ticker = self.ex.fetch_ticker(symbol)
qty = usdt_amount / ticker["last"]
return self.ex.create_market_buy_order(symbol, qty)
def market_sell(self, symbol: str, qty: float):
return self.ex.create_market_sell_order(symbol, qty)
PySide6 实时仪表盘
from PySide6.QtWidgets import QMainWindow, QLabel, QHBoxLayout, QWidget
from PySide6.QtCore import QTimer
from PySide6.QtGui import QColor
class Dashboard(QMainWindow):
def __init__(self, engine: OKXEngine):
super().__init__()
self.engine = engine
self.setWindowTitle("OKX 量化仪表盘")
self.resize(1200, 700)
self.setStyleSheet("background:#0D1117;color:#E2E8F0;")
self._build_ui()
self._start_ticker()
def _build_ui(self):
central = QWidget()
self.setCentralWidget(central)
layout = QHBoxLayout(central)
self.ticker_label = QLabel("BTC/USDT: --")
self.ticker_label.setStyleSheet("font-size:24px;font-weight:bold;color:#22C55E;")
layout.addWidget(self.ticker_label)
def _start_ticker(self):
self.timer = QTimer(self)
self.timer.timeout.connect(self._update_ticker)
self.timer.start(2000) # 2秒刷新
def _update_ticker(self):
try:
ticker = self.engine.ex.fetch_ticker("BTC/USDT")
price = ticker["last"]
change = ticker["percentage"]
color = "#22C55E" if change >= 0 else "#EF4444"
self.ticker_label.setText(
f"BTC/USDT: ${price:,.2f} {change:+.2f}%"
)
self.ticker_label.setStyleSheet(f"font-size:24px;font-weight:bold;color:{color};")
except Exception as e:
print(f"Ticker error: {e}")
回测结果示例
| 策略 | 时间段 | 收益率 | 最大回撤 | 胜率 |
|---|---|---|---|---|
| EMA(9,21) | 2023 | +34% | -12% | 54% |
| 布林带(20,2) | 2023 | +28% | -15% | 51% |
| MACD | 2023 | +19% | -18% | 49% |
| RSI 反转 | 2023 | +41% | -20% | 58% |
| 组合投票 | 2023 | +52% | -9% | 61% |
风险提示:量化交易存在亏损风险,历史回测不代表未来收益,请控制仓位,谨慎使用。