股票组合融资杠杆管理策略

本策略以外部生成的每日目标权重为输入,在每个交易日计算目标持仓,并通过 DolphinDB Backtest 引擎提交调仓订单。目标权重本身如何生成不属于本策略的实现范围,因此回测结果只能评价给定权重在指定执行规则下的表现,不能单独证明权重模型有效。

1. 策略介绍

本策略不在回测过程中选股或计算因子,而是读取已经生成的权重文件。每个交易日,策略以账户总权益为基数,将当日权重换算为目标市值和目标股数,再按照先卖后买的顺序调整组合。

1.1 核心思路

设交易日为 t,账户总权益为 E_t,股票 i 的目标权重为 w_{i,t},手续费率为 c,印花税率为 s。按下式计算目标市值:

其中,1-2c-s 为预留的一次买入手续费、一次卖出手续费和一次卖出印花税。随后使用当日开盘价将目标市值换算为目标股数。普通股票的目标股数向下取整到 100 股的整数倍;代码以 68 开头的股票识别科创板标的,首次买入数量不足 200 股时不下单,达到 200 股后允许按 1 股递增。

这是一种目标权重跟踪策略。权重文件负责表达“希望持有什么”,回测脚本负责把目标权重转换为订单,并受价格、成交量、涨跌停状态和可用现金约束。

1.2 交易标的

配套行情文件包含 .SH 后缀的上交所股票代码,行情频率为日频。策略使用普通股票账户,只建立或减少多头持仓,不包含卖空、融资或融券逻辑。

行情文件覆盖 2025-01-02 至 2025-06-30,共 117 个交易日、232,196 行记录和 1,998 个唯一标的。权重文件覆盖相同的 117 个交易日,共 32,871 行记录和 283 个唯一标的。每个交易日有 259 至 283 个正权重标的,日权重之和在浮点误差范围内等于 1。

这些文件用于演示回测流程。文档没有权重模型的生成逻辑、历史可用时点或基准信息,因此不能把权重解释为某个指数、因子组合或投资建议。用于真实研究时,必须确认交易日 t 的权重在当日开盘前已经可用,否则会产生未来数据问题。

1.3 交易规则

策略在每个有目标权重的交易日按以下顺序运行:

  1. 从回测引擎读取当前账户总权益、可用现金和各股票多头持仓。

  2. 根据当日权重、交易成本预留和当日开盘价计算目标股数。

  3. 对目标股数低于当前持仓的股票先提交卖出平仓订单。

  4. 对目标股数高于当前持仓的股票再提交开仓买入订单。

  5. 买入数量取目标差额与可用现金可承担数量的较小值,并应用普通股票整手和科创板最低申报数量规则。

  6. 成交量不大于 0,或收盘价等于相应涨跌停价时,脚本跳过订单。

2. 策略回测

以下按照准备数据、定义策略、设置参数、创建引擎、回放行情和读取结果的顺序展开。本案例已在 DolphinDB Server 3.00.6.1、Backtest 3.00.6.6 与模拟撮合引擎插件(Matching Engine Simulator) 3.00.6.6 上运行。服务器需要安装与 DolphinDB Server 版本匹配的两个插件;创建引擎前,代码会检查插件是否已安装,并加载这两个插件。

2.1 准备数据

案例使用 portfolioData.csv 作为日频行情输入,使用 weight.csv 作为每日目标权重输入。运行脚本前,应将相对路径改为 DolphinDB Server 进程实际可访问的路径。CSV 文件本身不保存 DolphinDB 的 TIMESTAMP、LONG 或数组向量类型信息,因此行情文件通过显式 schema 加载;权重文件由 loadText 推断类型,加载后应检查并统一 tradeDay 的日期类型。

