avatar of ianzeng123 ianzeng123
关注 私信
2
关注
431
关注者

多因子策略不是大厂专属:独立quant的研究框架

创建于: 2026-03-27 15:05:12, 更新于: 2026-03-31 18:40:35
comments   0
hits   17

[TOC]

多因子策略不是大厂专属:独立quant的研究框架

一、为什么需要自动化因子挖掘

如果你接触过量化交易,一定听过”因子”这个词。因子是什么?说白了,就是一种用数据表达的市场信号。比如价格动量、成交量异常、布林带位置——用来预测接下来一段时间某个币涨还是跌。

听起来简单,但真正做因子研究的人都知道,这件事有多难:

扎实的金融知识深厚的数理统计学背景 大量干净的历史数据 严格的回测框架

还要面对一个永远绕不过去的问题:因子会衰减

今天有效的信号,过几天可能就完全失灵了——因为市场参与者会学习、会适应,会把这个规律套利掉。所以因子挖掘从来不是一次性的工作,它需要持续迭代。

本文介绍的,正是一套把这件事自动化的系统:固定间隔循环进行完整的因子挖掘 → 验证 → 淘汰 → 信号合成 → 下单交易。用机器迭代替代人工重复,让策略始终跟上市场的变化节奏。

多因子策略不是大厂专属:独立quant的研究框架


二、系统整体架构

传统的因子挖掘流程是:研究员提出假设 → 写代码 → 跑回测 → 筛选 → 上线 → 几个月后发现失效 → 重来。整个周期可能要几周甚至几个月。

这套系统把整个循环压缩到固定间隔自动执行一次:

步骤 模块 说明
Step 1 获取标的池 按成交额筛选高流动性永续合约,检测市场状态
Step 2 检查因子池 分析当前因子健康度,确定本轮探索方向
Step 3 AI 生成因子 在约束框架下,让 AI 生成新维度候选因子
Step 4 IC 验证 用历史数据回放计算信息系数,淘汰无效因子
Step 5 相关性过滤 & 末位淘汰 去除信息重叠因子,保持因子池精而不多
Step 6 信号合成 & 下单 加权合成评分,超阈值信号触发调仓执行

系统由两个调度器驱动:慢触发器以小时级别执行一次完整的因子迭代流程;快触发器以秒级别轮询一次持仓状态,处理止盈止损和仪表板刷新。


三、各模块详解与核心代码

3.1 获取标的池

每轮开始,系统从交易所拉取所有永续合约实时行情,按成交额排序取前 N 名。流动性是因子有效性的前提——小币成交量稀疏,任何信号都容易失真。

同时检测 BTC 四小时线波动率历史分位,判断市场整体状态(normal / high_vol / low_vol / volatile),这个判断会直接影响 AI 生成因子的方向偏好。

// 按成交额筛选高流动性标的
const topN = $vars.topN || 150;
const tickers = exchange.GetTickers();

const filtered = tickers
  .filter(t => t.Symbol.endsWith('USDT.swap'))
  .map(t => ({ symbol: t.Symbol, quoteVolume: t.Last * t.Volume }))
  .sort((a, b) => b.quoteVolume - a.quoteVolume)
  .slice(0, topN)
  .map(t => t.symbol);

_G('afi_symbolPool', JSON.stringify(filtered));

// 检测 BTC 波动率分位,判断市场状态
const btcR = exchange.GetRecords('BTC_USDT.swap', PERIOD_H4);
const n = btcR.length;
const returns20 = [];
for (let i = n - 20; i < n; i++)
  returns20.push(Math.abs((btcR[i].Close - btcR[i-1].Close) / btcR[i-1].Close));
const avgVol = returns20.reduce((a, b) => a + b, 0) / returns20.length;

// 与全历史波动率比较,确定分位数
const allVols = [];
for (let i = 1; i < n; i++)
  allVols.push(Math.abs((btcR[i].Close - btcR[i-1].Close) / btcR[i-1].Close));
allVols.sort((a, b) => a - b);
let btcVolPercentile = allVols.findIndex(v => v >= avgVol) / allVols.length;

