6.V2脚本开发基础
从这一章开始,我们真正动手写代码。前面几章把环境搭好、交易所连上、V2框架的原理讲透,现在该把这些知识转化成能跑的脚本了。V2脚本开发并不复杂,核心是把交易逻辑塞进几个固定的方法里,再配上一套参数系统,就能让策略在Hummingbot里跑起来。
V2脚本架构与生命周期
V2脚本的骨架继承自 StrategyV2Base 类,这个基类在 hummingbot/strategy/strategy_v2_base.py 里定义。它本身又是 ScriptStrategyBase 的子类,所以V2脚本同时具备了V1脚本的简洁性和V2框架的模块化能力。
继承关系
整个继承链条是这样的:StrategyBase → ScriptStrategyBase → StrategyV2Base。最顶层的 StrategyBase 是Cython写的,处理底层事件循环和订单管理;ScriptStrategyBase 用Python封装了一层,提供了 buy()、sell() 这些直接操作市场的方法;StrategyV2Base 再往上加了两样东西:一是统一的市场数据接口,二是基于Executor的订单管理机制。
写V2脚本时,我们很少需要直接调用 buy() 或 sell()。取而代之的是创建Executor,让Executor去处理订单的生命周期。这种设计把"决策"和"执行"分开了,脚本专注生成交易信号,Executor专注把订单管好。
生命周期方法
一个V2脚本实例化后会经历几个阶段:
__init__:初始化连接器、市场数据提供者、配置参数on_start:策略启动时调用一次,适合在这里初始化Candles数据源on_tick:每隔tick_size(默认1秒)调用一次,是策略的核心循环format_status:用户执行status命令时调用,返回要展示的文本on_stop:策略停止时调用,做清理工作
on_tick 是最重要的方法。它里面通常会有一个主循环,检查市场数据、计算指标、生成信号,然后创建或更新Executor。这个方法必须轻量,不能阻塞,否则会影响整个事件循环。
配置参数定义与解析
V2框架最大的改进之一是把参数管理标准化了。以前写脚本得硬编码参数,现在可以定义一个配置类,让Hummingbot自动生成配置文件,还能在运行时动态更新。
配置类定义
配置类继承自 BaseClientModel,用Pydantic的 Field 定义每个参数。每个字段可以带 client_data,里面指定提示语、默认值、是否在新创建时提示用户等。
class SimpleMACDConfig(StrategyV2ConfigBase):
script_file_name: str = Field(default_factory=lambda: os.path.basename(__file__))
# 交易配置
exchange: str = Field(
"binance_perpetual",
client_data=ClientFieldData(
prompt_on_new=True,
prompt=lambda mi: "交易所名称 (如 binance_perpetual):"
)
)
trading_pair: str = Field(
"BTC-USDT",
client_data=ClientFieldData(
prompt_on_new=True,
prompt=lambda mi: "交易对 (如 BTC-USDT):"
)
)
order_amount: Decimal = Field(
Decimal("100"),
client_data=ClientFieldData(
prompt_on_new=True,
prompt=lambda mi: "订单金额 (USD):"
)
)
# 指标参数
fast_period: int = Field(12, client_data=ClientFieldData(prompt_on_new=False))
slow_period: int = Field(26, client_data=ClientFieldData(prompt_on_new=False))
prompt_on_new=True 的参数会在运行 create --script-config 时提示用户输入。prompt_on_new=False 的参数不会提示,但可以在配置文件里手动修改。default_factory 用来动态生成默认值,比如当前文件名。
配置文件创建
写好配置类后,在Hummingbot客户端执行:
create --script-config simple_macd.py
客户端会扫描 /scripts 目录,列出所有带配置类的脚本。选中我们的脚本后,会逐个提示 prompt_on_new=True 的参数。输入完毕后,配置文件保存在 conf/scripts/simple_macd.yml。
配置文件是YAML格式,可以手动编辑。修改后保存,脚本会在下一个 config_update_interval(默认60秒)自动加载新参数,无需重启。这种热更新能力在生产环境特别有用。
on_tick方法实现逻辑
on_tick 是策略的心跳,每秒跳动一次。它的实现模式通常分三步:获取数据、计算信号、执行动作。
数据获取
通过 market_data_provider 获取市场数据,这是V2框架的统一入口:
def on_tick(self):
# 获取最新价格
price = self.market_data_provider.get_price_by_type(
self.config.exchange,
self.config.trading_pair,
PriceType.MidPrice
)
# 获取K线数据
candles_df = self.market_data_provider.get_candles_df(
self.config.exchange,
self.config.trading_pair,
"1m",
100
)
# 获取订单簿
bids_df, asks_df = self.market_data_provider.get_order_book_snapshot(
self.config.exchange,
self.config.trading_pair
)
所有数据获取都是异步的,但 market_data_provider 帮我们封装好了,调用时数据已经准备好。candles_df 是Pandas DataFrame,可以直接用 pandas_ta 计算指标。
信号计算
拿到数据后,用技术指标生成交易信号。这里以MACD为例:
import pandas_ta as ta
def on_tick(self):
# 获取数据
candles_df = self.market_data_provider.get_candles_df(...)
# 计算MACD
macd_df = ta.macd(
candles_df["close"],
fast=self.config.fast_period,
slow=self.config.slow_period
)
# 获取最新信号
current_macd = macd_df.iloc[-1]["MACD_12_26_9"]
current_signal = macd_df.iloc[-1]["MACDs_12_26_9"]
# 生成信号
if current_macd > current_signal:
signal = 1 # 做多
elif current_macd < current_signal:
signal = -1 # 做空
else:
signal = 0 # 观望
信号可以是简单的多空判断,也可以是复杂的打分系统。关键是把逻辑封装成纯函数,方便测试。
执行动作
根据信号创建或更新Executor。V2框架提供了几种内置Executor:
PositionExecutor:管理单向仓位,带止损止盈GridExecutor:网格交易TWAPExecutor:时间加权平均价格执行
from hummingbot.smart_components.executors.position_executor import PositionExecutor
def on_tick(self):
# ... 获取信号
if signal == 1 and not self.long_executor:
# 创建多头Executor
self.long_executor = PositionExecutor(
strategy=self,
connector_name=self.config.exchange,
trading_pair=self.config.trading_pair,
side=TradeType.BUY,
amount=self.config.order_amount,
stop_loss=0.01,
take_profit=0.02
)
self.long_executor.start()
elif signal == -1 and self.long_executor:
# 关闭多头
self.long_executor.stop()
self.long_executor = None
Executor一旦启动,会自动处理订单创建、跟踪、止损止盈。脚本只需要在合适的时机启动或停止它。
format_status状态展示
status 命令是调试策略的利器。format_status 方法返回的字符串会原样显示在客户端里。默认实现只展示余额和活动订单,我们需要重写它来展示策略核心状态。
基础实现
def format_status(self) -> str:
if not self.ready_to_trade:
return "市场连接器未就绪"
lines = []
lines.append(f"策略: {self.config.strategy_name}")
lines.append(f"交易对: {self.config.trading_pair}")
lines.append(f"最新价格: {self.market_data_provider.get_price_by_type(...)}")
# 展示指标状态
if self.all_candles_ready:
candles_df = self.market_data_provider.get_candles_df(...)
lines.append(f"MACD: {candles_df.iloc[-1]['MACD_12_26_9']:.4f}")
lines.append(f"Signal: {candles_df.iloc[-1]['MACDs_12_26_9']:.4f}")
# 展示Executor状态
if self.long_executor:
lines.append(f"多头仓位: {self.long_executor.status}")
lines.append(f" 盈亏: {self.long_executor.pnl:.2f} USD")
return "\n".join(lines)
状态信息分三类:市场数据、指标数值、Executor状态。展示太多会刷屏,只放关键信息。
高级技巧
可以用 tabulate 库格式化表格,或者用ANSI颜色高亮重要信息。Hummingbot客户端支持基本的颜色代码:
def format_status(self) -> str:
lines = []
# 红色表示亏损
pnl = self.long_executor.pnl if self.long_executor else 0
color = "\033[91m" if pnl < 0 else "\033[92m" # 红/绿
reset = "\033[0m"
lines.append(f"盈亏: {color}{pnl:.2f} USD{reset}")
return "\n".join(lines)
不过颜色要慎用,太多会眼花缭乱。
市场订单创建与取消
虽然V2推荐用Executor,但有时候需要直接操作订单。ScriptStrategyBase 提供了底层方法。
创建订单
# 限价买单
order_id = self.buy(
connector_name="binance_perpetual",
trading_pair="BTC-USDT",
amount=Decimal("0.01"),
order_type=OrderType.LIMIT,
price=Decimal("65000")
)
# 市价卖单
order_id = self.sell(
connector_name="binance_perpetual",
trading_pair="BTC-USDT",
amount=Decimal("0.01"),
order_type=OrderType.MARKET
)
buy() 和 sell() 返回 order_id,可以用来跟踪订单状态。amount 是交易货币的数量,不是USD。OrderType 枚举包括 LIMIT、MARKET、LIMIT_MAKER 等。
取消订单
# 取消单个订单
self.cancel("binance_perpetual", "BTC-USDT", order_id)
# 取消所有订单
self.cancel_all_orders("binance_perpetual", "BTC-USDT")
取消是异步操作,命令发出后订单不会立刻消失,需要等待交易所确认。可以在 did_cancel_order 事件处理器里做后续处理。
事件处理
订单状态变化会触发事件,重写这些方法可以自定义处理逻辑:
def did_fill_order(self, event: OrderFilledEvent):
self.logger().info(f"订单成交: {event.order_id} {event.trade_type} {event.amount}")
def did_cancel_order(self, event: OrderCancelledEvent):
self.logger().info(f"订单取消: {event.order_id}")
def did_fail_order(self, event: MarketOrderFailureEvent):
self.logger().error(f"订单失败: {event.order_id} {event.error}")
事件系统是基于回调的,不需要轮询订单状态。这在高频场景下效率更高。
账户数据访问接口
策略需要实时知道账户里有多少钱、多少仓位。V2框架提供了几个便捷方法。
余额查询
# 获取全部余额DataFrame
balance_df = self.get_balance_df()
print(balance_df)
# 获取单个资产余额
connector = self.connectors["binance_perpetual"]
btc_balance = connector.get_balance("BTC")
usdt_balance = connector.get_balance("USDT")
get_balance_df() 返回的DataFrame包含四列:Exchange、Asset、Total Balance、Available Balance。get_balance() 返回的是可用余额,不包括挂单冻结的部分。
活动订单
# 获取所有活动订单DataFrame
orders_df = self.active_orders_df()
print(orders_df)
# 获取单个订单
order = self.connectors["binance_perpetual"].get_order(order_id)
active_orders_df() 返回的DataFrame包含Exchange、Market、Side、Price、Amount、Age等列。Age是订单存活时间,用来判断是否需要刷新。
仓位信息
对于永续合约,还需要查询仓位:
connector = self.connectors["binance_perpetual"]
position = connector.get_position("BTC-USDT")
if position:
print(f"仓位大小: {position.amount}")
print(f"开仓价格: {position.entry_price}")
print(f"未实现盈亏: {position.unrealized_pnl}")
get_position() 返回 Position 对象,包含仓位方向、数量、杠杆等信息。现货连接器调用这个方法会返回 None。
基础脚本示例解析
理论讲得差不多,来看一个完整的可运行示例。这个脚本实现了一个简单的RSI均值回归策略:RSI低于30时买入,高于70时卖出,用 PositionExecutor 管理仓位。
完整代码
import os
from decimal import Decimal
from pydantic import Field
import pandas_ta as ta
from hummingbot.strategy.strategy_v2_base import StrategyV2Base, StrategyV2ConfigBase
from hummingbot.core.data_type.common import OrderType, PriceType, TradeType
from hummingbot.smart_components.executors.position_executor import PositionExecutor
from hummingbot.client.config.config_data_types import ClientFieldData
from hummingbot.core.data_type.trade_fee import TokenAmount
class SimpleRSIConfig(StrategyV2ConfigBase):
script_file_name: str = Field(default_factory=lambda: os.path.basename(__file__))
exchange: str = Field(
"binance_perpetual",
client_data=ClientFieldData(
prompt_on_new=True,
prompt=lambda mi: "交易所名称:"
)
)
trading_pair: str = Field(
"BTC-USDT",
client_data=ClientFieldData(
prompt_on_new=True,
prompt=lambda mi: "交易对:"
)
)
order_amount: Decimal = Field(
Decimal("50"),
client_data=ClientFieldData(
prompt_on_new=True,
prompt=lambda mi: "订单金额 (USD):"
)
)
rsi_length: int = Field(14, client_data=ClientFieldData(prompt_on_new=False))
rsi_low: int = Field(30, client_data=ClientFieldData(prompt_on_new=False))
rsi_high: int = Field(70, client_data=ClientFieldData(prompt_on_new=False))
class SimpleRSIStrategy(StrategyV2Base):
def __init__(self, connectors):
super().__init__(connectors)
self.config = None
self.position_executor = None
def on_start(self):
# 初始化配置
self.config = SimpleRSIConfig.load_from_file(self.config_file_path)
# 初始化K线数据源
self.market_data_provider.initialize_candles_feed(
connector=self.config.exchange,
trading_pair=self.config.trading_pair,
interval="1m",
max_records=500
)
def on_tick(self):
if not self.market_data_provider.ready:
return
# 获取RSI
candles_df = self.market_data_provider.get_candles_df(
self.config.exchange,
self.config.trading_pair,
"1m",
self.config.rsi_length + 10
)
rsi_series = ta.rsi(candles_df["close"], length=self.config.rsi_length)
current_rsi = rsi_series.iloc[-1]
# 检查是否需要开平仓
if current_rsi < self.config.rsi_low and not self.position_executor:
# RSI过低,开多
self.position_executor = PositionExecutor(
strategy=self,
connector_name=self.config.exchange,
trading_pair=self.config.trading_pair,
side=TradeType.BUY,
amount=self.config.order_amount,
stop_loss=0.02,
take_profit=0.03
)
self.position_executor.start()
self.notify_hb_app(f"RSI {current_rsi:.1f} < {self.config.rsi_low},开多")
elif current_rsi > self.config.rsi_high and self.position_executor:
# RSI过高,平多
self.position_executor.stop()
self.position_executor = None
self.notify_hb_app(f"RSI {current_rsi:.1f} > {self.config.rsi_high},平多")
def format_status(self) -> str:
if not self.ready_to_trade:
return "市场连接器未就绪"
lines = []
lines.append(f"策略: SimpleRSI")
lines.append(f"交易对: {self.config.trading_pair}")
# 显示RSI
if self.market_data_provider.ready:
candles_df = self.market_data_provider.get_candles_df(...)
rsi = ta.rsi(candles_df["close"], length=self.config.rsi_length).iloc[-1]
lines.append(f"RSI: {rsi:.2f}")
# 显示Executor状态
if self.position_executor:
lines.append(f"仓位状态: {self.position_executor.status}")
lines.append(f"盈亏: {self.position_executor.pnl:.2f} USD")
return "\n".join(lines)
代码解析
这个脚本展示了V2开发的完整流程:
- 配置定义:
SimpleRSIConfig类定义了所有参数,带提示语,支持配置文件 - 初始化:
on_start里加载配置、初始化K线数据源 - 主逻辑:
on_tick里计算RSI,根据阈值创建或关闭Executor - 状态展示:
format_status展示RSI值和Executor状态 - 通知系统:
notify_hb_app在关键操作时发送通知
运行步骤
把代码保存为 /scripts/simple_rsi.py,然后在Hummingbot客户端:
# 创建配置文件
create --script-config simple_rsi.py
# 按提示输入参数,保存为 conf_simple_rsi.yml
# 启动策略
start --script simple_rsi.py --conf conf_simple_rsi.yml
# 查看状态
status
# 实时状态
status --live
策略会每秒检查一次RSI,满足条件就自动下单。PositionExecutor 会管理止损止盈,不需要我们操心。
总结
V2脚本开发的核心是理解三个东西:配置系统、on_tick 循环、Executor机制。配置让参数管理变得规范;on_tick 是策略大脑,每秒做一次决策;Executor是执行层,负责把订单管好。把这三件事搞明白,就能写出健壮的交易策略。
这一章只讲了基础,下一章会深入控制器设计。控制器能把策略逻辑进一步抽象,让同一个脚本跑多个配置,甚至多个策略混跑。这是V2框架真正强大的地方。