表 1. 表 2-1 输入文件概览
文件 用途 数据范围 行数 唯一标的数
portfolioData.csv 日频行情与扩展信号 2025-01-02 至 2025-06-30,共 117 个交易日 232,196 1,998
weight.csv 每日目标权重 32,871 283
表 2. 表 2-2 日频行情表结构
列名 DolphinDB 类型 说明
symbol SYMBOL 股票代码,带 .SH 后缀
tradeTime TIMESTAMP 行情所属交易日的零点时间戳
open DOUBLE 开盘价,单位为元/股;策略用作目标数量计算和委托价格
low DOUBLE 当日最低价,单位为元/股
high DOUBLE 当日最高价,单位为元/股
close DOUBLE 收盘价,单位为元/股;当前脚本还用它判断涨跌停
volume LONG 成交量;不大于 0 时不提交订单
amount DOUBLE 成交金额,单位为元
upLimitPrice DOUBLE 当日涨停价,单位为元/股
downLimitPrice DOUBLE 当日跌停价,单位为元/股
prevClosePrice DOUBLE 前收盘价,单位为元/股
signal DOUBLE[] 扩展信号数组;本策略逻辑没有读取该字段
表 3. 表 2-3 目标权重表结构
列名 DolphinDB 类型 说明
tradeDay DATE 目标权重生效日;CSV 中保存为带纳秒部分的时间文本,加载后应检查并转换为 DATE
symbol SYMBOL 股票代码,带 .SH 后缀
weight DOUBLE 目标权重,取值大于 0;同一交易日的权重之和约为 1

目标脚本使用以下代码加载两份数据,并把权重表转换为以 DATE 为键的字典:

path = "/.../"

w = loadText(path + "weight.csv") 
w = select * from w where weight > 0. order by tradeDay
dailyWeight = dict(DATE, ANY, true)
for (idate in sort(distinct(w.tradeDay))) {
    s = select symbol, weight from w where tradeDay = idate
    dailyWeight[idate] = s
}
​
name = ["symbol", "tradeTime", "open", "low", "high", "close", "volume",
        "amount", "upLimitPrice", "downLimitPrice", "prevClosePrice", "signal"]
type = ["STRING", "TIMESTAMP", "DOUBLE", "DOUBLE", "DOUBLE", "DOUBLE", "LONG",
        "DOUBLE", "DOUBLE", "DOUBLE", "DOUBLE", "DOUBLE[]"]
dailyData = loadText(path + "portfolioData.csv", schema=table(name as name, type as type))

2.2 编写策略逻辑

策略由 5 个辅助函数和一个 onBar 回调组成。辅助函数负责交易条件检查、数量取整和开盘价读取;onBar 负责把当日目标权重转换为目标头寸并提交订单。

交易条件函数:isBuyAble 与 isSellAble

isBuyAble(data) 和 isSellAble(data) 接收单只股票的日频行情记录,返回 BOOL 标量。前者在成交量不大于 0 或收盘价等于涨停价时返回 false,后者在成交量不大于 0 或收盘价等于跌停价时返回 false。函数不修改账户状态。

def isBuyAble(data) {
    if (data.volume <= 0.001 or data.upLimitPrice == data.close) {
        return false
    }
    return true
}
​
def isSellAble(data) {
    if (data.volume <= 0.001 or data.downLimitPrice == data.close) {
        return false
    }
    return true
}

数量处理函数:checkBuyVolume 与 getBuyAbleVolume

  • checkBuyVolume(istock, num) 将目标持仓差额转换为符合脚本规则的买入数量。

  • getBuyAbleVolume(istock, cash, price) 根据可用现金和价格计算最多可买数量。

两者都返回非负数量;普通股票按 100 股向下取整,68 开头的股票要求首次数量至少为 200 股。

def checkBuyVolume(istock, num) {
    if (num <= 0) { return 0 }
    if (istock.substr(0, 2) == "68" and num >= 200) {
        return num
    } else if (istock.substr(0, 2) == "68") {
        return 0
    }
    return floor(int(num) / 100) * 100
}
​
def getBuyAbleVolume(istock, cash, price) {
    if (price <= 0.) { return 0 }
    if (istock.substr(0, 2) == "68" and floor(cash / price) >= 200) {
        return floor(cash / price)
    } else if (istock.substr(0, 2) == "68") {
        return 0
    }
    return floor(floor(cash / price) / 100) * 100
}

开盘价读取函数:getOpenPrice

getOpenPrice(msg, istock) 从当前交易日的行情字典中读取指定股票的开盘价。函数返回 open 字段对应的 DOUBLE 标量,不修改行情、权重表或账户状态。该函数在 onBar 计算目标持仓之前调用。

def getOpenPrice(msg, istock) {
    return msg[istock]["open"]
}

日频回调函数:onBar

onBar(mutable context, msg, indicator) 在日频行情进入引擎时调用。

  • context 是可变 DICTIONARY,包含引擎句柄、当前交易日期和时间,以及脚本写入的权重字典和成本参数。

  • msg 是以股票代码为键、以当日行情记录为值的 DICTIONARY。

  • indicator 是引擎预留的指标结果参数,本策略没有读取它。

