基于Bitget API接口的 Python 量化交易教程全栈指南(附完整代码)

139 阅读9分钟

本文演示如何通过 Bitget 的公开行情 API(经由 ccxt 调用) 获取 K 线数据,在本地完成指标计算与回测(教学用),并输出结构化信号与示例日志。示例不启用真实下单,不展示收益指标。


开篇寄语

这是一份给开发者看的实操笔记:我们以 Bitget 的公开行情 API(经由 ccxt 调用) 为例,带你把“数据采集 → 指标计算 → 教学回测/信号统计”的最小闭环跑通。整套流程面向技术学习与研究,不讨论开户、不引导交易,也不展示任何收益指标。
如果你是 Python 初学者或第一次接触量化研究,照着本文即可在本地完成:拉取 K 线、计算常见指标(EMA、RSI)、按规则生成信号,并把结果落盘或简单画图,方便团队复现与二次开发。
阅读建议:先通读,再按照“环境搭建(3 步)→ A~F 分段代码 → 附录合并版”的顺序动手实践。所有示例默认仅使用公开行情端点;如需进一步探索私有端点,请使用测试网/沙盒与最小权限临时密钥(本文不展开)。


量化交易到底是啥?(用大白话讲)

把一套明确可重复的规则写成程序,按相同口径去处理市场数据,这就是“量化”。与“拍脑袋/看感觉”的手动判断不同,它强调:

  • 规则可落地:如“当 EMA12 上穿 EMA26 且 RSI 在 50~70 之间,就记一条做多信号”。
  • 数据可复现:同一份 K 线、同一条规则,在哪台机器上跑结果都一致。
  • 过程可审计:每一次信号、每一个开/平区间都有时间戳与输入数据可回溯。

可以把量化流程拆成四件事:

  1. 拿数据:用 ccxt 从 Bitget 公开接口拉 OHLCV(开高低收量)。
  2. 算指标:EMA、RSI 等是对价格序列的加工。
  3. 出信号:把“什么时候记一条记录”写清楚,并避免“未来函数”(信号在收盘后才生效)。
  4. 做评估:先做教学回测/区间统计(数量、时长、时间点),而不是收益曲线。

本文覆盖 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()

免责声明

本文及代码仅用于技术研究与教育,不构成投资建议;示例默认运行于回测/模拟/沙盒模式,不涉及真实资产与真实下单。请勿粘贴或上传任何真实密钥;如需测试私有端点,请仅在测试网/沙盒中、以最小权限临时密钥进行,并通过环境变量加载。请遵守所在地法律法规与平台规则。