这是一个非常棒的实践性问题!将 Backtrader 从回测框架扩展到模拟或实盘交易,是每个量化交易者都会遇到的关键一步。

你无法直接将 Backtrader 连接到同花顺的模拟交易。原因在于:

  • 没有官方API:同花顺、东方财富等国内券商软件,没有为个人开发者提供公开、稳定、免费的交易API。它们的软件是封闭的系统。

  • Backtrader的机制:Backtrader的实时交易模式需要一个实现了其 Broker 和 DataFeed 接口的“桥梁”来与外部券商进行通信。

那么,如何实现这个流程呢?我们需要一个**中间件(或称为“桥梁”)**来模拟人在操作同花顺客户端。目前最流行和可行的方案是使用 easytrader 这个库。

核心思路:
Backtrader (你的策略) -> 自定义Broker -> easytrader -> 同花顺客户端 -> 同花顺模拟交易服务器

easytrader 是一个通过自动化操作PC客户端(如点击、输入等)来实现程序化交易的Python库。

下面,我将为你详细讲解实现这个交易流程的步骤,并提供关键代码示例。


交易流程实现步骤

第1步:环境准备
  1. 安装 Backtrader:

    Generated bash
    pip install backtrader

    Use code with caution.Bash
  2. 安装 easytrader:

    Generated bash
    pip install easytrader

    Use code with caution.Bash
  3. 安装同花顺PC客户端:

    • 去同花顺官网下载并安装最新版的PC客户端。

    • 登录你的账户,并确保你已经开通了模拟交易功能。

    • 关键:在你的策略运行时,同花顺客户端必须处于登录状态,并停留在交易主界面。

  4. 准备一个实时数据源:

    • easytrader 只负责交易,不负责提供行情数据。Backtrader在实时模式下需要一个数据源。

    • 你可以使用 Tushare, AkShare, baostock 等免费数据接口来获取实时或分钟级的K线数据。这里我们以 AkShare 为例。

    Generated bash
    pip install akshare

    Use code with caution.Bash
第2步:创建自定义的 Broker

这是最核心的一步。我们需要创建一个类,继承 backtrader.BrokerBase,并实现其中的关键方法,让这些方法的内部逻辑调用 easytrader 来完成。

ths_broker.py (示例代码)

Generated python

import backtrader as bt
import easytrader