函数没有显式返回值,主要副作用是向引擎提交订单。执行顺序为读取当日权重、获取权益与持仓、计算目标持仓、先提交卖单,再根据可用现金提交买单。

def onBar(mutable context, msg, indicator) {
    weightsToday = context["targetWeight"][date(context["tradeDate"])]
    idate = date(context["tradeDate"])
    if (typestr(weightsToday) == "VOID") { return }
​
    totalEquity = Backtest::getTotalPortfolios(context["engine"]).totalEquity[0]
    pos = select symbol, longPosition from Backtest::getPosition(context["engine"])
    pos = dict(pos.symbol, pos.longPosition)
​
    update weightsToday
        set targetMv = totalEquity *
            (1 - 2 * context["commission"] - context["tax"]) * weight
    update weightsToday set open = each(getOpenPrice{msg}, symbol)
    update weightsToday
        set targetPos = iif(open <= 0., 0,
            iif(symbol.substr(0, 2) == "68" and floor(targetMv / open) >= 200,
                floor(targetMv / open),
                iif(symbol.substr(0, 2) == "68", 0,
                    floor(floor(targetMv / open) / 100) * 100)))
    update weightsToday set Pos = nullFill(pos[symbol], 0)
​
    sell = select symbol, Pos - targetPos as num
        from weightsToday where targetPos - Pos < 0
    sell = dict(sell.symbol, sell.num)
    for (istock in sell.keys()) {
        data = msg[istock]
        num = sell[istock]
        if (isSellAble(data) and num > 0) {
            Backtest::submitOrder(
                context["engine"],
                (istock, context["tradeTime"], 5, msg[istock].open, num, 3),
                "卖出平仓")
        }
    }
​
    buy = select symbol, targetPos - Pos as num
        from weightsToday where targetPos - Pos > 0
    buy = dict(buy.symbol, buy.num)
    for (istock in buy.keys()) {
        data = msg[istock]
        num = buy[istock]
        cash = Backtest::getAvailableCash(context["engine"])
        price = data.open
        qty = getBuyAbleVolume(istock, cash * (1 - context["commission"]), price)
        num = min(checkBuyVolume(istock, num), qty)
        if (isBuyAble(data) and num > 0) {
            Backtest::submitOrder(
                context["engine"],
                (istock, context["tradeTime"], 5, price, int(num), 1),
                "开仓买入")
        }
    }
}

2.3 设置回测参数

脚本使用 config 配置回测区间、账户、成本、行情和撮合模式,并通过 context 向回调传递每日权重和成本参数。

config = {
    startDate: 2025.01.01,
    endDate: 2025.07.01,
    strategyGroup: "stock",
    cash: 100000000,
    commission: 0.0005,
    tax: 0.001,
    dataType: 4,
    matchingMode: 3,
    outputOrderInfo: true
}
​
context = {
    targetWeight: dailyWeight,
    commission: config["commission"],
    tax: config["tax"]
}
config["context"] = context
表 4. 表 2-4 回测参数说明
参数 类型 取值 说明
startDate DATE 2025-01-01 回测区间起点;配套行情从 2025-01-02 开始
endDate DATE 2025-07-01 回测区间终点;配套行情截至 2025-06-30
strategyGroup STRING stock 使用普通股票策略模式
cash INT 100,000,000 初始资金,单位为元
commission DOUBLE 0.0005 单边手续费率 0.05%
tax DOUBLE 0.001 卖出印花税率 0.1%
dataType INT 4 使用日频行情
matchingMode INT 3 按委托价格成交;本策略以 open 作为委托价格
outputOrderInfo BOOL true 输出订单状态信息
context.targetWeight DICTIONARY dailyWeight 按 DATE 索引每日目标权重表
context.commission DOUBLE 0.0005 回调函数计算目标市值和可买数量时使用的手续费率
context.tax DOUBLE 0.001 回调函数预留卖出印花税时使用的税率

commission 和 tax 同时保存在 config 与 context 中。前者供回测引擎计算实际交易费用,后者供策略回调计算目标市值和可买数量;调整成本参数时必须保持两处一致。

2.4 创建回测引擎

脚本先加载 Matching Engine Simulator 和 Backtest 插件,再将 onBar 注册为日线回调。loadPlugin 只加载已经安装的插件,不会自动安装缺失插件。

