本文演示如何通过 Bitget 的公开行情 API(经由 ccxt 调用) 获取 K 线数据,在本地完成指标计算与回测(教学用),并输出结构化信号与示例日志。示例不启用真实下单,不展示收益指标。
开篇寄语
这是一份给开发者看的实操笔记:我们以 Bitget 的公开行情 API(经由 ccxt 调用) 为例,带你把“数据采集 → 指标计算 → 教学回测/信号统计”的最小闭环跑通。整套流程面向技术学习与研究,不讨论开户、不引导交易,也不展示任何收益指标。
如果你是 Python 初学者或第一次接触量化研究,照着本文即可在本地完成:拉取 K 线、计算常见指标(EMA、RSI)、按规则生成信号,并把结果落盘或简单画图,方便团队复现与二次开发。
阅读建议:先通读,再按照“环境搭建(3 步)→ A~F 分段代码 → 附录合并版”的顺序动手实践。所有示例默认仅使用公开行情端点;如需进一步探索私有端点,请使用测试网/沙盒与最小权限临时密钥(本文不展开)。
量化交易到底是啥?(用大白话讲)
把一套明确可重复的规则写成程序,按相同口径去处理市场数据,这就是“量化”。与“拍脑袋/看感觉”的手动判断不同,它强调:
- 规则可落地:如“当 EMA12 上穿 EMA26 且 RSI 在 50~70 之间,就记一条做多信号”。
- 数据可复现:同一份 K 线、同一条规则,在哪台机器上跑结果都一致。
- 过程可审计:每一次信号、每一个开/平区间都有时间戳与输入数据可回溯。
可以把量化流程拆成四件事:
- 拿数据:用 ccxt 从 Bitget 公开接口拉 OHLCV(开高低收量)。
- 算指标:EMA、RSI 等是对价格序列的加工。
- 出信号:把“什么时候记一条记录”写清楚,并避免“未来函数”(信号在收盘后才生效)。
- 做评估:先做教学回测/区间统计(数量、时长、时间点),而不是收益曲线。
本文覆盖 1~3 步 + 教学级别的第 4 步,帮助你把数据与规则打通;后续关于策略有效性、风控与真实执行,属于另外一套工程与合规课题,这里不展开。
技术栈选型(新手友好版)
语言与运行环境
- Python 3.9+:生态成熟、上手快,数据处理与绘图工具齐全。
- venv/conda:建议使用虚拟环境,依赖更可控、团队复现更容易。
核心库
- ccxt:统一适配多家交易所的 API;本文用它访问 Bitget 公开行情。
- pandas / numpy:处理时间序列、实现 EMA/RSI。
- matplotlib(可选):画“收盘价 + EMA”示意图,便于人工检查。
为什么此阶段不引入更“大”的框架?
- 打样/教学阶段,越轻越好:依赖少、部署快、错误面小。确定数据/指标/信号口径后,再考虑引入回测/执行框架降低工程成本。
最小安装清单(requirements.txt)
ccxt>=4.3.0
pandas>=2.0.0
numpy>=1.24.0
matplotlib>=3.7.0
目录建议
bitget-quant-teaching/
├─ main.py
├─ requirements.txt
└─ data/
一次性环境验证
import ccxt
ex = ccxt.bitget({"enableRateLimit": True})
if hasattr(ex, "set_sandbox_mode"):
ex.set_sandbox_mode(True) # 防误操作的好习惯
print(ex.fetch_ticker("BTC/USDT")["last"])
口径与合规注意
- 本文只用公开行情端点即可完成全部内容。
- 若研究私有端点,请使用测试网/沙盒 + 最小权限临时密钥,并通过环境变量加载,勿把密钥写进代码或文章。
- 遵守接口限频与使用条款,避免无意义的高频抓取。
环境搭建(3 步)
Step 1|项目与目录
mkdir bitget-quant-teaching && cd bitget-quant-teaching
mkdir data
Step 2|安装依赖
python -V # 建议 3.9+
pip install -U ccxt pandas numpy matplotlib
Step 3|快速验证
import ccxt
ex = ccxt.bitget({"enableRateLimit": True})
if hasattr(ex, "set_sandbox_mode"):
ex.set_sandbox_mode(True)
print(ex.fetch_ticker("BTC/USDT")["last"])
Step A:创建 Bitget 交易所实例(含沙盒与保险开关)
import os, ccxt
EXCHANGE_ID = os.getenv("EXCHANGE_ID", "bitget")
USE_SANDBOX = True
ALLOW_TRADE = False # 顶层保险开关(不启用真实交易)
def build_exchange(exchange_id: str):
cls = getattr(ccxt, exchange_id)
ex = cls({
"enableRateLimit": True,
"options": {"defaultType": "spot"},
"timeout": 15000,
"userAgent": "bitget-quant-teaching/1.0",
})
if hasattr(ex, "set_sandbox_mode"):
try:
ex.set_sandbox_mode(USE_SANDBOX) # 防误操作
except Exception:
pass
return ex
Step B:获取 K 线(OHLCV)并转为 DataFrame
import pandas as pd
import os
SYMBOL = os.getenv("SYMBOL", "BTC/USDT")
TIMEFRAME = os.getenv("TIMEFRAME", "1h")
LIMIT = int(os.getenv("LIMIT", "1000"))
def fetch_ohlcv_df(exchange, symbol: str, timeframe: str, limit: int) -> pd.DataFrame:
o = exchange.fetch_ohlcv(symbol=symbol, timeframe=timeframe, limit=limit)
df = pd.DataFrame(o, columns=["ts","open","high","low","close","volume"])
df["time_utc"] = pd.to_datetime(df["ts"], unit="ms", utc=True)
df.set_index("time_utc", inplace=True)
return df
Step C:指标计算(EMA、RSI)
import numpy as np
import pandas as pd
def ema(series: pd.Series, span: int) -> pd.Series:
return series.ewm(span=span, adjust=False).mean()
def rsi(series: pd.Series, period: int = 14) -> pd.Series:
delta = series.diff()
gain = (delta.clip(lower=0)).ewm(alpha=1/period, adjust=False).mean()
loss = (-delta.clip(upper=0)).ewm(alpha=1/period, adjust=False).mean()
rs = gain / (loss.replace(0, np.nan))
out = 100 - (100 / (1 + rs))
return out.fillna(50)
def enrich_indicators(df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
out["ema_fast"] = ema(out["close"], 12)
out["ema_slow"] = ema(out["close"], 26)
out["rsi_14"] = rsi(out["close"], 14)
return out
Step D:策略信号(EMA 交叉 + RSI 过滤,收盘后生效)
import pandas as pd
def gen_signals(df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
cross_up = (out["ema_fast"] > out["ema_slow"]) & (out["ema_fast"].shift(1) <= out["ema_slow"].shift(1))
cross_down = (out["ema_fast"] < out["ema_slow"]) & (out["ema_fast"].shift(1) >= out["ema_slow"].shift(1))
long_cond = cross_up & (out["rsi_14"].between(50, 70))
short_cond = cross_down & (out["rsi_14"].between(30, 50))
out["signal"] = 0
out.loc[long_cond.shift(1, fill_value=False), "signal"] = 1
out.loc[short_cond.shift(1, fill_value=False), "signal"] = -1
return out
Step E:教学回测(统计信号与持仓区间)
import pandas as pd
from typing import Tuple
def teaching_backtest(df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.DataFrame]:
signals_df = df.copy()
position = 0 # 1=long, -1=short, 0=flat
entry_time = entry_price = None
logs = []
for idx, row in signals_df.iterrows():
sig = int(row["signal"])
px = float(row["close"])
if position == 0 and sig in (1, -1): # 开仓
position, entry_time, entry_price = sig, idx, px
elif position != 0 and ((position == 1 and sig == -1) or (position == -1 and sig == 1)): # 反向→平仓并反手
logs.append({
"direction": "long" if position == 1 else "short",
"entry_time": entry_time,
"exit_time": idx,
"entry_close": entry_price,
"exit_close": px
})
position, entry_time, entry_price = sig, idx, px
trade_log = pd.DataFrame(logs)
if not trade_log.empty:
tf_to_min = {
"1m": 1, "3m": 3, "5m": 5, "15m": 15, "30m": 30,
"1h": 60, "2h": 120, "4h": 240, "6h": 360, "8h": 480, "12h": 720, "1d": 1440
}
step = tf_to_min.get(TIMEFRAME, 1)
trade_log["bars_held"] = ((trade_log["exit_time"] - trade_log["entry_time"])
.dt.total_seconds() / 60.0 / step).round().clip(lower=1).astype(int)
return signals_df, trade_log
Step F:数据落盘与可视化(可选)
from pathlib import Path
OUT_DIR = Path("./data"); OUT_DIR.mkdir(parents=True, exist_ok=True)
def save_outputs(signals_df: pd.DataFrame, trade_log: pd.DataFrame):
ohlcv_out = OUT_DIR / "ohlcv_signals.csv"
log_out = OUT_DIR / "trade_log.csv"
signals_df.reset_index().to_csv(ohlcv_out, index=False)
if not trade_log.empty:
tlog = trade_log.copy()
tlog["entry_time"] = tlog["entry_time"].dt.tz_convert("UTC").dt.strftime("%Y-%m-%d %H:%M:%S")
tlog["exit_time"] = tlog["exit_time"].dt.tz_convert("UTC").dt.strftime("%Y-%m-%d %H:%M:%S")
tlog.to_csv(log_out, index=False)
def plot_close_ema(signals_df: pd.DataFrame, path: Path):
try:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(10,5))
ax = fig.add_subplot(111)
signals_df["close"].plot(ax=ax, label="close")
signals_df["ema_fast"].plot(ax=ax, label="ema_fast(12)")
signals_df["ema_slow"].plot(ax=ax, label="ema_slow(26)")
ax.set_title(f"{EXCHANGE_ID} {SYMBOL} {TIMEFRAME} close & EMA")
ax.set_xlabel("time (UTC)")
ax.legend()
fig.tight_layout()
fig.savefig(path, dpi=150)
plt.close(fig)
except Exception as e:
print(f"[warn] 绘图失败:{e}")
主程序(串起来跑一遍)
def main():
ex = build_exchange(EXCHANGE_ID)
df = fetch_ohlcv_df(ex, SYMBOL, TIMEFRAME, LIMIT)
if df.empty:
print("[error] 未获取到任何K线数据;请检查交易对/周期/网络。"); return
df_ind = enrich_indicators(df)
df_sig = gen_signals(df_ind)
signals_df, trade_log = teaching_backtest(df_sig)
long_cnt = int((signals_df["signal"] == 1).sum())
short_cnt = int((signals_df["signal"] == -1).sum())
print(f"[info] 信号统计:long={long_cnt}, short={short_cnt}")
save_outputs(signals_df, trade_log)
plot_close_ema(signals_df, Path("./data/close_ema.png"))
if __name__ == "__main__":
main()
附:完整代码(保存为 main.py 运行)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from pathlib import Path
from typing import Tuple
import numpy as np
import pandas as pd
import ccxt
# =============== 全局配置 ===============
EXCHANGE_ID = os.getenv("EXCHANGE_ID", "bitget")
SYMBOL = os.getenv("SYMBOL", "BTC/USDT")
TIMEFRAME = os.getenv("TIMEFRAME", "1h")
LIMIT = int(os.getenv("LIMIT", "1000"))
USE_SANDBOX = True
ALLOW_TRADE = False # 不启用真实交易
OUT_DIR = Path("./data")
OUT_DIR.mkdir(parents=True, exist_ok=True)
# =============== 构建交易所实例(公开行情) ===============
def build_exchange(exchange_id: str) -> ccxt.Exchange:
cls = getattr(ccxt, exchange_id)
ex = cls({
"enableRateLimit": True,
"options": {"defaultType": "spot"},
"timeout": 15000,
"userAgent": "bitget-quant-teaching/1.0",
})
if hasattr(ex, "set_sandbox_mode"):
try:
ex.set_sandbox_mode(USE_SANDBOX)
except Exception:
pass
return ex
# =============== 获取 K 线(OHLCV) ===============
def fetch_ohlcv_df(exchange: ccxt.Exchange, symbol: str, timeframe: str, limit: int) -> pd.DataFrame:
o = exchange.fetch_ohlcv(symbol=symbol, timeframe=timeframe, limit=limit)
df = pd.DataFrame(o, columns=["ts","open","high","low","close","volume"])
df["time_utc"] = pd.to_datetime(df["ts"], unit="ms", utc=True)
df.set_index("time_utc", inplace=True)
return df
# =============== 指标 ===============
def ema(series: pd.Series, span: int) -> pd.Series:
return series.ewm(span=span, adjust=False).mean()
def rsi(series: pd.Series, period: int = 14) -> pd.Series:
delta = series.diff()
gain = (delta.clip(lower=0)).ewm(alpha=1/period, adjust=False).mean()
loss = (-delta.clip(upper=0)).ewm(alpha=1/period, adjust=False).mean()
rs = gain / (loss.replace(0, np.nan))
out = 100 - (100 / (1 + rs))
return out.fillna(50)
def enrich_indicators(df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
out["ema_fast"] = ema(out["close"], 12)
out["ema_slow"] = ema(out["close"], 26)
out["rsi_14"] = rsi(out["close"], 14)
return out
# =============== 信号 ===============
def gen_signals(df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
cross_up = (out["ema_fast"] > out["ema_slow"]) & (out["ema_fast"].shift(1) <= out["ema_slow"].shift(1))
cross_down = (out["ema_fast"] < out["ema_slow"]) & (out["ema_fast"].shift(1) >= out["ema_slow"].shift(1))
long_cond = cross_up & (out["rsi_14"].between(50, 70))
short_cond = cross_down & (out["rsi_14"].between(30, 50))
out["signal"] = 0
out.loc[long_cond.shift(1, fill_value=False), "signal"] = 1
out.loc[short_cond.shift(1, fill_value=False), "signal"] = -1
return out
# =============== 教学回测(仅统计区间) ===============
def teaching_backtest(df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.DataFrame]:
signals_df = df.copy()
position = 0
entry_time = entry_price = None
logs = []
for idx, row in signals_df.iterrows():
sig = int(row["signal"]); px = float(row["close"])
if position == 0 and sig in (1, -1):
position, entry_time, entry_price = sig, idx, px
elif position != 0 and ((position == 1 and sig == -1) or (position == -1 and sig == 1)):
logs.append({
"direction": "long" if position == 1 else "short",
"entry_time": entry_time,
"exit_time": idx,
"entry_close": entry_price,
"exit_close": px
})
position, entry_time, entry_price = sig, idx, px
trade_log = pd.DataFrame(logs)
if not trade_log.empty:
tf_to_min = {"1m":1,"3m":3,"5m":5,"15m":15,"30m":30,"1h":60,"2h":120,"4h":240,"6h":360,"8h":480,"12h":720,"1d":1440}
step = tf_to_min.get(TIMEFRAME, 1)
trade_log["bars_held"] = ((trade_log["exit_time"] - trade_log["entry_time"]).dt.total_seconds() / 60.0 / step) .round().clip(lower=1).astype(int)
return signals_df, trade_log
# =============== 输出与可视化 ===============
def save_outputs(signals_df: pd.DataFrame, trade_log: pd.DataFrame):
ohlcv_out = OUT_DIR / "ohlcv_signals.csv"
log_out = OUT_DIR / "trade_log.csv"
signals_df.reset_index().to_csv(ohlcv_out, index=False)
if not trade_log.empty:
tlog = trade_log.copy()
tlog["entry_time"] = tlog["entry_time"].dt.tz_convert("UTC").dt.strftime("%Y-%m-%d %H:%M:%S")
tlog["exit_time"] = tlog["exit_time"].dt.tz_convert("UTC").dt.strftime("%Y-%m-%d %H:%M:%S")
tlog.to_csv(log_out, index=False)
def plot_close_ema(signals_df: pd.DataFrame, path: Path):
try:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(10,5))
ax = fig.add_subplot(111)
signals_df["close"].plot(ax=ax, label="close")
signals_df["ema_fast"].plot(ax=ax, label="ema_fast(12)")
signals_df["ema_slow"].plot(ax=ax, label="ema_slow(26)")
ax.set_title(f"{EXCHANGE_ID} {SYMBOL} {TIMEFRAME} close & EMA")
ax.set_xlabel("time (UTC)"); ax.legend()
fig.tight_layout(); fig.savefig(path, dpi=150); plt.close(fig)
except Exception as e:
print(f"[warn] 绘图失败:{e}")
# =============== 主流程 ===============
def main():
ex = build_exchange(EXCHANGE_ID)
df = fetch_ohlcv_df(ex, SYMBOL, TIMEFRAME, LIMIT)
if df.empty:
print("[error] 未获取到任何K线数据;请检查交易对/周期/网络。"); return
df_ind = enrich_indicators(df)
df_sig = gen_signals(df_ind)
signals_df, trade_log = teaching_backtest(df_sig)
long_cnt = int((signals_df["signal"] == 1).sum())
short_cnt = int((signals_df["signal"] == -1).sum())
print(f"[info] 信号统计:long={long_cnt}, short={short_cnt}")
save_outputs(signals_df, trade_log)
plot_close_ema(signals_df, OUT_DIR / "close_ema.png")
if __name__ == "__main__":
main()
免责声明
本文及代码仅用于技术研究与教育,不构成投资建议;示例默认运行于回测/模拟/沙盒模式,不涉及真实资产与真实下单。请勿粘贴或上传任何真实密钥;如需测试私有端点,请仅在测试网/沙盒中、以最小权限临时密钥进行,并通过环境变量加载。请遵守所在地法律法规与平台规则。