[原创] AI 时代,量化人真正必备的能力,已经不是写代码

从一张差点爆仓的网格,到一次与 AI 共同完成的策略重写

一张跑在韩国股指上的网格,在真实行情里露出四根刺。追着这四根刺往下挖,最后挖到的不是参数,而是一整套推翻重来的数学。而全程真正稀缺的,不是"会解题",是"提对问题"。

  • 标的:EWY 韩国股指(合约)
  • 形态:动态移动网格,10 倍杠杆
  • 终局:通用网格策略 · 数学自适应版 v4.0

图 1:左侧的固定网格在趋势中不断积累风险,右侧则是由递归估计与风控闸门支撑的自适应结构。


00 · 缘起:一张网格,四根刺

故事从一个主观判断开始。我看好韩国市场的一段趋势,于是把一张通用网格挂上了 EWY——区间内低买高卖,价格突破就往有利方向平移跟随。逻辑很干净,回测也好看。

但真金白银跑起来,四个问题一根根扎出来。下面这四段,是我在盘面前实实在在的感受:

刺一 · 低效。 格距和每格下单金额都是写死的固定值。可股票不是 7×24 均匀波动的——亚盘收盘到美盘开盘那一段,价格几乎不动,波动率低得可怜。这时候网格还用着和剧烈时段一样的大区间、一样的下单额,格子稀稀拉拉挂在那儿半天不成交,钱就那么躺着。当然,一旦有突发事件,它又会突然大幅波动。问题就在于:低波动的时候,还在用固定的大区间下单,效率低得刺眼。

刺二 · 爆仓。 这是最要命的。一旦行情进入持续下跌,做多的网格会不停地在更低的格子接多单——越跌越买、越买越多,而且没有任何止损。10 倍杠杆之下,这就是典型的"接飞刀",仓位越堆越重,迟早爆仓。策略里虽然能显示清算价,但它只是显示,从不干预。

刺三 · 收益回吐,还干等浪费时间。 网格在震荡里一点点攒出来的收益,看着挺美。可只要来一波像样的下跌,账户收益几乎全部吐回去。更难受的是接下来——我抱着一堆套牢的多单,只能死等价格回归,才能把它们各自止盈。这一等就是大段时间,资金卡在里面动弹不得,机会成本白白流失。

刺四 · 高位死库存。 那些在高位开的单子,其实是靠底部反复的震荡收益,一点一点把它们的亏损"磨平"的。这个过程又慢又被动。我想要的是:网格的最高点能够灵活下调,别一直死扛着那堆高位的货。

这四根刺,看起来是四个毛病。但往下挖,它们是同一根。

图 2:震荡阶段的小额收益不断积累;一旦进入单边下跌,逆势库存的风险会以更快速度放大。


01 · 诊断:四个症状,一个病根

用希腊字母说最清楚。网格的本质是做空波动率(short gamma):它靠价格来回震荡吃钱,赌的是均值回归。而做多网格在下跌里不断接飞刀,净持仓(delta)越堆越大——它对趋势毫无防御。

把盈亏拆开看,结构是这样的:

网格盈亏  ≈  ( 震荡收割 ∝ σ²/d )  −  ( 库存 × 趋势漂移 μ )
             └── 永远为正,吃波动 ──┘   └── 下跌时对多头是纯亏损 ──┘
  • 震荡项:价格穿越格距 d 越频繁、波动 σ 越大,收割越多,永远为正。
  • 趋势项库存 × μ。在下跌里 μ 长期为负,而库存又在不断变大,这一项会发散,最终吞掉一切。

真正的病根:这是一个"做空波动"的策略,却没有任何风险控制。

更糟的是,传统网格"只往有利方向移动"的设计,其实和网格初衷是相反的:价格涨破上沿时它上移、在更高的价位挂新多单(追高买入);价格跌破下沿时它不动、不撤单、无止损(死扛接飞刀)。这等于在放大那个趋势项。