let marketState = 'normal';
if (btcVolPercentile > 0.8)      marketState = 'high_vol';
else if (btcVolPercentile < 0.3) marketState = 'low_vol';

_G('afi_marketState', marketState);
_G('afi_btcVolPct', btcVolPercentile.toFixed(2));

3.2 检查因子池状态

在让 AI 生成新因子之前,系统先盘点当前因子池的健康状况:哪些因子近期 IC 持续下滑(衰减),哪些维度还没有被覆盖。这些信息会直接作为约束条件传给 AI,避免重复探索已失效的方向。

const factorPool = JSON.parse(_G('afi_factorPool') || '[]');
const icHistory  = JSON.parse(_G('afi_icHistory')  || '{}');
const icDecayWindow    = $vars.icDecayWindow    || 48;    // 近期窗口长度
const icDecayThreshold = $vars.icDecayThreshold || -0.01; // 衰减判定阈值
const targetFactorCount = $vars.targetFactorCount || 10;

const degradedFactors = [];
for (const factor of factorPool) {
  const icArr = icHistory[factor.name] || [];
  if (icArr.length >= 20) {
    const window    = Math.min(icArr.length, icDecayWindow);
    const recentAvg = icArr.slice(-window).reduce((a, b) => a + b, 0) / window;
    if (recentAvg < icDecayThreshold)
      degradedFactors.push({
        name: factor.name,
        recentIC: recentAvg.toFixed(4),
        rationale: factor.rationale
      });
  }
}

// 动态决定本轮需要探索多少个新因子
const explorationBuffer = $vars.explorationBuffer || 3;
const explorationCount  = Math.max(
  explorationBuffer,
  targetFactorCount - validCount + explorationBuffer
);

const action = factorPool.length === 0 ? 'generate_initial' : 'iterate_factors';

3.3 构建 Prompt,让 AI 发明因子

AI 拿到的不是一个开放式任务,而是一套有约束的框架。Prompt 中包含:当前市场状态、已有效因子列表(禁止重复)、近期衰减因子(禁止微调)、已覆盖维度、尚未探索的维度。

这样生成出来的候选因子,才是真正面向新方向的尝试,而不是对已有因子换个参数重新跑一遍。

// 迭代模式 Prompt 关键片段
const usedDimensions = factorPool
  .map(f => f.name + '(' + (f.rationale || '') + ')')
  .join(', ') || '暂无';

const validSummary = validFactors.map(f => {
  const arr    = icHistory[f.name] || [];
  const avg    = arr.length > 0
    ? (arr.reduce((a,b) => a+b, 0) / arr.length).toFixed(4) : 'N/A';
  const recent = arr.length >= 20
    ? (arr.slice(-20).reduce((a,b) => a+b, 0) / 20).toFixed(4) : 'N/A';
  return f.name + ': 历史IC=' + avg + ' 近期IC=' + recent + ' | 逻辑: ' + f.rationale;
}).join('\n') || '暂无';

const degradedSummary = degradedFactors.length > 0
  ? degradedFactors.map(f =>
      f.name + ': 近期IC=' + f.recentIC + ' | 原逻辑: ' + f.rationale
    ).join('\n')
  : '本轮无衰减因子';

prompt += '【当前有效因子(不需要生成变体)】\n' + validSummary + '\n\n';
prompt += '【近期衰减因子(禁止在这些维度上微调)】\n' + degradedSummary + '\n\n';
prompt += '【已覆盖维度(禁止重复)】\n' + usedDimensions + '\n\n';
prompt += '【尚未探索的维度(优先从这里选)】\n' + unusedSample + '\n\n';
prompt += '生成 ' + explorationCount + ' 个全新方向因子:\n';
prompt += '1. 必须与已覆盖维度完全不同,禁止微调失效因子\n';
prompt += '2. 优先从尚未探索的维度中选取\n';
prompt += '3. 优先设计非线性组合因子\n';
prompt += '4. 针对当前 ' + marketState + ' 市场状态设计\n';

AI 的 System Prompt 内置了完整的发明者平台 TA 函数规范、代码格式约束、加密市场先验知识,以及所有可探索的因子维度清单(完整内容见策略源码)。输出格式严格为纯 JSON(无 Markdown 包裹):