try { loadPlugin("MatchingEngineSimulator") } catch(ex) { print ex }
try { loadPlugin("Backtest") } catch(ex) { print ex }
go
​
strategyName = "Backtest_test1"
callbacks = dict(STRING, ANY)
callbacks["onBar"] = onBar
​
try { Backtest::dropBacktestEngine(strategyName) } catch(ex) { print ex }
engine = Backtest::createBacktester(strategyName, config, callbacks,false)

脚本将 createBacktester 的第四个参数显式设置为 false,以非 JIT 模式运行。本案例的实际结果也在该模式下取得。

2.5 执行回测

行情表传入 Backtest::appendQuotationMsg 后,引擎按交易日触发 onBar。全部行情处理完成后,脚本调用 Backtest::appendEndMarker 结束回测并触发引擎收尾处理。

Backtest::appendQuotationMsg(engine, dailyData)
Backtest::appendEndMarker(engine)

只有在结束标记处理完成后才能读取完整结果。重复运行前应删除旧引擎并重新创建,避免复用已有账户状态。

2.6 获取回测结果

目标脚本查询交易明细、每日持仓、每日账户权益和收益概述:

tradeDetails = Backtest::getTradeDetails(engine)
dailyPosition = Backtest::getDailyPosition(engine)
dailyPortfolios = Backtest::getDailyTotalPortfolios(engine)
returnSummary = Backtest::getReturnSummary(engine)
  • tradeDetails 同时包含订单的不同状态行,不能把每一行都计为一笔成交。分析时应根据 orderId 去重订单,并结合 orderStatus、tradeQty 和未完成数量区分订单与实际成交。

  • dailyPosition 用于检查每日持仓。

  • dailyPortfolios 用于分析账户权益、净值和费用。

  • returnSummary 用于汇总收益和风险。

脚本没有设置真实基准,因此不能把基准字段解释为某个指数,也不能据此宣称获得超额收益。

3. 回测结果分析

本策略已使用配套 CSV 在 DolphinDB Server 3.00.6.1、Backtest 3.00.6.6 和 Matching Engine Simulator 3.00.6.6 环境中完整执行。回测覆盖 2025-01-02 至 2025-06-30,共处理 117 个交易日和 232,196 行行情,初始资金为 100,000,000 元。

表 5. 表 3-1 收益与账户结果
指标 实际结果
期末总权益 93,257,742.76 元
期末现金 4,579,092.16 元
期末持仓市值 88,678,650.60 元
区间总收益率 -6.7423%
年化收益率 -13.9589%
年化波动率 16.7040%
夏普比率 -0.8357
最大回撤 11.1195%
日胜率 49.5726%
累计交易费用 6,317,065.36 元
期末持仓股票数 241
期末持仓股数 9,604,763 股

初始资金为 1 亿元,期末总权益为 9,325.77 万元,区间亏损 674.23 万元。累计交易金额约为 63.62 亿元,交易费用达到 631.71 万元,是影响结果的重要因素;这与策略按日再平衡、订单数量较多的特征一致。收益概述没有配置真实基准,返回的基准收益、Alpha 和 Beta 不具备指数比较含义。

最后一个交易日共有 259 个目标权重标的,其中只有 223 个具有当日行情。期末实际持有 241 只股票,其中 21 只已经不在最后一日目标集合中,市值合计为 8,140,140.07 元,约占期末总权益的 8.73%。这说明当前调仓逻辑只处理当日权重表内的股票,未对退出目标集合的历史持仓统一清仓,实际组合与目标权重存在偏离。

以上结果基于案例 CSV 和给定执行规则,只用于验证策略实现与回测流程。输入权重的生成方法和历史可用时点没有提供,且开盘委托的交易条件使用了同一交易日的收盘信息,因此不能把本次收益解释为真实可交易策略的历史表现,也不能据此判断权重模型有效或无效。

4. 总结

本案例展示了如何把每日目标权重转换为日频股票组合的目标持仓,并通过 DolphinDB Backtest 引擎按先卖后买的顺序提交调仓订单。脚本包含成本预留、普通股票整手、科创板最低申报数量、停牌和涨跌停过滤以及可用现金约束,适合说明权重驱动型组合回测的基本结构。

在配套数据上,策略完成了 117 个交易日和 232,196 行行情的处理,产生 27,800 个订单,期末总权益为 93,257,742.76 元,区间收益率为 -6.7423%。实际结果同时表明,按日调仓产生的交易费用较高,缺失行情和退出目标集合后未清仓会造成持仓偏离。用于真实投资研究前,应明确权重可用时点,统一行情覆盖范围,并完善目标集合退出、未成交和交易时点处理。