回头看那四根刺——低效、爆仓、回吐、死库存——全是同一个 库存 × μ 在不同阶段的表情。承认这一点,方向就清楚了:不能只调参数,得给这张裸奔的短 gamma 网格,补上一套会思考的骨架。


02 · 会呼吸的网格

先解最直观的刺一。区间宽度不该是价格的固定百分比,而应该跟着波动结构走:低波动时自动收窄、密集收割小波动;剧烈时自动张开、不被一根长针扫穿。像肺一样一呼一吸。

图 3:左侧波动较低,格距与区间同步收紧;右侧波动放大,网格随均衡幅度向外扩张。


03 · 方向的迷雾:毫秒是噪声,漂移才是敌人

刺二、刺三的核心,是方向判断

资本市场毫秒级的跳动看着吓人,但那恰恰是网格要收割的噪声——你不需要预测它,也预测不了。真正要估的,是那个藏在噪声底下的慢变量:漂移 μ。它的时间尺度是分钟到小时,不是毫秒。

把高频跳动当成方向信号去频繁止损,是所有网格翻车的认知根源——用噪声做决策,必然一边浪费收益(频繁止损的手续费和错杀),一边还是躲不过真正的趋势。

于是问题变成:怎么在不频繁止损、也不硬扛的前提下,连续地感知 μ?这需要一只"第二只眼睛"。


04 · 第二只眼睛:确认,而不是预测

EWY 是韩国股指,它有成分股——三星、SK 海力士。关键的转念是:不去赌"谁领先谁"(那种领先关系并不稳定,会被地缘、美联储、消息面不断打乱),而是用成分股来确认一个下跌是不是"真的"。这只需要同步性,不需要领先性。

数学上,把 EWY 的每一步收益,拆成两块:

r_EWY  =  β · r_参考   +   ε
          └─系统性─┘     └特质性┘
       广泛的/基本面的     EWY 自己的
       → 真趋势           → 会回归

判断规则一下子清晰了:

价格自己看不出这个区别,一根阴线长得都一样。加上成分股这只眼睛,就能把趋势和噪声分开——这正是我一直想要的"短期偏离 vs 长期转换"的分水岭。

图 4:左侧三条价格流同步走弱,系统判为广泛趋势并关闭逆势开仓;右侧只有目标短期偏离,均值回归通道仍然保留。


05 · 让参数自己收敛

还有一个更深的不满:区间该多宽、该等多久回归——凭什么由我拍脑袋定?

如果价差是均值回归的,它就服从 OU 过程,那么它的时间尺度是可以从数据里估出来的,不是我输入的参数。核心是回归半衰期

H = ln2 / θ        (θ 是回归速度)
  • 半衰期 H 定持仓时间尺度,更重要的是给出退出判据:超过 k·H 还不回归 = 回归关系变质 = 趋势转换确认 → 退出,而不是死等。
  • 均衡幅度 σ_eq 定区间宽度。

"等多久算太久",从一种主观焦虑,变成了一个数字。刺三里那个"干等回归浪费时间",第一次有了精确的闸门。


06 · 收敛:三个递归估计器

所有诉求,最后收敛成一个原则——递归、在线、无固定窗口。每来一根 K 线更新一次,记忆随信噪比与回归速度自适应,而不是回看固定的 N 根。三个估计器扛起全部判断。

图 5:Kalman 跟踪局部水平与斜率,RLS-OU 估计回归尺度,跨资产 β 分解系统性漂移;三路结果最后共同决定区间与风控闸门。

① Kalman 局部水平 + 斜率 —— 取代一切 ATR / MA / RSI

用一个二维卡尔曼滤波(状态 = 水平 + 斜率)在线跟踪价格。它没有回看窗口:卡尔曼增益随信噪比自适应,市场稳则记忆长、突变则快速遗忘。观测噪声 R(也就是波动率)用 Robbins-Monro 递归在线估计——软记忆,不是固定窗口。