{
  "factors": [
    {
      "name": "MomentumAcceleration",
      "rationale": "短期动量加速度,捕捉散户追涨惯性拐点",
      "code": "(records[n-1].Close - records[n-6].Close)/records[n-6].Close - (records[n-2].Close - records[n-7].Close)/(records[n-7].Close + 0.0001)",
      "direction": 1,
      "type": "exploration"
    }
  ]
}

3.4 IC 验证:数据说话,不靠直觉

IC(信息系数,Information Coefficient)测量的是:用因子算出来的截面排名,和下一根 K 线真实涨跌幅排名之间的相关性有多高。IC 越高,说明这个因子的预测越准。

验证方式是历史回放(Walk-Forward):拿过去几百根 K 线,在每个时间点 t,用 t-1 时刻的数据计算因子值,预测第 t 根 K 线的涨跌。时序严格对齐,杜绝未来函数。

function calcRankICFull(code, symRecords, factorName) {
  const syms    = Object.keys(symRecords);
  const icList  = [];
  const minLen  = 30;
  const allLengths = syms.map(s => symRecords[s].length);
  const minSymLen  = Math.min(...allLengths);
  const testLen    = Math.min(500, minSymLen - 1);

  for (let t = minLen; t < testLen; t++) {
    const fVals = [], nRets = [];

    for (const sym of syms) {
      const fullRecords = symRecords[sym];
      // 用 t-1 期数据计算因子(slice(0, t) 不包含第 t 根 K 线)
      const records = fullRecords.slice(0, t);
      const n = records.length;

      const v = (function() { return eval(code); })();
      if (isNaN(v) || !isFinite(v)) continue;

      fVals.push({ sym, val: v });
      // 预测第 t 根 K 线的真实收益
      nRets.push({
        sym,
        ret: (fullRecords[t].Close - fullRecords[t-1].Close) / fullRecords[t-1].Close
      });
    }
    if (fVals.length < 8) continue;

    // 计算 Rank IC(Spearman 相关系数)
    const fRank = {}, rRank = {};
    [...fVals].sort((a,b) => a.val - b.val).forEach((x,i) => fRank[x.sym] = i);
    [...nRets].sort((a,b) => a.ret - b.ret).forEach((x,i) => rRank[x.sym] = i);

    const ss = fVals.map(x => x.sym);
    const fr = ss.map(s => fRank[s]);
    const rr = ss.map(s => rRank[s]);
    const n2 = ss.length;
    const fm = fr.reduce((a,b) => a+b, 0) / n2;
    const rm = rr.reduce((a,b) => a+b, 0) / n2;
    const num = fr.map((f,i) => (f-fm) * (rr[i]-rm)).reduce((a,b) => a+b, 0);
    const den = Math.sqrt(
      fr.map(f => (f-fm)**2).reduce((a,b) => a+b, 0) *
      rr.map(r => (r-rm)**2).reduce((a,b) => a+b, 0)
    );
    if (den > 0) icList.push(num / den);
  }

  const avgIC = icList.length > 0
    ? icList.reduce((a,b) => a+b, 0) / icList.length : 0;
  return { avgIC, icList };
}

IC 阈值由变量 $vars.icThreshold 控制,默认 0.02。这是一个相对宽松的入门门槛,适合快速筛掉明显无效的因子;如需更严格的统计显著性控制,可根据实际情况调高该值。未通过阈值的因子,不论逻辑多完美,直接淘汰。


3.5 相关性过滤 & 末位淘汰

通过 IC 验证的因子还需过两道关:

第一道:相关性过滤。 如果两个因子的截面得分高度相似(|corr| > 阈值),保留 IC 更高的那个,舍弃另一个。就像两票背后是同一个人的想法,合并成一票即可,多一票也不多一个观点。

第二道:末位淘汰。 因子池有容量上限,超出部分按表现排序,最差的出局。近期 IC 持续下滑的因子会以近期 IC 而非历史平均 IC 参与排名,承受更大的淘汰压力。

