Files
NewStock/qmt/backtest_consumer.py
liaozhaorun 7b4112b70b efactor(qmt): 重构架构 - 统一信号发送和消息处理
- 修复 qmt_engine.py 中 initialize() 的重复代码
- 新增 message_processor.py: Redis Stream 消息处理器
- 新增 logger.py: 细粒度日志模块
- 新增 qmt_sender.py: 统一信号发送端(槽位+百分比模式)
- 新增 backtest_consumer.py: 回测消息消费者
- 删除旧模块: qmt_trader.py, qmt_signal_sender.py, qmt_percentage_sender.py
- 更新文档: qmt_functionality.md 反映新架构
2026-02-25 20:49:56 +08:00

441 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# coding: utf-8
"""
QMT 回测消息消费者
独立的回测消息消费脚本,用于:
- 消费回测消息流
- 记录完整的处理流程日志
- 模拟订单执行(不执行真实交易)
- ACK 确认消息
使用方法:
# 守护模式(持续运行,处理所有配置的策略)
python backtest_consumer.py
# 单次运行模式(处理一次后退出)
python backtest_consumer.py --once
# 指定策略运行
python backtest_consumer.py --strategy StrategyA
python backtest_consumer.py --once --strategy StrategyA,StrategyB
配置说明:
可通过环境变量或 .env.local 文件配置:
- REDIS_HOST: Redis 主机地址(默认: localhost
- REDIS_PORT: Redis 端口(默认: 6379
- REDIS_PASSWORD: Redis 密码(默认: None
- REDIS_DB: Redis 数据库(默认: 0
- BACKTEST_CONSUMER_ID: 消费者ID默认: backtest-consumer-1
- BACKTEST_STRATEGIES: 默认策略列表,逗号分隔
- LOG_LEVEL: 日志级别(默认: DEBUG
- LOG_FILE: 日志文件路径(默认: logs/backtest_consumer.log
消息格式:
消费的消息为 JSON 格式,包含以下字段:
- strategy_name: 策略名称
- stock_code: 股票代码(如 000001.SZ
- action: 交易动作BUY/SELL
- price: 价格
- total_slots: 目标持仓槽位数
- timestamp: 时间戳
- is_backtest: 是否为回测模式
"""
import os
import sys
import time
import json
import signal
import argparse
from datetime import datetime
from typing import List, Optional, Dict, Any
# 添加父目录到路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
from message_processor import StreamMessageProcessor
from logger import QMTLogger
except ImportError as e:
print(f"导入错误: {e}")
print("请确保在 qmt 目录下运行此脚本")
sys.exit(1)
def load_config() -> Dict[str, Any]:
"""加载配置文件
从 /qmt/config/.env.local 加载环境变量配置
Returns:
配置字典
"""
# 尝试加载 python-dotenv
dotenv_loaded = False
try:
from dotenv import load_dotenv
dotenv_available = True
except ImportError:
dotenv_available = False
print("[Config] 警告: 未安装 python-dotenv使用默认配置")
# 如果 dotenv 可用,尝试加载配置文件
if dotenv_available:
# 尝试多个路径
env_paths = [
os.path.join(
os.path.dirname(os.path.abspath(__file__)), "config", ".env.local"
),
os.path.join(os.path.dirname(__file__), "config", ".env.local"),
os.path.join(os.path.dirname(__file__), "..", "config", ".env.local"),
"/qmt/config/.env.local",
"config/.env.local",
]
for env_path in env_paths:
if os.path.exists(env_path):
load_dotenv(env_path)
print(f"[Config] 加载配置文件: {env_path}")
dotenv_loaded = True
break
if not dotenv_loaded:
print("[Config] 警告: 未找到 .env.local 文件,使用默认配置")
# 从环境变量读取配置
config = {
"redis": {
"host": os.getenv("REDIS_HOST", "localhost"),
"port": int(os.getenv("REDIS_PORT", "6379")),
"password": os.getenv("REDIS_PASSWORD") or None,
"db": int(os.getenv("REDIS_DB", "0")),
},
"consumer_id": os.getenv("BACKTEST_CONSUMER_ID", "backtest-consumer-1"),
"block_timeout": int(os.getenv("BACKTEST_BLOCK_TIMEOUT", "5000")),
"strategies": os.getenv("BACKTEST_STRATEGIES", ""),
"simulate_delay": float(os.getenv("BACKTEST_SIMULATE_DELAY", "0.1")),
"log_level": os.getenv("LOG_LEVEL", "DEBUG"),
"log_file": os.getenv("LOG_FILE", "logs/backtest_consumer.log"),
}
# 解析策略列表
if config["strategies"]:
config["strategy_list"] = [
s.strip() for s in config["strategies"].split(",") if s.strip()
]
else:
config["strategy_list"] = []
return config
class BacktestConsumer:
"""回测消息消费者"""
def __init__(self, config: Dict[str, Any]):
"""初始化回测消费者
Args:
config: 配置字典
"""
self.config = config
self.logger = QMTLogger(name="BacktestConsumer", log_file=config["log_file"])
# 初始化 Stream 处理器
self.processor = StreamMessageProcessor(redis_config=config["redis"])
# 运行状态
self.running = True
# 统计信息
self.stats = {
"total_received": 0,
"total_processed": 0,
"total_failed": 0,
"start_time": datetime.now(),
}
self.logger.info(f"[BacktestConsumer] 初始化完成")
self.logger.info(f"[BacktestConsumer] 消费者ID: {config['consumer_id']}")
self.logger.info(
f"[BacktestConsumer] 策略列表: {config['strategy_list'] or '所有策略'}"
)
def signal_handler(self, signum, frame):
"""信号处理函数"""
self.logger.info(f"[BacktestConsumer] 收到信号 {signum},准备退出...")
self.running = False
def simulate_order(self, strategy_name: str, data: Dict[str, Any]) -> bool:
"""模拟订单执行
Args:
strategy_name: 策略名称
data: 消息数据
Returns:
模拟成功返回 True
"""
action = data.get("action")
stock_code = data.get("stock_code")
price = data.get("price", 0)
total_slots = data.get("total_slots", 0)
self.logger.log_order_execution(
strategy_name=strategy_name,
stock_code=stock_code,
action=action,
volume=100, # 模拟数量
price=price,
)
# 模拟延迟
if self.config["simulate_delay"] > 0:
time.sleep(self.config["simulate_delay"])
# 记录模拟结果
if action == "BUY":
self.logger.info(
f"[模拟交易] {strategy_name} 买入 {stock_code} @ {price}, "
f"目标持仓: {total_slots}"
)
elif action == "SELL":
self.logger.info(
f"[模拟交易] {strategy_name} 卖出 {stock_code} @ {price}, 清仓释放槽位"
)
else:
self.logger.warning(f"[模拟交易] 未知动作: {action}")
return False
# 记录成功
self.logger.log_order_execution(
strategy_name=strategy_name,
stock_code=stock_code,
action=action,
volume=100,
price=price,
order_id=-1, # 模拟订单ID
)
return True
def process_messages(self, strategy_name: str) -> int:
"""处理单个策略的消息
Args:
strategy_name: 策略名称
Returns:
处理的消息数量
"""
processed = 0
try:
# 消费消息(回测模式,不使用消费者组)
messages = self.processor.consume_message(
strategy_name=strategy_name,
consumer_id=self.config["consumer_id"],
is_backtest=True,
block_ms=100, # 短阻塞
count=10, # 每次最多处理10条
)
if not messages:
return 0
for msg_id, data in messages:
self.stats["total_received"] += 1
try:
stream_key = f"qmt:{strategy_name}:backtest"
# 1. 记录消息接收
self.logger.log_message_receive(stream_key, msg_id, data)
# 2. 记录消息解析
self.logger.log_message_parse(
strategy_name=data.get("strategy_name", strategy_name),
stock_code=data.get("stock_code", ""),
action=data.get("action", ""),
price=data.get("price", 0),
extra_fields={
"total_slots": data.get("total_slots"),
"timestamp": data.get("timestamp"),
"is_backtest": data.get("is_backtest"),
},
)
# 3. 业务校验
self.logger.log_validation(
validation_type="field_check",
strategy_name=strategy_name,
details={"fields": list(data.keys())},
result=True,
)
# 4. 模拟订单执行
success = self.simulate_order(strategy_name, data)
if success:
self.stats["total_processed"] += 1
else:
self.stats["total_failed"] += 1
# 5. ACK 消息(回测模式不需要真正的 ACK但记录日志
self.logger.log_ack(stream_key, msg_id, True)
processed += 1
except Exception as e:
error_msg = f"消息处理异常: {str(e)}"
self.logger.error(error_msg, exc_info=True)
self.logger.log_failure(
stream_key=f"qmt:{strategy_name}:backtest",
message_id=msg_id,
error_reason=str(e),
to_failure_queue=False,
)
self.stats["total_failed"] += 1
except Exception as e:
self.logger.error(f"消费消息异常: {str(e)}", exc_info=True)
return processed
def run_once(self, specific_strategies: Optional[List[str]] = None) -> None:
"""单次运行
Args:
specific_strategies: 指定要处理的策略列表None 则处理所有
"""
strategies = specific_strategies or self.config["strategy_list"]
if not strategies:
self.logger.warning("[BacktestConsumer] 未指定策略,退出")
return
self.logger.info(f"[BacktestConsumer] 单次运行,策略: {strategies}")
total_processed = 0
for strategy in strategies:
count = self.process_messages(strategy)
total_processed += count
if count > 0:
self.logger.info(
f"[BacktestConsumer] 策略 {strategy} 处理 {count} 条消息"
)
self.logger.info(
f"[BacktestConsumer] 单次运行完成,共处理 {total_processed} 条消息"
)
self._log_stats()
def run_daemon(self, specific_strategies: Optional[List[str]] = None) -> None:
"""守护模式运行
Args:
specific_strategies: 指定要处理的策略列表None 则处理所有
"""
strategies = specific_strategies or self.config["strategy_list"]
if not strategies:
self.logger.warning("[BacktestConsumer] 未指定策略,退出")
return
self.logger.info(f"[BacktestConsumer] 守护模式启动,策略: {strategies}")
self.logger.info("按 Ctrl+C 停止")
# 设置信号处理
signal.signal(signal.SIGINT, self.signal_handler)
signal.signal(signal.SIGTERM, self.signal_handler)
last_heartbeat = time.time()
while self.running:
try:
# 处理所有策略
for strategy in strategies:
if not self.running:
break
self.process_messages(strategy)
# 心跳日志每60秒
if time.time() - last_heartbeat > 60:
self.logger.log_heartbeat(
component="BacktestConsumer",
status="running",
extra_info=self.stats,
)
last_heartbeat = time.time()
# 短暂休眠避免CPU占用过高
time.sleep(0.1)
except Exception as e:
self.logger.error(f"守护循环异常: {str(e)}", exc_info=True)
time.sleep(1)
self.logger.info("[BacktestConsumer] 守护模式停止")
self._log_stats()
def _log_stats(self) -> None:
"""记录统计信息"""
runtime = (datetime.now() - self.stats["start_time"]).total_seconds()
self.logger.info("=" * 50)
self.logger.info("[统计信息]")
self.logger.info(f"运行时间: {runtime:.1f}")
self.logger.info(f"接收消息: {self.stats['total_received']}")
self.logger.info(f"处理成功: {self.stats['total_processed']}")
self.logger.info(f"处理失败: {self.stats['total_failed']}")
self.logger.info("=" * 50)
def main():
"""主函数"""
parser = argparse.ArgumentParser(
description="QMT 回测消息消费者",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用示例:
python backtest_consumer.py # 守护模式,处理所有配置的策略
python backtest_consumer.py --once # 单次运行
python backtest_consumer.py --strategy A,B # 指定策略
python backtest_consumer.py --once --strategy A # 单次运行指定策略
""",
)
parser.add_argument(
"--once", action="store_true", help="单次运行模式(处理一次后退出)"
)
parser.add_argument(
"--strategy",
type=str,
default="",
help="指定策略名称,多个用逗号分隔(如: StrategyA,StrategyB",
)
args = parser.parse_args()
# 加载配置
config = load_config()
# 命令行参数覆盖配置
specific_strategies = None
if args.strategy:
specific_strategies = [s.strip() for s in args.strategy.split(",") if s.strip()]
# 创建消费者
consumer = BacktestConsumer(config)
# 运行
if args.once:
consumer.run_once(specific_strategies)
else:
consumer.run_daemon(specific_strategies)
if __name__ == "__main__":
main()