def kf_step(st, y):
    """Kalman 局部水平+趋势,逐点更新一个状态字典。观测噪声 R 在线自适应,无窗口。"""
    st["price"] = y
    if not st["init"]:
        st["l"] = y
        st["s"] = 0.0
        st["R"] = max((y * 0.0005) ** 2, 1e-12)   # 初始波动先验≈5bp
        st["init"] = True
        return

    R = st["R"] if st["R"] > 0 else 1e-12
    q_s = KF_Q_RATIO * R          # 斜率过程噪声(响应性,唯一软先验)
    q_l = KF_Q_RATIO * R * 0.25

    # 预测: F=[[1,1],[0,1]]
    l_pred = st["l"] + st["s"]
    s_pred = st["s"]
    pp00 = st["p00"] + st["p01"] + st["p10"] + st["p11"] + q_l
    pp01 = st["p01"] + st["p11"]
    pp10 = st["p10"] + st["p11"]
    pp11 = st["p11"] + q_s

    # 更新: H=[1,0]
    e = y - l_pred                 # 创新(本次预测误差)
    S = pp00 + R
    k0 = pp00 / S
    k1 = pp10 / S
    st["l"] = l_pred + k0 * e
    st["s"] = s_pred + k1 * e
    st["p00"] = (1 - k0) * pp00
    st["p01"] = (1 - k0) * pp01
    st["p10"] = pp10 - k1 * pp00
    st["p11"] = pp11 - k1 * pp01

    # 观测噪声自适应(Robbins-Monro,软记忆,非固定窗口)
    st["R"] = st["R"] + KF_ADAPT * (e * e - st["R"])
    st["sigma"] = math.sqrt(st["R"])          # 瞬时波动
    st["z"]     = st["s"] / (st["sigma"] + 1e-12)   # 趋势强度=斜率/波动,带符号

输出的 st["z"](斜率/波动,一个带符号的"每根信噪比")就是我们的趋势强度信号。注意这个状态是一个字典 st——同一套数学,目标和每个参考标的复用

② RLS-AR(1),遗忘因子自洽 —— 让时间尺度自己长出来

用带遗忘因子的递归最小二乘,在线拟合 price_t = a + b·price_{t-1}。由回归系数 b 反推 OU 半衰期 H 和均衡幅度 σ_eq

精髓在于遗忘因子由估出的半衰期反过来决定(记忆≈OU_MEMORY 个半衰期)——记忆长度是数据自己收敛出来的,不是我拍的窗口。

def _rls_update(y_prev, y):
    """RLS-AR(1) 估计均值回归。遗忘因子由估出的半衰期自洽给出,无固定窗口。"""
    global _rls_a, _rls_b, _rls_p00, _rls_p01, _rls_p10, _rls_p11
    global _rls_resid_var, _rls_init, _ou_half_life, _ou_sigma_eq

    # 遗忘因子:记忆 ≈ OU_MEMORY 个半衰期;半衰期未知时用温和默认
    if _ou_half_life > 0:
        mem = max(OU_MEMORY * _ou_half_life, 5.0)
        ff  = math.exp(-1.0 / mem)
    else:
        ff = 0.99
    ff = min(max(ff, 0.90), 0.9995)

    phi0, phi1 = 1.0, y_prev
    pph0 = _rls_p00 * phi0 + _rls_p01 * phi1
    pph1 = _rls_p10 * phi0 + _rls_p11 * phi1
    denom = ff + phi0 * pph0 + phi1 * pph1
    k0 = pph0 / denom
    k1 = pph1 / denom
    e = y - (_rls_a * phi0 + _rls_b * phi1)
    _rls_a += k0 * e
    _rls_b += k1 * e
    _rls_p00 = (_rls_p00 - k0 * pph0) / ff
    _rls_p01 = (_rls_p01 - k0 * pph1) / ff
    _rls_p10 = (_rls_p10 - k1 * pph0) / ff
    _rls_p11 = (_rls_p11 - k1 * pph1) / ff
    _rls_resid_var = _rls_resid_var + KF_ADAPT * (e * e - _rls_resid_var)

    b = _rls_b
    if 0.0 < b < 1.0:
        theta = -math.log(b)
        _ou_half_life = math.log(2.0) / theta               # 半衰期(根)
        if (1 - b * b) > 1e-9 and _rls_resid_var > 0:
            _ou_sigma_eq = math.sqrt(_rls_resid_var / (1 - b * b))  # 均衡幅度
    else:
        _ou_half_life = 0.0   # b≥1:无回归(单位根/趋势),交给趋势闸门处理