// 相关性过滤(保留 IC 最高的,丢弃高度相关的冗余因子)
const corrThreshold = $vars.corrThreshold || 0.7;
survivedFactors.sort((a, b) => b.icAvg - a.icAvg); // 先按 IC 降序排列

const decorrelatedFactors = [];
for (const factor of survivedFactors) {
  let isRedundant = false;
  for (const selected of decorrelatedFactors) {
    const corr = Math.abs(calcCorrelation(
      factorScoresMap[factor.name],
      factorScoresMap[selected.name]
    ));
    if (corr > corrThreshold) {
      // 记录被吸收的相关因子(仪表板展示用)
      selected.corrGroup = (selected.corrGroup ? selected.corrGroup + ',' : '')
                           + factor.name;
      isRedundant = true;
      break;
    }
  }
  if (!isRedundant) decorrelatedFactors.push({ ...factor, corrGroup: '' });
}

// 末位淘汰:衰减因子用近期 IC 而非历史均值参与排名
const targetFactorCount = $vars.targetFactorCount || 10;
decorrelatedFactors.sort((a, b) => {
  const scoreA = a.isDecaying ? a.recentIC : a.icAvg;
  const scoreB = b.isDecaying ? b.recentIC : b.icAvg;
  return scoreB - scoreA;
});
const finalPool = decorrelatedFactors.slice(0, targetFactorCount);

_G('afi_factorPool', JSON.stringify(finalPool));

注:相关性计算基于当前截面的因子得分,在某些时刻可能存在偶发的误判。更稳健的做法是取历史多截面的平均相关性,这是后续可以改进的方向。


3.6 信号合成与调仓执行

因子池稳定后,系统给每个标的计算一个综合评分:将每个因子的截面值做 Z-score 标准化,然后按各自近期 IC 加权叠加——表现越好的因子占比越大,近期 IC 为负的因子权重置零。

// 因子权重:近期 IC 加权(负 IC 因子权重置零)
const weights = {};
let totalW = 0;
for (const f of factorPool) {
  const arr       = icHistory[f.name] || [];
  const recentArr = arr.slice(-48);
  const recentIC  = recentArr.length > 0
    ? recentArr.reduce((a,b) => a+b, 0) / recentArr.length : 0;
  const w = Math.max(0, recentIC); // 负 IC → 权重为 0
  weights[f.name] = w;
  totalW += w;
}
if (totalW > 0)
  Object.keys(weights).forEach(k => weights[k] /= totalW);
else
  factorPool.forEach(f => weights[f.name] = 1 / factorPool.length);

// Z-score 标准化
function zscore(fname) {
  const vals = validSyms
    .map(s => ({ sym: s, val: rawMatrix[s][fname] }))
    .filter(x => x.val !== null);
  if (vals.length < 5) return {};
  const mean = vals.reduce((a,b) => a + b.val, 0) / vals.length;
  const std  = Math.sqrt(vals.reduce((a,b) => a + (b.val - mean)**2, 0) / vals.length);
  const r = {};
  vals.forEach(x => r[x.sym] = std > 0 ? (x.val - mean) / std : 0);
  return r;
}

// 合成评分
const scores = {};
for (const sym of validSyms) {
  let score = 0;
  for (const f of factorPool) {
    const z = zscore(f.name)[sym];
    if (z !== undefined) score += weights[f.name] * f.direction * z;
  }
  scores[sym] = score;
}

// 阈值过滤:信号模糊的直接跳过,不入场
const longShortN      = $vars.longShortN      || 5;
const longThreshold   = $vars.longThreshold   || 0.3;
const shortThreshold  = $vars.shortThreshold  || -0.3;

const sorted        = Object.keys(scores).sort((a,b) => scores[b] - scores[a]);
const longList      = sorted.filter(s => scores[s] >= longThreshold).slice(0, longShortN);
const shortList     = sorted.slice().reverse()
                            .filter(s => scores[s] <= shortThreshold).slice(0, longShortN);

调仓执行时,先平掉不在本轮名单里的旧仓位,再按账户权益等比例开入新信号:

