1. ¿Por qué es necesaria la minería automatizada de factores?
Si has tenido contacto con el trading cuantitativo, seguramente has escuchado el término "factor". ¿Qué es un factor? En pocas palabras, es una señal del mercado expresada mediante datos. Por ejemplo, el momento del precio, anomalías en el volumen, la posición de las Bandas de Bollinger: se utiliza para predecir si una moneda subirá o bajará en un período de tiempo determinado.
Suena simple, pero aquellos que realmente investigan factores saben lo difícil que es:
Conocimientos sólidos en finanzas y una base profunda en estadística matemática
Una gran cantidad de datos históricos limpios
Un marco de backtesting riguroso
Además, hay que enfrentar un problema inevitable: los factores se deterioran
Una señal que es efectiva hoy, puede volverse completamente inútil en unos días, porque los participantes del mercado aprenden, se adaptan y arbitran ese patrón. Por lo tanto, la minería de factores nunca es un trabajo único; requiere una iteración continua.
Lo que presenta este artículo es precisamente un sistema que automatiza este proceso: un ciclo fijo que ejecuta de forma repetida la minería completa de factores → verificación → eliminación → síntesis de señales → ejecución de órdenes. Reemplazar la repetición manual con iteraciones de máquina permite que la estrategia siempre se mantenga al ritmo de los cambios del mercado.
2. Arquitectura general del sistema
El proceso tradicional de minería de factores es: el investigador plantea una hipótesis → escribe código → ejecuta backtesting → filtra → pone en producción → meses después descubre que ya no funciona → vuelve a empezar. Todo el ciclo puede tomar semanas o incluso meses.
Este sistema comprime todo el ciclo para que se ejecute automáticamente en un intervalo fijo:
| Paso | Módulo | Descripción |
|---|---|---|
| Paso 1 | Obtener pool de activos | Filtrar contratos perpetuos de alta liquidez según volumen de operaciones, detectar estado del mercado |
| Paso 2 | Verificar pool de factores | Analizar la salud actual de los factores, determinar la dirección de exploración de esta ronda |
| Paso 3 | Generar factores con IA | Bajo un marco de restricciones, hacer que la IA genere factores candidatos de nuevas dimensiones |
| Paso 4 | Validación IC | Calcular el coeficiente de información mediante backtesting con datos históricos, eliminar factores ineficaces |
| Paso 5 | Filtro de correlación y eliminación por rezago | Eliminar factores con información superpuesta, mantener el pool de factores preciso y no excesivo |
| Paso 6 | Síntesis de señales y ejecución de órdenes | Sintetizar puntuaciones ponderadas, las señales que superan el umbral activan el rebalanceo de la cartera |
El sistema está impulsado por dos schedulers: activador lento ejecuta un ciclo completo de iteración de factores cada hora; activador rápido consulta el estado de las posiciones cada segundo, manejando take profit/stop loss y actualizando el dashboard.
3. Explicación detallada de cada módulo y código central
3.1 Obtener pool de activos
Al inicio de cada ronda, el sistema extrae los datos en tiempo real de todos los contratos perpetuos del exchange, los ordena por volumen de operaciones y toma los primeros N. La liquidez es un requisito previo para la efectividad de un factor: las monedas pequeñas con bajo volumen de negociación distorsionan cualquier señal.
Al mismo tiempo, se detecta la posición percentil de la volatilidad en el gráfico de 4 horas de BTC para determinar el estado general del mercado (normal / high_vol / low_vol / volatile). Esta evaluación influye directamente en la preferencia de dirección para la generación de factores por parte de la IA.
javascript
// Filtrar activos de alta liquidez según volumen de operaciones
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));
// Detectar percentil de volatilidad de BTC para determinar estado del mercado
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;
// Comparar con la volatilidad histórica completa para determinar percentil
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 Verificar el estado del pool de factores
Antes de que la IA genere nuevos factores, el sistema primero evalúa la salud del pool actual: qué factores han mostrado una disminución constante del IC en los últimos períodos (deterioro) y qué dimensiones aún no se han cubierto. Esta información se pasa directamente a la IA como restricción para evitar explorar direcciones ya agotadas.
javascript
const factorPool = JSON.parse(_G('afi_factorPool') || '[]');
const icHistory = JSON.parse(_G('afi_icHistory') || '{}');
const icDecayWindow = $vars.icDecayWindow || 48; // Longitud de la ventana reciente
const icDecayThreshold = $vars.icDecayThreshold || -0.01; // Umbral de deterioro
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
});
}
}
// Determinar dinámicamente cuántos factores nuevos explorar en esta ronda
const explorationBuffer = $vars.explorationBuffer || 3;
const explorationCount = Math.max(
explorationBuffer,
targetFactorCount - validCount + explorationBuffer
);
const action = factorPool.length === 0 ? 'generate_initial' : 'iterate_factors';
3.3 Construir el prompt para que la IA invente factores
La IA no recibe una tarea abierta, sino un marco con restricciones. El prompt incluye: estado actual del mercado, lista de factores existentes (prohibir repeticiones), factores recientemente deteriorados (prohibir ajustes menores), dimensiones ya cubiertas y dimensiones que aún no se han explorado.
De esta manera, los factores candidatos generados realmente son intentos hacia nuevas direcciones, no simplemente recalcular un factor existente con parámetros diferentes.
javascript
// 迭代模式 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 包裹):
json
{
"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 Validación de IC: los datos hablan, no la intuición
El IC (Coeficiente de Información, Information Coefficient) mide: ¿qué tan alta es la correlación entre el ranking transversal calculado con el factor y el ranking real de la fluctuación de precios de la siguiente vela? Cuanto mayor es el IC, más precisa es la predicción del factor.
El método de validación es la reproducción histórica (Walk-Forward): tomando los últimos cientos de velas, en cada punto temporal t, se calcula el valor del factor utilizando los datos del momento t-1 para predecir la subida o bajada de la vela t. La secuencia temporal está estrictamente alineada, eliminando cualquier función futura.
javascript
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 };
}
El umbral de IC está controlado por la variable
$vars.icThreshold, con un valor predeterminado de 0.02. Este es un umbral de entrada relativamente laxo, adecuado para filtrar rápidamente factores claramente inválidos; si se requiere un control de significancia estadística más estricto, se puede aumentar este valor según la situación real. Los factores que no superen el umbral, sin importar lo perfecta que sea su lógica, serán eliminados directamente.
3.5 Filtrado de correlación y eliminación de los últimos puestos
Los factores que pasan la validación de IC aún deben superar dos pruebas adicionales:
Primera prueba: Filtrado de correlación. Si dos factores tienen puntuaciones transversales muy similares (|corr| > umbral), se conserva el que tenga mayor IC y se descarta el otro. Es como si dos votos representaran la misma idea de una persona; basta con fusionarlos en un solo voto, tener uno más no añade una opinión adicional.
Segunda prueba: Eliminación de los últimos puestos. El conjunto de factores tiene un límite de capacidad; cuando se excede, los factores se clasifican por rendimiento y los peores salen. Los factores cuyo IC reciente ha estado disminuyendo participan en la clasificación con su IC reciente en lugar de su IC histórico promedio, soportando una mayor presión de eliminación.
javascript
// Filtro de correlación (conserva el de mayor IC, descarta factores redundantes altamente correlacionados)
const corrThreshold = $vars.corrThreshold || 0.7;
survivedFactors.sort((a, b) => b.icAvg - a.icAvg); // Ordenar primero por IC descendente
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) {
// Registrar el factor correlacionado absorbido (para visualización en panel)
selected.corrGroup = (selected.corrGroup ? selected.corrGroup + ',' : '')
+ factor.name;
isRedundant = true;
break;
}
}
if (!isRedundant) decorrelatedFactors.push({ ...factor, corrGroup: '' });
}
// Eliminación del último: los factores en decaimiento usan el IC reciente en lugar de la media histórica para el ranking
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));
Nota: El cálculo de correlación se basa en las puntuaciones del factor en el corte transversal actual, lo que puede ocasionar juicios erróneos ocasionales en ciertos momentos. Un enfoque más robusto sería tomar la correlación promedio de múltiples cortes transversales históricos, lo cual es una dirección de mejora futura.
3.6 Síntesis de señales y ejecución de rebalanceo
Una vez establecido el pool de factores, el sistema calcula una puntuación compuesta para cada activo: estandariza los valores transversales de cada factor mediante Z-score y luego los pondera según su IC reciente (a mayor contribución del factor, mayor peso; los factores con IC reciente negativo tienen peso cero).
javascript
// Pesos de factores: ponderación por IC reciente (factores con IC negativo tienen peso cero)
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 negativo → peso 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);
// Estandarización 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;
}
// Puntuación compuesta
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;
}
// Filtro por umbral: señales ambiguas se descartan, no se abren posiciones
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);
Durante la ejecución del rebalanceo, primero se cierran las posiciones antiguas que no estén en la lista actual y luego se abren las nuevas señales de manera proporcional al patrimonio de la cuenta:
javascript
const positionRatio = $vars.positionRatio || 0.8; // Proporción de uso del patrimonio total
const maxLeverage = $vars.maxLeverage || 3;
const account = exchange.GetAccount();
const equity = account.Equity || account.Balance;
const perAmt = (equity * positionRatio) / (longList.length + shortList.length);
// Cerrar posiciones antiguas que no están en el conjunto objetivo
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));
// Limpiar estado de trailing take profit
const cm = sym.match(/^(.+)_USDT/);
if (cm) { _G(cm[1] + '_maxpnl', null); _G(cm[1] + '_trail', null); }
}
}
// Abrir nuevas posiciones por señal (usando orden de mercado, -1 indica mercado)
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 Monitoreo de posiciones: Stop Loss / Take Profit / Trailing Stop Dinámico
El activador rápido se ejecuta a nivel de segundos, monitoreando en tiempo real las ganancias/pérdidas flotantes de todas las posiciones, implementando tres lógicas de salida:
- Stop Loss fijo: Pérdida flotante superior a
STOP_LOSS_PCT(por defecto 5%) → cierre automático. - Take Profit fijo: Ganancia flotante superior a
TAKE_PROFIT_PCT(por defecto 10%) → cierre automático. - Trailing Stop Dinámico: Se activa cuando la ganancia flotante alcanza
TRAIL_TRIGGER(3%); el umbral de retroceso se ajusta dinámicamente según la máxima ganancia flotante.
javascript
const STOP_LOSS_PCT = $vars.stopLossPct || 5;
const TAKE_PROFIT_PCT = $vars.takeProfitPct || 10;
const TRAIL_TRIGGER = 3; // Activación del trailing stop al alcanzar 3% de ganancia flotante
// Umbral de retroceso dinámico: cuanto mayor es la ganancia máxima, mayor es el retroceso permitido
function getDynamicTrailDrawdown(maxPnl) {
if (maxPnl >= 7) return 3; // Ganancia máxima ≥7%, se permite retroceso de 3%
if (maxPnl >= 4) return 2; // Ganancia máxima ≥4%, se permite retroceso de 2%
return 1.5; // En otros casos, retroceso de 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;
// Rastrear la máxima ganancia flotante
let maxPnl = _G(coin + '_maxpnl');
if (maxPnl === null) { maxPnl = pnlPct; _G(coin + '_maxpnl', maxPnl); }
else if (pnlPct > maxPnl) { maxPnl = pnlPct; _G(coin + '_maxpnl', maxPnl); }
// Activar trailing stop
if (!_G(coin + '_trail') && maxPnl >= TRAIL_TRIGGER) {
_G(coin + '_trail', true);
Log(coin + ' Activando trailing stop, ganancia flotante: +' + pnlPct.toFixed(2) + '%');
}
const trailDrawdown = getDynamicTrailDrawdown(maxPnl);
let reason = null;
if (_G(coin + '_trail') && (maxPnl - pnlPct) >= trailDrawdown)
reason = 'Trailing stop (retroceso ' + (maxPnl - pnlPct).toFixed(2) + '%, umbral ' + trailDrawdown + '%)';
if (!reason && pnlPct <= -STOP_LOSS_PCT) reason = 'Stop loss (' + pnlPct.toFixed(2) + '%)';
if (!reason && pnlPct >= TAKE_PROFIT_PCT) reason = 'Take profit (' + pnlPct.toFixed(2) + '%)';
if (reason) {
exchange.CreateOrder(pos.Symbol, isLong ? 'closebuy' : 'closesell', -1, amt);
Log(coin, 'Disparado', reason);
_G(coin + '_maxpnl', null); _G(coin + '_trail', null);
}
}
}
4. Decisiones clave de diseño
4.1 ¿Por qué usar Rank IC (en lugar de Pearson IC)?
Rank IC utiliza rangos en lugar de valores brutos para calcular la correlación, siendo naturalmente robusto frente a valores extremos (outliers) en los factores. La distribución de precios en el mercado de criptomonedas tiene colas pesadas; Pearson IC se distorsiona fácilmente por unas pocas velas extremas, mientras que Rank IC ofrece mayor estabilidad.
4.2 Alineación temporal estricta, evitando funciones futuras
Tanto la validación de IC como el cálculo de señales en línea utilizan de forma uniforme los valores de los factores en t-1 para predecir el rendimiento en t: en la validación, records pasa fullRecords.slice(0, t), truncando físicamente los datos futuros; no importa cómo el código del factor generado por IA haga referencia a records[n], solo accederá al historial hasta t-1. En línea, se elimina la última vela (slice(0, n-1)) para calcular los valores de los factores y predecir la subida/bajada de la siguiente vela. Ambas lógicas son idénticas, evitando que la mirada a datos futuros infla artificialmente el IC.
4.3 Ponderación del IC reciente, peso adaptativo al mercado
Los pesos de los factores no son fijos, sino que se ajustan dinámicamente según el IC reciente. Cuando un factor comienza a fallar (disminución del IC reciente), su peso en la síntesis de señales se reduce automáticamente hasta llegar a cero, y el sistema puede completar el reequilibrio de pesos sin intervención manual.
4.4 Arquitectura de doble disparador
La iteración de factores es una tarea de recálculo pesado (extracción de velas + backtesting de IC + llamada de IA), que es suficiente ejecutar una vez por hora; la protección de posiciones es una tarea sensible al tiempo que requiere respuesta en segundos. Dividir ambas en disparadores de diferentes frecuencias evita el bloqueo mutuo.
V. Observación en vivo: Proceso de iteración de factores
Tras dos días de operación en vivo, se observaron los siguientes fenómenos:
- Los factores que ingresaron al pool inicialmente tenían un IC histórico generalmente entre 0.04 y 0.07, superando el umbral básico.
- A medida que avanza la iteración, el IC reciente de casi todos los factores está disminuyendo; algunos caen de 0.06 a 0.008, otros caen a valores negativos. Esto indica que las señales capturadas por estos factores están fallando en el entorno actual del mercado.
- Después de que el sistema detecta la degradación, en la siguiente ronda prioriza la exploración de dimensiones aún no cubiertas para buscar nuevos factores candidatos que reemplacen a los existentes. Todo el proceso no requiere intervención manual.
Dos días es un período corto, insuficiente para verificar plenamente si la capacidad de adaptación del sistema es realmente efectiva. Aquí solo se registra que el sistema ha ejecutado acciones de iteración según lo esperado; conclusiones más significativas requieren una observación continua a más largo plazo. Sin embargo, este proceso ya muestra que la lógica básica del diseño del sistema está funcionando: no se aferra a señales fallidas, sino que continúa explorando nuevas dimensiones.
VI. Reflexiones finales
Construir este sistema no es para demostrar que la IA puede vencer al mercado. Sino para decir que, en la era de la IA, muchas cosas que antes solo las instituciones de élite podían hacer, ahora las personas comunes también tienen la oportunidad de intentar.
Minería de factores, iteración de estrategias, ejecución automática: cosas que antes requerían un equipo, una gran infraestructura de datos y años de acumulación para construirse, hoy se pueden poner en marcha con un flujo de trabajo.
Esto no significa que genere ganancias estables. El mercado siempre es más complejo que cualquier sistema. Pero significa que las barreras están disminuyendo, las herramientas se están volviendo más potentes y la posibilidad de que las personas comunes participen en esto está aumentando.
⚠️ Advertencia de riesgo: Cualquier estrategia conlleva riesgo de pérdidas. El contenido de este artículo es solo para referencia técnica de aprendizaje y no constituye un consejo de inversión. Antes de operar en vivo, asegúrese de realizar pruebas exhaustivas.
Código fuente de la estrategia: Versión de prueba de estrategia cuantitativa de minería de factores adaptativos
- 1