class TongHuaShunBroker(bt.BrokerBase):
    """
    一个连接同花顺和 Backtrader 的自定义 Broker
    """
    def __init__(self, **kwargs):
        super(TongHuaShunBroker, self).__init__()
        self.user = easytrader.use('ths')  # 初始化 easytrader,使用同花顺
        # 这里需要根据你同花顺客户端的路径进行配置
        # 例如: self.user.connect(r'C:\ths\xiadan.exe')
        # 如果 easytrader 能够自动找到,则无需 connect
        
        self.orders = [] # 存储 Backtrader 的订单对象

    def start(self):
        super(TongHuaShunBroker, self).start()
        print("Broker started. Connecting to TongHuaShun...")
        # 尝试获取余额,以验证连接
        try:
            balance = self.user.balance
            print("Successfully connected to THS. Balance info:", balance)
        except Exception as e:
            print(f"Failed to connect to THS: {e}")
            raise ConnectionError("Could not connect to TongHuaShun client.")

    def getcash(self):
        # 获取可用现金
        cash = self.user.balance.get('enable_balance', 0.0)
        return cash

    def getvalue(self, datas=None):
        # 获取总资产
        value = self.user.balance.get('asset_balance', 0.0)
        return value
        
    def getposition(self, data, clone=True):
        # 获取指定标的的持仓
        # easytrader 的持仓返回一个列表,需要查找
        code = data._name # data._name 通常是股票代码
        positions = self.user.position
        for pos in positions:
            if pos['stock_code'] == code:
                # Backtrader 需要一个 Position 对象
                position = bt.position.Position(size=pos['current_amount'], price=pos['cost_price'])
                return position
        # 如果没有找到,返回空仓位
        return bt.position.Position(size=0, price=0.0)

    def _submit(self, owner, data, side, exectype, size, price):
        # 实际的下单逻辑
        code = data._name
        try:
            if side == bt.Order.BUY:
                result = self.user.buy(stock_code=code, price=price, amount=size)
                print(f"BUY Order Submitted to THS: {result}")
            elif side == bt.Order.SELL:
                result = self.user.sell(stock_code=code, price=price, amount=size)
                print(f"SELL Order Submitted to THS: {result}")
            else:
                return None # 不支持其他订单类型
            
            # 创建一个 Backtrader 的 Order 对象并返回
            order = bt.Order(
                data=data,
                owner=owner,
                # ... 其他参数 ...
            )
            order.submit(self)
            self.orders.append(order)
            return order

        except Exception as e:
            print(f"Order submission failed for {code}: {e}")
            return None

    # 你需要实现 buy, sell, cancel 等方法
    # Backtrader 会调用这些公共方法,它们内部应调用 _submit 或 _cancel
    def buy(self, owner, data, size, price=None, plimit=None,
            exectype=None, valid=None, tradeid=0, oco=None,
            trailamount=None, trailpercent=None, **kwargs):
        
        exectype = exectype or bt.Order.Market # 简化处理,默认为市价单
        price = price or data.close[0] # 如果是市价单,价格不重要,但 easytrader 需要一个价格
        
        return self._submit(owner, data, bt.Order.BUY, exectype, size, price)

    def sell(self, owner, data, size, price=None, plimit=None,
             exectype=None, valid=None, tradeid=0, oco=None,
             trailamount=None, trailpercent=None, **kwargs):
        
        exectype = exectype or bt.Order.Market
        price = price or data.close[0]
        
        return self._submit(owner, data, bt.Order.SELL, exectype, size, price)

    # 注意:这是一个简化的 Broker。一个完整的 Broker 还需要处理订单状态更新、
    # 部分成交、撤单(cancel)等复杂情况。这需要轮询 `user.entrust` 来获取订单状态并更新。

Use code with caution.Python

第3步:创建自定义的 DataFeed

我们需要一个能从 AkShare 获取数据并喂给 Backtrader 的数据源。

akshare_data.py (示例代码)

Generated python

import backtrader as bt
import akshare as ak
import pandas as pd
from datetime import datetime, time

class AkShareData(bt.feeds.PandasData):
    """
    一个从 AkShare 获取实时/历史数据的 Data Feed
    """
    params = (
        ('fromdate', datetime(2023, 1, 1)),
        ('todate', datetime.now()),
        ('timeframe', bt.TimeFrame.Minutes),
        ('compression', 1), # 1分钟线
        ('stock_code', ''),
    )

    def __init__(self):
        super(AkShareData, self).__init__()
        self.get_data()

    def get_data(self):
        print(f"Fetching data for {self.p.stock_code}...")
        # AkShare 获取分钟级数据接口示例
        df = ak.stock_zh_a_hist_min_em(
            symbol=self.p.stock_code,
            period=str(self.p.compression),
            start_date=self.p.fromdate.strftime('%Y-%m-%d %H:%M:%S'),
            end_date=self.p.todate.strftime('%Y-%m-%d %H:%M:%S')
        )
        
        # 数据清洗和格式化以符合 backtrader 要求
        df.rename(columns={
            '时间': 'datetime',
            '开盘': 'open',
            '收盘': 'close',
            '最高': 'high',
            '最低': 'low',
            '成交量': 'volume'
        }, inplace=True)
        
        df['datetime'] = pd.to_datetime(df['datetime'])
        df.set_index('datetime', inplace=True)
        df['openinterest'] = 0 # backtrader 需要这一列

        # 将数据加载到 PandasData 中
        self.p.dataname = df

    # 在实时模式下,你需要实现 _load() 方法来动态获取新数据
    # 这里我们先用历史数据模拟