区间宽度直接由 σ_eq 决定,随波动/回归速度自适应,带上下限保护——没有任何固定窗口

def effective_width_abs(ref_price):
    """区间宽度由 OU 均衡幅度 σ_eq 决定,随波动结构自适应。未热身时回退固定百分比。"""
    if estimators_ready() and _ou_sigma_eq > 0:
        w = WIDTH_SIGMA_MULT * _ou_sigma_eq
        floor_w = pct_to_abs(GRID_WIDTH_PCT, ref_price) * WIDTH_FLOOR_FRAC
        cap_w   = pct_to_abs(GRID_WIDTH_PCT, ref_price) * WIDTH_CAP_MULT
        return min(max(w, floor_w), cap_w)
    return pct_to_abs(GRID_WIDTH_PCT, ref_price)

③ 跨资产 β 分解 + 库存风控 —— 第二只眼睛与安全带

这是把系统性趋势判断的"确认"真正嵌进去。每根新 K 线,在收益空间用 EWMA 递归估计目标对各参考的回归系数 β_j,再把目标漂移拆成系统性与特质性两部分:

# —— update_estimators 内部,逐根更新 β 与系统性趋势 ——

# 递归回归系数 β_j = cov / var (无窗口软记忆)
r_t = (c - prev_close) / prev_close            # 目标本根收益
r_j = (rc - _ref_prevc[j]) / _ref_prevc[j]     # 参考本根收益
_ref_cov[j] += KF_ADAPT * (r_t * r_j - _ref_cov[j])
_ref_var[j] += KF_ADAPT * (r_j * r_j - _ref_var[j])
_ref_beta[j] = _ref_cov[j] / _ref_var[j]

# 系统性(被参考确认的)漂移: ĝ_sys = Σ β_j · 参考分数漂移
g_sys = 0.0
for j in range(nref):
    g_sys += _ref_beta[j] * (_kf_refs[j]["s"] / _kf_refs[j]["price"])
sig_frac     = _vol_sigma / pt
_trend_z_sys = g_sys / (sig_frac + 1e-12)      # 系统性趋势强度
_sys_share   = abs(g_sys) / (abs(g_t) + 1e-12) # 系统性占比

于是趋势闸门只认被参考确认的那部分。启用参考时用系统性 z_sys,否则退回目标自身 z

def gate_trend_z():
    """
    启用参考且数据可用 → 用【系统性】z_sys(被三星/海力士等确认的部分);否则退回目标自身 z。
    目标跌但参考没跟(特质噪声) → z_sys≈0 → 不拦,照收割;
    目标跌且参考同步(系统真趋势) → z_sys 显著 → 拦住逆势单。
    """
    if ref_n() > 0 and _refs_ok and estimators_ready():
        return _trend_z_sys
    return _trend_z

而"最高点灵活下调"(刺四),用库存保留价偏移(Avellaneda-Stoikov 保留价的离散版)实现:多头库存越大,有效上沿越往下压,高位自动停开——天花板自己会降。

def effective_ceiling():
    """多头库存越大,有效上沿越往下压——高位不再加仓,天花板自动下调。"""
    long_amt  = assets[base_currency].get("long_amount", 0)
    short_amt = assets[base_currency].get("short_amount", 0)
    net_usdt  = (long_amt - short_amt) * assets[base_currency]["ctVal"] * get_price()
    inv_ratio = min(max(0.0, net_usdt / (Funding * MAX_NET_EXPOSURE_MULT)), 1.0)
    drop = INVENTORY_SKEW * inv_ratio * (range_high - range_low)
    return range_high - drop

这些闸门统一收束在开仓判断里——趋势、净敞口硬上界、库存天花板、回撤熔断,任何一道不通过就不开新仓,但绝不强平已有的止盈单(既不逆势加仓、也不频繁止损):