const positionRatio = $vars.positionRatio || 0.8;  // 总权益使用比例
const maxLeverage   = $vars.maxLeverage   || 3;
const account       = exchange.GetAccount();
const equity        = account.Equity || account.Balance;
const perAmt        = (equity * positionRatio) / (longList.length + shortList.length);

// 平掉不在目标集合里的旧仓位
const targetSet = new Set([...longList, ...shortList]);
for (const sym of Object.keys(currentHoldings)) {
  if (!targetSet.has(sym)) {
    const pos    = currentHoldings[sym];
    const isLong = pos.Type === PD_LONG || pos.Type === 0;
    exchange.CreateOrder(sym, isLong ? 'closebuy' : 'closesell', -1, Math.abs(pos.Amount));
    // 清除止盈追踪状态
    const cm = sym.match(/^(.+)_USDT/);
    if (cm) { _G(cm[1] + '_maxpnl', null); _G(cm[1] + '_trail', null); }
  }
}

// 开入新信号仓位(使用市价单,-1 表示市价)
function openPos(sym, isLong) {
  exchange.SetMarginLevel(sym, maxLeverage);
  const market = allMarkets[sym];
  const price  = exchange.GetTicker(sym).Last;
  const ctVal  = (market.CtVal && market.CtVal > 0) ? market.CtVal : 1;
  const amtPrec = market.AmountPrecision !== undefined ? market.AmountPrecision : 0;
  const minQty  = (market.MinQty && market.MinQty > 0) ? market.MinQty : 1;
  const maxQty  = (market.MaxQty && market.MaxQty > 0) ? market.MaxQty : 999999;
  let qty = _N(perAmt / price / ctVal, amtPrec);
  qty = Math.min(Math.max(qty, minQty), maxQty);
  exchange.CreateOrder(sym, isLong ? 'buy' : 'sell', -1, qty);
}

3.7 持仓监控:止损 / 止盈 / 动态移动止盈

快触发器按秒级别运行一次,实时监控所有持仓浮盈状况,执行三种离场逻辑:

  • 固定止损:浮亏超过 STOP_LOSS_PCT(默认 5%)自动平仓
  • 固定止盈:浮盈超过 TAKE_PROFIT_PCT(默认 10%)自动平仓
  • 动态移动止盈:浮盈达到 TRAIL_TRIGGER(3%)后启用,回撤阈值随最高浮盈动态调整
const STOP_LOSS_PCT   = $vars.stopLossPct   || 5;
const TAKE_PROFIT_PCT = $vars.takeProfitPct || 10;
const TRAIL_TRIGGER   = 3; // 浮盈达到 3% 后启动移动止盈

// 动态回撤阈值:最高浮盈越高,允许的回撤空间越大
function getDynamicTrailDrawdown(maxPnl) {
  if (maxPnl >= 7) return 3;   // 最高盈利 ≥7%,允许回撤 3%
  if (maxPnl >= 4) return 2;   // 最高盈利 ≥4%,允许回撤 2%
  return 1.5;                   // 其他情况,回撤 1.5%
}

function monitorTPSL(positions, tickers) {
  for (const pos of (positions || [])) {
    if (Math.abs(pos.Amount) === 0) continue;
    const cm = pos.Symbol.match(/^(.+)_USDT/); if (!cm) continue;
    const coin   = cm[1];
    const ticker = tickers[coin + '_USDT.swap']; if (!ticker) continue;

    const isLong = pos.Type === PD_LONG || pos.Type === 0;
    const cur    = ticker.Last;
    const ent    = pos.Price;
    const amt    = Math.abs(pos.Amount);
    const pnlPct = (cur - ent) * (isLong ? 1 : -1) / ent * 100;

    // 追踪最高浮盈
    let maxPnl = _G(coin + '_maxpnl');
    if (maxPnl === null) { maxPnl = pnlPct; _G(coin + '_maxpnl', maxPnl); }
    else if (pnlPct > maxPnl) { maxPnl = pnlPct; _G(coin + '_maxpnl', maxPnl); }

    // 启动移动止盈
    if (!_G(coin + '_trail') && maxPnl >= TRAIL_TRIGGER) {
      _G(coin + '_trail', true);
      Log(coin + ' 启动移动止盈,浮盈: +' + pnlPct.toFixed(2) + '%');
    }

    const trailDrawdown = getDynamicTrailDrawdown(maxPnl);
    let reason = null;
    if (_G(coin + '_trail') && (maxPnl - pnlPct) >= trailDrawdown)
      reason = '移动止盈(回撤 ' + (maxPnl - pnlPct).toFixed(2) + '%, 阈值 ' + trailDrawdown + '%)';
    if (!reason && pnlPct <= -STOP_LOSS_PCT)  reason = '止损(' + pnlPct.toFixed(2) + '%)';
    if (!reason && pnlPct >= TAKE_PROFIT_PCT) reason = '止盈(' + pnlPct.toFixed(2) + '%)';

    if (reason) {
      exchange.CreateOrder(pos.Symbol, isLong ? 'closebuy' : 'closesell', -1, amt);
      Log(coin, '触发', reason);
      _G(coin + '_maxpnl', null); _G(coin + '_trail', null);
    }
  }
}