Use code with caution.Python

第4步:组装并运行 Cerebro

现在,我们将自定义的 Broker、DataFeed 和你的策略组合在一起。

run_ths.py

Generated python

import backtrader as bt
from datetime import datetime
from ths_broker import TongHuaShunBroker # 导入自定义 Broker
from akshare_data import AkShareData    # 导入自定义 DataFeed

# 1. 你的交易策略
class MyStrategy(bt.Strategy):
    def next(self):
        # 这是一个非常简单的示例策略:如果当前时间是 14:55,就买入100股
        current_time = self.data.datetime.time()
        if current_time == time(14, 55):
            print(f"{self.data.datetime.datetime(0)}: It's 14:55, buying 100 shares...")
            self.buy(size=100) # easytrader 买入单位是“股”

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            print("Order Submitted/Accepted")
            return

        if order.status in [order.Completed]:
            if order.isbuy():
                print(f"BUY EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}")
            elif order.issell():
                print(f"SELL EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}")

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            print("Order Canceled/Margin/Rejected")

# 2. 主程序
if __name__ == '__main__':
    # 确保同花顺客户端已经登录并打开
    
    cerebro = bt.Cerebro()

    # 3. 设置自定义 Broker
    my_broker = TongHuaShunBroker()
    cerebro.setbroker(my_broker)
    
    # 4. 添加数据
    # 注意:这里我们用历史分钟数据来模拟。
    # 在真实的实时场景中,你需要让 DataFeed 动态加载数据,
    # 并在 cerebro.run() 中设置 qlive=True
    stock_code = '600519' # 以贵州茅台为例
    data = AkShareData(
        dataname=stock_code,
        fromdate=datetime(2023, 11, 1),
        todate=datetime(2023, 11, 30),
        timeframe=bt.TimeFrame.Minutes,
        compression=1,
        stock_code=stock_code # 将股票代码传入
    )
    cerebro.adddata(data, name=stock_code) # name 参数很重要,Broker会用它

    # 5. 添加策略
    cerebro.addstrategy(MyStrategy)

    # 6. 设置初始资金(这只是给 backtrader 一个参考,实际资金以来 THS Broker)
    cerebro.broker.setcash(100000.0)

    # 7. 运行
    print("Starting strategy...")
    cerebro.run()
    print("Final Portfolio Value: %.2f" % cerebro.broker.getvalue())

Use code with caution.Python

运行和注意事项

  1. 先运行同花顺:在运行 Python 脚本之前,必须手动打开同花顺PC客户端,登录你的模拟交易账户。

  2. easytrader的连接:easytrader 可能需要你指定同花顺下单程序的路径,这可以在 TongHuaShunBroker 的 __init__ 中通过 self.user.connect(...) 来设置。

  3. 简化与现实的差距

    • 订单状态:上面的 Broker 示例非常简化。现实中,订单提交后不会立即成交,你需要轮询查询订单状态,并据此更新 Backtrader 内部的订单对象。这是实现一个健壮 Broker 最复杂的部分。

    • 错误处理:网络中断、交易被拒、资金不足等情况都需要完善的错误处理逻辑。

    • 实时数据:真正的实时交易需要一个能推送或轮询最新K线数据的 DataFeed。你需要修改 AkShareData 的 _load 方法来实现这个功能,并使用 cerebro.run(qlive=True)。

    • 风险:easytrader 这种方式依赖于客户端UI,如果同花顺软件更新,可能会导致脚本失效。它存在一定的稳定性和延迟问题,不适用于高频交易。

总结:
通过 easytrader 作为桥梁,将 Backtrader 连接到同花顺模拟交易是完全可行的。核心工作是编写一个自定义的 Broker 类,将 Backtrader 的交易指令(如 buy, sell, getcash)翻译成 easytrader 的函数调用。虽然过程比较复杂,但这是目前在A股市场进行个人程序化交易最主流的方案之一。

Logo

加入社区!打开量化的大门,首批课程上线啦!

更多推荐