def can_open(side, grid_price):
    """开新仓的多道数学闸门:趋势 / 净敞口 / 库存天花板 / 回撤熔断。"""
    if estimators_ready():
        gz = gate_trend_z()
        if side == "long"  and gz <= -TREND_Z_THRESHOLD: return False, "trend"
        if side == "short" and gz >=  TREND_Z_THRESHOLD: return False, "trend"

    # 净敞口硬上界(防无限接飞刀爆仓)
    net_usdt = (long_amt - short_amt) * ct_val * price
    if side == "long"  and  net_usdt >= Funding * MAX_NET_EXPOSURE_MULT: return False, "risk"
    if side == "short" and -net_usdt >= Funding * MAX_NET_EXPOSURE_MULT: return False, "risk"

    # 库存天花板:高位停开
    if side == "long" and grid_price > effective_ceiling():           return False, "skew"

    if check_drawdown_breaker():                                       return False, "dd"
    return True, ""

07 · 落地:四根刺,一次拔清

重构成 v4.0 后,四根刺各自对上了一个数学机制——不再是打补丁,而是同一套骨架的不同投影:

参考合约只要在参数里填一行 SAMSUNG_USDT, SKHYNIX_USDT(用 GetRecords 直接取数,无需配置附加交易所),其余全自适应。


08 · 验证:第二只眼睛,睁开了

用合成序列做数值验证。同样是 EWY 在跌,就看成分股确认与否,闸门给出完全相反的动作——这正是"确认而非预测"该有的样子:

目标自己看都是下跌(z ≈ −0.3),但有了第二只眼睛:一个判为真跌该躲,一个判为噪声该赚。价格永远分不出的区别,数学分出来了。


尾声

这次优化真正的收获,不是那份代码。是一条路径:

从实盘里那根具体的刺出发,拒绝用过时的、无法自证有效的套路去糊,一路逼问到数学的第一性,让参数自己收敛出来。

四根刺,最后收敛成两个可调旋钮(趋势灵敏度、区间松紧)和一个风险偏好。剩下的,交给数据自己说话——以及,交给回测去检验这套地基到底赚不赚钱。那是下一段故事。


写在最后 · 这个时代的答案

老实说,这篇文章里的数学,我并不都懂。卡尔曼滤波怎么推导、RLS 的遗忘因子为什么要那样自洽、OU 半衰期背后的随机微分方程——真要我从头讲一遍,我讲不清楚。

但这恰恰是这个时代最好的地方。

想做一桌好菜,酱油和醋不需要我们亲手去酿——我们照样能做出美味。在大模型的时代,能力的分工已经悄悄换了位置:重要的,不再是"解决问题",而是"从实践中发现问题"。

求解的那一段路,工具可以替我们走完——推导、建模、写代码、做验证。但"哪里痛、为什么痛、真正的问题到底是什么",这一段,只能来自真实的实践。是我在盘面前一天天盯着那四根刺,才知道该往哪问;是我坚持"固定窗口的指标无效",才逼出了那套递归无窗口的骨架。工具能回答问题,但提不出问题;能走完路,但选不了方向。

知识的阶梯正在越变越低,几乎人人都能借助工具,一步就站到答案跟前。可也正因为如此,真正稀缺的东西变了:是发现真正问题的眼力,是在无数表象里迈向正确那条路的判断力。方向对了,答案自会浮现;方向错了,再强的算力也只是把你更快地带向错误。

所以这篇文章记录的,与其说是一套量化策略,不如说是一次示范——

一个并不精通全部数学的人,只要能从实盘里精准地指出"这四根刺"、并且不肯用似是而非的套路敷衍过去,就能借助这个时代的工具,把问题一根根拔干净。

在人人都能拿到答案的时代,先学会提对的问题。

 

免责声明:信息仅供参考,不构成投资及交易建议。投资者据此操作,风险自担。
如果觉得文章对你有用,请随意赞赏收藏
相关推荐
相关下载
登录后评论
Copyright © 2019 宽客在线