四、关键设计决策

4.1 为什么用 Rank IC(而非 Pearson IC)

Rank IC 使用排名而非原始值计算相关性,对因子中的极端值(离群点)天然鲁棒。加密市场价格分布厚尾严重,Pearson IC 容易被少数几根极端 K 线扭曲,而 Rank IC 的稳定性更强。

4.2 时序严格对齐,杜绝未来函数

IC 验证和线上信号计算均统一使用 t-1 期因子值预测 t 期收益:验证时,records 传入 fullRecords.slice(0, t),将未来数据在物理层面截断,AI 生成的因子代码无论如何引用 records[n],访问到的都是截止到 t-1 的历史;线上时,去掉最后一根 K 线(slice(0, n-1))计算因子值,预测下一根 K 线的涨跌。两者逻辑完全一致,避免因看了未来数据而虚高 IC。

4.3 近期 IC 加权,权重随市场自适应

因子权重不是固定的,而是随近期 IC 动态调整。当某个因子开始失效(近期 IC 下降),它在信号合成中的权重自动减小直至归零,系统不需要人工干预就能完成权重再平衡。

4.4 双触发器架构

因子迭代是重计算任务(K 线拉取 + IC 回测 + AI 调用),按小时级别执行一次足够;持仓保护是时间敏感任务,需要秒级响应。将两者拆分成不同频率的触发器,避免相互阻塞。


五、实盘观察:因子迭代过程

实盘运行两天后,观察到以下现象:

  • 早期进池的因子,历史 IC 普遍在 0.04 到 0.07 之间,通过了基本门槛。
  • 随着迭代推进,几乎所有因子的近期 IC 都在下滑,有的从 0.06 降到 0.008,有的跌进负值。这说明这些因子捕捉的信号在当前市场环境下正在失效。
  • 系统检测到衰减后,下一轮优先探索尚未覆盖的维度,去找新候选因子替换。整个过程不需要人工干预。

多因子策略不是大厂专属:独立quant的研究框架

两天时间较短,还不足以充分验证系统的自适应能力是否真实有效。这里只是记录了系统按预期执行了迭代动作,更有意义的结论需要更长周期的持续观察。但这个过程本身已经能看出系统设计的基本逻辑在运转:它不会死守失效的信号,而是持续尝试新维度。


六、写在最后

搭这套系统,不是为了证明 AI 能战胜市场。而是想说,在 AI 这个时代,很多过去只有顶尖机构才能做到的事情,普通人也有机会尝试了。

因子挖掘、策略迭代、自动执行——这些以前需要一个团队、大量数据基础设施和几年积累才能搭起来的东西,今天可以用一套工作流跑起来。

这不意味着它会稳定盈利。市场永远比任何系统都复杂。但它意味着,门槛在降低,工具在变强,普通人参与这件事的可能性在变大。

⚠️ 风险提示:任何策略都有亏损风险,本文内容仅供技术学习参考,不构成投资建议。实盘前请务必充分测试。

策略源码:自适应因子挖掘量化策略测试版

相关推荐