副标题:用QSS语法高亮+AST抽象语法树+自定义控件矩阵,打造专业级量化策略IDE
一、为什么量化策略编辑器是Qt最具挑战性的UI工程
在股票/期货量化交易系统中,策略编辑器是用户每天花费时间最长的核心模块。它不是简单的文本输入框,而是一个完整的嵌入式IDE------需要同时处理:
- 策略语法解析:将类Python/类C++的DSL转化为可执行代码
- 语法高亮与智能提示:实时代码着色、变量名补全、API函数提示
- 可视化策略构建:拖拽式生成代码,降低编程门槛
- 实时回测与绩效展示:编辑器内一键回测,结果直接在面板展示
- 策略版本管理:分支、标签、历史记录
- 风控规则配置:止损/止盈/仓位限制的可视化配置界面
业界常见的方案有两种:自研引擎 (如同花顺MindGo、聚宽)和基于开源改造(如基于Monaco Editor的Web方案)。Qt方案的优势在于:原生性能、无浏览器依赖、与Qt后端框架深度耦合。本篇聚焦Qt技术实现,搭建一套完整的策略编辑器架构。
二、整体架构设计
┌─────────────────────────────────────────────────────────┐
│ StrategyEditorWindow │
├─────────────┬───────────────────────┬───────────────────┤
│ Navigator │ CodeEditorArea │ PropertiesPanel │
│ (策略树) │ │ (属性面板) │
│ │ ┌─────────────────┐ │ │
│ · 策略A │ │ SyntaxHighlighter│ │ 策略参数配置 │
│ · 策略B │ │ · 关键词高亮 │ │ · 标的列表 │
│ · 函数库 │ │ · 函数名高亮 │ │ · 回测参数 │
│ │ │ · 字符串高亮 │ │ · 风控规则 │
│ │ │ CodeCompleter │ │ │
│ │ │ · API自动补全 │ │ VisualBuilder │
│ │ │ · 参数提示 │ │ (可视化构建器) │
│ │ └─────────────────┘ │ │
├─────────────┴───────────────────────┴───────────────────┤
│ OutputPanel (回测/日志/错误输出) │
└─────────────────────────────────────────────────────────┘
核心模块划分:
| 模块 | 技术选型 | 职责 |
|---|---|---|
| 语法高亮 | QSyntaxHighlighter 自定义 | 策略代码的关键词、函数、字符串着色 |
| 自动补全 | QCompleter + 自定义Model | API函数、变量名、策略模板的补全 |
| DSL解析器 | 手写递归下降解析器 / QRegularExpression | 策略语法 → AST抽象语法树 |
| 可视化构建 | QGraphicsView + 自定义NodeItem | 拖拽式策略流程图构建 |
| 回测引擎接口 | QProcess / QThread | 调用Python回测进程,捕获stdout输出 |
| 策略管理 | QAbstractItemModel + QTreeView | 策略树、版本管理 |
三、核心源码解析
3.1 语法高亮:多层叠加上色的实现
Qt自带的QSyntaxHighlighter基于文本块级叠加上色机制,每个文本块(通常是段落QTextBlock)可以设置独立的字符格式(QTextCharFormat)。一个完整的策略语法高亮需要覆盖以下元素:
cpp
// SyntaxHighlighter.h
#pragma once
#include <QSyntaxHighlighter>
#include <QRegularExpression>
#include <QVector>
struct HighlightingRule {
QRegularExpression pattern;
QTextCharFormat format;
};
class StrategySyntaxHighlighter : public QSyntaxHighlighter
{
Q_OBJECT
public:
explicit StrategySyntaxHighlighter(QTextDocument *parent = nullptr);
protected:
void highlightBlock(const QString &text) override;
private:
void loadStrategyKeywords(); // 加载策略语法关键词
void loadAPIFunctions(); // 加载策略API函数
void applyColorRegion(const QString& text,
const QRegularExpression& startRx,
const QRegularExpression& endRx,
const QTextCharFormat& fmt);
QVector<HighlightingRule> m_keywordRules;
QVector<HighlightingRule> m_apiFunctionRules;
QRegularExpression m_stringStartRx; // 字符串开始正则
QRegularExpression m_stringEndRx; // 字符串结束正则
QRegularExpression m_commentStartRx; // 注释开始
QRegularExpression m_numberRx; // 数字正则
QTextCharFormat m_keywordFormat; // 关键词格式(蓝色粗体)
QTextCharFormat m_functionFormat; // 函数格式(青色)
QTextCharFormat m_stringFormat; // 字符串格式(橙色)
QTextCharFormat m_commentFormat; // 注释格式(灰色斜体)
QTextCharFormat m_numberFormat; // 数字格式(紫色)
QTextCharFormat m_builtinFormat; // 内置对象格式(绿色)
QTextCharFormat m_paramFormat; // 策略参数格式(黄色背景)
};
关键实现:highlightBlock中的多规则匹配顺序至关重要------先处理注释和字符串(它们会吞掉关键词),再处理关键词:
cpp
// SyntaxHighlighter.cpp
void StrategySyntaxHighlighter::highlightBlock(const QString &text)
{
// 第一步:处理整行注释(# 开头)
QRegularExpressionMatch commentMatch = m_commentStartRx.match(text);
if (commentMatch.hasMatch()) {
int commentIndex = commentMatch.capturedStart();
setFormat(commentIndex, text.length() - commentIndex, m_commentFormat);
setCurrentBlockState(0);
return; // 注释行无需继续高亮
}
// 第二步:处理字符串(跨行状态机)
if (previousBlockState() != StringState) {
QRegularExpressionMatch startMatch = m_stringStartRx.match(text);
if (startMatch.hasMatch()) {
int start = startMatch.capturedStart();
int end = text.indexOf(m_stringEndRx, start + 1);
if (end == -1) {
setCurrentBlockState(StringState);
setFormat(start, text.length() - start, m_stringFormat);
} else {
setFormat(start, end - start + 1, m_stringFormat);
}
}
} else {
// 续上行的字符串
int end = text.indexOf(m_stringEndRx);
if (end == -1) {
setFormat(0, text.length(), m_stringFormat);
} else {
setFormat(0, end + 1, m_stringFormat);
setCurrentBlockState(0);
}
}
// 第三步:处理数字常量
QRegularExpressionMatchIterator numIt = m_numberRx.globalMatch(text);
while (numIt.hasNext()) {
QRegularExpressionMatch match = numIt.next();
setFormat(match.capturedStart(), match.capturedLength(), m_numberFormat);
}
// 第四步:处理API函数(优先级低于字符串)
for (const auto& rule : m_apiFunctionRules) {
QRegularExpressionMatchIterator it = rule.pattern.globalMatch(text);
while (it.hasNext()) {
QRegularExpressionMatch match = it.next();
// 避免覆盖已上色的字符串区域
if (format(match.capturedStart()).isEmpty())
setFormat(match.capturedStart(), match.capturedLength(), rule.format);
}
}
// 第五步:处理关键词(最高优先级,且不覆盖已有格式)
for (const auto& rule : m_keywordRules) {
QRegularExpressionMatchIterator it = rule.pattern.globalMatch(text);
while (it.hasNext()) {
QRegularExpressionMatch match = it.next();
if (format(match.capturedStart()).isEmpty())
setFormat(match.capturedStart(), match.capturedLength(), rule.format);
}
}
}
void StrategySyntaxHighlighter::loadAPIFunctions()
{
// 量化策略API函数库
QStringList apiFunctions = {
// 行情数据API
R"(\bsymbol\()", R"(\bhistory\()", R"(\bsnapshot\()",
R"(\bget_bars\()", R"(\brealtime_quote\()", R"(\bmoney_flow\()",
R"(\blimit_order\()", R"(\bmarket_order\()", R"(\bcancel_order\()",
R"(\bget_position\()", R"(\bget_orders\()", R"(\bget_trades\()",
R"(\bbuy_open\()", R"(\bsell_close\()", R"(\bbuy_close\()",
R"(\bsell_open\()", R"(\bset_stop_loss\()", R"(\bset_take_profit\()",
R"(\bcontext\.now\()", R"(\bcontext\.portfolio\()", R"(\bcontext\.account\()",
R"(\blog_info\()", R"(\blog_warning\()", R"(\bplot\()"
};
for (const QString& func : apiFunctions) {
HighlightingRule rule;
rule.pattern = QRegularExpression(func);
rule.format = m_functionFormat;
m_apiFunctionRules.append(rule);
}
}
3.2 自动补全:QCompleter与自定义模型
QCompleter默认使用QFileSystemModel,需要扩展为QAbstractListModel提供策略相关的补全:
cpp
// StrategyCompleterModel.h
class StrategyCompleterModel : public QAbstractListModel
{
Q_OBJECT
public:
enum CompletionRole {
CompletionTextRole = Qt::UserRole + 1,
CompletionDetailRole,
CompletionCategoryRole // 分类:keyword/api/variable/template
};
explicit StrategyCompleterModel(QObject *parent = nullptr);
void setContext(const QString& code, int cursorPosition);
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
private:
void buildCompletions();
void addAPICompletions();
void addVariableCompletions();
void addTemplateCompletions();
void scoreAndSort();
struct CompletionItem {
QString text;
QString detail;
QString category;
int score; // 匹配得分
};
QVector<CompletionItem> m_items;
QString m_currentWord;
QString m_codeContext;
};
// StrategyCompleterModel.cpp
void StrategyCompleterModel::setContext(const QString& code, int cursorPosition)
{
beginResetModel();
m_codeContext = code;
// 提取当前光标前的单词作为补全前缀
int wordStart = cursorPosition - 1;
while (wordStart >= 0 && code[wordStart].isLetterOrNumber()) {
--wordStart;
}
m_currentWord = code.mid(wordStart + 1, cursorPosition - wordStart - 1);
buildCompletions();
scoreAndSort();
endResetModel();
}
QVariant StrategyCompleterModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= m_items.size())
return QVariant();
const CompletionItem &item = m_items[index.row()];
switch (role) {
case Qt::DisplayRole:
return item.text;
case CompletionDetailRole:
return item.detail;
case CompletionCategoryRole:
return item.category;
case Qt::ForegroundRole: {
if (item.category == "keyword")
return QColor("#569cd6"); // VS Code蓝色
if (item.category == "api")
return QColor("#dcdcaa"); // 函数名黄色
if (item.category == "variable")
return QColor("#9cdcfe"); // 变量浅蓝
return QColor("#ce9178"); // 模板橙色
}
default:
return QVariant();
}
}
// 使用方式
StrategySyntaxHighlighter *highlighter = new StrategySyntaxHighlighter(ui->codeEditor->document());
StrategyCompleterModel *completerModel = new StrategyCompleterModel(this);
QCompleter *completer = new QCompleter(completerModel, this);
completer->setWidget(ui->codeEditor);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setFilterMode(Qt::MatchContains); // 模糊匹配
completer->setMaxVisibleItems(8);
completer->setCompletionMode(QCompleter::PopupCompletion);
connect(ui->codeEditor, &QPlainTextEdit::textChanged,
this, [this, completer]() {
// 文本变化时更新补全上下文(带防抖)
});
connect(completer, QOverload<const QString &>::of(&QCompleter::activated),
this, [this](const QString &completion) {
// 插入补全,处理参数占位符
if (completion.contains("(")) {
// 自动插入括号和参数占位符
ui->codeEditor->insertPlainText(
completion + "{{}}"); // {{}}作为参数占位符
}
});
3.3 DSL解析器:策略语法的AST构建
策略编辑器需要理解代码结构才能做"变量提取"、"函数导航"、"错误标注"。这需要构建一个抽象语法树(AST):
cpp
// StrategyAST.h
struct ASTNode {
enum class Type { Module, Assignment, IfStatement, WhileStatement,
FunctionDef, FunctionCall, Comparison, BinaryOp,
UnaryOp, Literal, Variable, Subscript, Attribute };
Type type;
int line;
int column;
QVariant value; // 具体值(用于Literal等)
QVector<ASTNode*> children;
QString toDebugString(int indent = 0) const;
};
class StrategyParser {
public:
explicit StrategyParser(const QString& code);
ASTNode* parse();
// 错误报告
struct ParseError { int line; int column; QString message; };
QVector<ParseError> errors() const { return m_errors; }
// AST查询接口
QVector<ASTNode*> findVariables() const;
QVector<ASTNode*> findFunctionDefs() const;
QVector<ASTNode*> findFunctionCalls(const QString& funcName) const;
ASTNode* findNodeAt(int line, int column) const;
private:
// 递归下降解析器各方法
ASTNode* parseModule();
ASTNode* parseStatement();
ASTNode* parseAssignment();
ASTNode* parseIfStatement();
ASTNode* parseExpression();
ASTNode* parseComparison();
ASTNode* parseAdditive();
ASTNode* parseMultiplicative();
ASTNode* parseUnary();
ASTNode* parsePrimary();
ASTNode* parseFunctionDef();
ASTNode* parseFunctionCall(const QString& funcName);
// 词法分析
struct Token {
enum class Type { Identifier, Number, String, Keyword,
Operator, LeftParen, RightParen, Colon,
Newline, Eof };
Type type;
QString text;
int line;
int column;
};
QVector<Token> tokenize(const QString& code);
Token peek();
Token consume();
bool match(Token::Type type);
const QString m_code;
QVector<Token> m_tokens;
int m_pos = 0;
QVector<ParseError> m_errors;
};
DSL示例(类Python语法):
# 双均线策略 v2.1
@strategy(name="双均线策略", author="Trader", version="2.1")
class DoubleMaStrategy:
# 策略参数
fast_period: int = 5 # 快速均线周期
slow_period: int = 20 # 慢速均线周期
trade_volume: int = 100 # 单笔交易量
def initialize(self, context):
context.add_symbol("600519") # 贵州茅台
context.set_frequency("1d") # 日线
def handle_data(self, context, data):
ma5 = data.ma(self.symbol, self.fast_period)
ma20 = data.ma(self.symbol, self.slow_period)
if ma5 > ma20 and not self.has_position(context):
context.buy_open(self.symbol, self.trade_volume)
elif ma5 < ma20 and self.has_position(context):
context.sell_close(self.symbol)
解析器实现关键------Token定义和递归下降核心:
cpp
// StrategyParser.cpp
ASTNode* StrategyParser::parseFunctionCall(const QString& funcName)
{
// context.buy_open(symbol, volume) 解析为 FunctionCall 节点
auto *node = new ASTNode;
node->type = ASTNode::Type::FunctionCall;
if (match(Token::Type::Identifier)) {
// 继续解析: symbol, volume)
while (!match(Token::Type::RightParen)) {
node->children.append(parseExpression());
if (!match(Token::Type::Comma))
break;
consume();
}
}
return node;
}
QVector<ASTNode*> StrategyParser::findFunctionCalls(const QString& funcName) const
{
QVector<ASTNode*> results;
std::function<void(ASTNode*)> traverse = [&](ASTNode* node) {
if (!node) return;
if (node->type == ASTNode::Type::FunctionCall
&& node->value.toString() == funcName)
results.append(node);
for (ASTNode* child : node->children)
traverse(child);
};
ASTNode* root = m_ast.get(); // 假设parser保留AST指针
traverse(root);
return results;
}
有了AST,可以实现边栏策略树导航 (显示函数定义列表)、实时错误标注 (QMetaObject::invokeMethod驱动语法检查)、变量作用域分析等功能。
3.4 可视化策略构建:QGraphicsView节点编辑器
对于非程序员用户,可视化构建器是刚需。基于QGraphicsView实现节点编辑器:
cpp
// NodeItem.h
class NodeItem : public QGraphicsObject
{
Q_OBJECT
public:
enum class NodeType { Signal, Indicator, Condition, Action, Output };
NodeItem(NodeType type, const QString& title, QGraphicsItem *parent = nullptr);
// 端口定义
void addInputPort(const QString& name, const QString& dataType);
void addOutputPort(const QString& name, const QString& dataType);
QRectF boundingRect() const override;
void paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
// 端口连接
struct Port {
QString name;
QString dataType;
bool isInput;
QPointF scenePosition() const;
};
QVector<Port> inputPorts() const { return m_inputs; }
QVector<Port> outputPorts() const { return m_outputs; }
signals:
void nodeUpdated(NodeItem*);
void connectionRequested(NodeItem* from, int outIdx,
NodeItem* to, int inIdx);
protected:
QPainterPath shape() const override;
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
private:
NodeType m_nodeType;
QString m_title;
QVector<Port> m_inputs;
QVector<Port> m_outputs;
QRectF m_contentRect;
// 节点样式(类VS Code节点编辑器)
static const QColor COLOR_SIGNAL; // 蓝色 = 数据源
static const QColor COLOR_INDICATOR; // 紫色 = 指标计算
static const QColor COLOR_CONDITION;// 黄色 = 条件判断
static const QColor COLOR_ACTION; // 绿色 = 交易操作
};
节点连接线使用贝塞尔曲线绘制:
cpp
// ConnectionLine.cpp
class ConnectionLine : public QGraphicsItem
{
public:
ConnectionLine(QPointF start, QPointF end, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent), m_start(start), m_end(end) {}
QPainterPath shape() const override {
QPainterPath p;
p.moveTo(m_start);
// 三次贝塞尔曲线,自动绕过中间节点
QPointF ctrl1(m_start.x() + qAbs(m_end.x() - m_start.x()) * 0.5, m_start.y());
QPointF ctrl2(m_end.x() - qAbs(m_end.x() - m_start.x()) * 0.5, m_end.y());
p.cubicTo(ctrl1, ctrl2, m_end);
return p;
}
void paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override {
QPen pen(QColor("#569cd6"), 2.0);
pen.setStyle(Qt::SolidLine);
painter->setPen(pen);
painter->drawPath(shape());
// 箭头指示方向
QPainterPath arrowPath;
arrowPath.moveTo(m_end);
arrowPath.lineTo(m_end + QPointF(-6, -3));
arrowPath.lineTo(m_end + QPointF(-6, 3));
arrowPath.closeSubpath();
painter->fillPath(arrowPath, QColor("#569cd6"));
}
private:
QPointF m_start, m_end;
};
节点到代码的生成:可视化构建完成后,需要从节点图反向生成策略代码字符串:
cpp
// NodeToCodeGenerator.cpp
QString NodeToCodeGenerator::generate(NodeScene *scene)
{
QString code;
QTextStream out(&code);
out << "# 自动生成策略代码\n";
out << "# 警告: 请勿直接修改此文件\n\n";
out << "@strategy(name=\"自动生成策略\")\n";
out << "class AutoStrategy:\n\n";
// 遍历所有节点,按拓扑排序确定执行顺序
QVector<NodeItem*> sorted = topologicalSort(scene->nodes());
for (NodeItem* node : sorted) {
switch (node->nodeType()) {
case NodeItem::NodeType::Signal:
out << " # 信号节点: " << node->title() << "\n";
out << " def get_signal(self, data):\n";
out << generateSignalCode(node) << "\n";
break;
case NodeItem::NodeType::Indicator:
out << " # 指标节点: " << node->title() << "\n";
out << generateIndicatorCode(node) << "\n";
break;
case NodeItem::NodeType::Action:
out << " # 交易操作: " << node->title() << "\n";
out << generateActionCode(node) << "\n";
break;
}
}
return code;
}
3.5 回测结果实时展示面板
回测引擎通常以独立Python子进程运行(QProcess),通过stdout输出JSON格式的中间结果:
cpp
// BacktestRunner.cpp
class BacktestRunner : public QObject
{
Q_OBJECT
public:
void runBacktest(const QString& strategyCode,
const BacktestConfig& config);
signals:
void progressUpdated(int percent, const QString& status);
void equityCurveReady(const QJsonArray& equityData);
void tradesReady(const QJsonArray& trades);
void metricsReady(const QJsonObject& metrics);
void errorOccurred(const QString& error);
private slots:
void onProcessOutput();
void onProcessError();
private:
QProcess *m_process;
QJsonDocument m_buffer; // 粘包处理
void parseLine(const QString& line);
};
void BacktestRunner::parseLine(const QString& line)
{
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(line.toUtf8(), &err);
if (err.error != QJsonParseError::NoError) {
qWarning() << "JSON parse error:" << err.errorString();
return;
}
QJsonObject obj = doc.object();
QString type = obj["type"].toString();
if (type == "progress") {
int percent = obj["percent"].toInt();
QString status = obj["status"].toString();
Q_EMIT progressUpdated(percent, status);
}
else if (type == "equity") {
Q_EMIT equityCurveReady(obj["data"].toArray());
}
else if (type == "trade") {
Q_EMIT tradesReady(obj["data"].toArray());
}
else if (type == "metrics") {
Q_EMIT metricsReady(obj["data"].toObject());
}
}
void BacktestRunner::onProcessOutput()
{
// QProcess有粘包问题,需要按行分割 + JSON边界对齐
while (m_process->canReadLine()) {
QString line = QString::fromUtf8(m_process->readLine()).trimmed();
if (line.isEmpty()) continue;
parseLine(line);
}
}
回测面板使用QCustomPlot绘制资金曲线(这正好与已有博客的QCustomPlot系列形成互补------这里强调的是策略编辑器内嵌的回测展示,而非QCustomPlot本身的技术原理):
cpp
// EquityCurveWidget.cpp
class EquityCurveWidget : public QWidget
{
Q_OBJECT
public:
explicit EquityCurveWidget(QWidget *parent = nullptr);
public slots:
void setEquityData(const QJsonArray& data);
void setTrades(const QJsonArray& trades);
private:
QCustomPlot *m_plot;
void drawEquityCurve(const QVector<double>& dates,
const QVector<double>& equity);
void overlayTradeMarkers(const QVector<double>& dates,
const QVector<double>& equity,
const QVector<bool>& isBuy);
};
void EquityCurveWidget::drawEquityCurve(
const QVector<double>& dates,
const QVector<double>& equity)
{
m_plot->clearGraphs();
// 主曲线:资金曲线
QCPGraph *equityGraph = m_plot->addGraph();
equityGraph->setData(dates, equity);
equityGraph->setPen(QPen(QColor("#569cd6"), 2));
equityGraph->setBrush(QBrush(QColor("#569cd6", 30)));
// 绘制买入/卖出标记(绿三角/红三角)
for (int i = 0; i < m_trades.size(); ++i) {
const Trade& t = m_trades[i];
QCPItemTracer *marker = new QCPItemTracer(m_plot);
marker->setGraph(equityGraph);
marker->setData(t.timestamp, t.equity);
marker->setStyle(QCPItemTracer::tsCircle);
marker->setPen(QPen(t.direction == Trade::Buy
? QColor("#50fa7b") : QColor("#ff5555")));
marker->setBrush(Qt::white);
}
m_plot->rescaleAxes();
m_plot->replot();
}
四、架构设计总结
量化策略编辑器的技术挑战本质上是IDE构建的挑战------Qt提供了足够强大的基础组件,但需要精心编排:
- 语法高亮层 :
QSyntaxHighlighter的多规则叠加上色是基础,关键是处理注释/字符串与关键词的优先级关系 - 语义理解层:DSL解析器构建AST,提供变量/函数/调用的索引,是IDE功能(导航、补全、错误标注)的数据源
- 可视化构建层 :
QGraphicsView实现节点编辑器,贝塞尔曲线连接线,拓扑排序实现代码生成 - 执行回测层 :
QProcess管理Python子进程,stdout流式JSON传输,本地QCustomPlot实时渲染曲线
这四个层次各有侧重,却通过统一的数据流串联:用户在编辑器/可视化界面中操作 → AST/节点图存储 → 代码生成器输出策略代码 → QProcess驱动Python回测引擎 → stdout回传结果 → UI实时展示。理解这条数据流,就理解了整个系统的运行逻辑。
《注:若有发现问题欢迎大家提出来纠正》
以上仅为技术分享参考,不构成投资建议