4. Investigue factores de sincronización transversales con HistoryPanel
Sincronización transversal: umbral del factor -> máscara bool -> comparar rendimientos de la cartera y CAGR
Este tutorial se centra en un escenario de investigación muy común y muy práctico: solo tenemos una pequeña cantidad de acciones (entre unas pocas y una docena aproximadamente) y queremos tomar decisiones de sincronización de mantener/no mantener a lo largo de la propia línea de tiempo de cada acción; luego agregue esas decisiones en una única curva de cartera y finalmente compárela con el punto de referencia (HS300) para obtener una métrica anualizada reutilizable (CAGR).
Primero, dejemos claro el posicionamiento: HistoryPanel aquí sirve como un contenedor de investigación de factores livianos. Su punto fuerte es que conecta “datos -> condiciones -> definición de investigación (máscara) -> agregación de cartera -> explicación visual”, lo que nos permite validar rápidamente si vale la pena profundizar en una regla. No es un motor de backtesting comercial: no maneja la semántica comercial completa, como costos de transacción, liquidación, deslizamiento, restricciones de capital, tamaño mínimo de transacción, etc. El objetivo de este artículo es construir un bucle de investigación, no un bucle comercial.
4.1. 0. 开场:先跑通一个最小可用的研究闭环
Como muestra el siguiente código, con solo unas pocas líneas podemos obtener un HistoryPanel y trazar el primer gráfico (ya sea un gráfico de líneas o velas japonesas), lo que demuestra que «el canal de datos y visualización funciona».
import qteasy as qt
stocks = ['000001.SZ', '600519.SH', '300750.SZ']
benchmark = '000300.SH'
shares = stocks + [benchmark]
hp = qt.get_kline(
shares=shares,
start='20220101',
end='20221231',
freq='D',
as_panel=True,
)
# 最小可跑:能出图即可
fig = hp.plot(interactive=True)
fig
Si ejecuta esto en un Notebook, normalmente podrá ver un gráfico inmediatamente desde el fragmento anterior.
Sin embargo, el simple hecho de poder “tramar algo” está lejos de ser suficiente. Si realmente queremos utilizarlo como herramienta de investigación diaria, nos encontraremos con al menos los siguientes problemas:
Cómo implementar la regla de sincronización: es fácil tener una regla en mente como «mantener cuando MACD>0», pero una vez que la escribes como código, a menudo se distorsiona: nombres de columnas incorrectos, columnas faltantes, todo sale NaN o diferentes acciones están alineadas de manera inconsistente. El resultado es que el script de investigación genera errores tan pronto como lo ejecuta o, peor aún, no genera errores pero las conclusiones no son confiables.
Cómo alinear formas de máscara: La condición es esencialmente 2D (stock × tiempo), pero
HistoryPaneles 3D (stock × tiempo × campo). Si las formas no se alinean, puede aparecer un error sutil en el que “crees que estás filtrando acciones, pero en realidad estás filtrando campos”; y este tipo de error a menudo no genera un error de inmediato: simplemente hace que la curva de acciones parezca “un poco desviada”.Sin un punto de referencia, es difícil juzgar el desempeño: mirar solo la curva de la cartera hace que sea fácil darse una palmadita en la espalda. Podría simplemente estar siguiendo un cierto estilo o tendencia general del mercado durante parte de 2022. Incorporar
000300.SHcomo referencia al menos responde a una pregunta más práctica: ¿esta regla de tiempo está creando retornos excesivos, o simplemente se está moviendo con el mercado?Resultados, pero difíciles de explicar: Incluso si la cartera larga tiene un rendimiento superior, sigue siendo difícil explicar “por qué ganó”. La investigación real debe ser reproducible y revisable: cuando el rendimiento muestra un punto de inflexión claro, ¿podemos volver rápidamente al gráfico y señalar “qué factores desencadenantes cambiaron el estado de la posición”?
Afortunadamente, estas capacidades se pueden completar paso a paso. Este artículo sigue un ritmo de “ponerlo en funcionamiento primero y luego mejorarlo” para desarrollarlo en un pequeño flujo de trabajo de investigación verdaderamente utilizable.
4.2. 0.5 Primero, muestra el resultado final (con qué terminaremos)
Para mantener el ritmo más firme, primero dejemos claro el “estado final”. Después de seguir este artículo, obtendrá al menos dos tipos de resultados:
Comparación a nivel de cartera: coloque las curvas
LONG / SHORT / 000300.SH(todas normalizadas para comenzar en 1,0) en el mismo gráfico y podrá saber de un vistazo si «la regla de tiempo es efectiva durante el período de investigación».Explicación a nivel de activo único: Trace el gráfico de velas (o curva de precios) para una acción determinada y resalte los momentos en los que se activa la «condición de tenencia». De esta manera, cuando vea un segmento anormal en la curva de la cartera, podrá volver rápidamente al gráfico de acciones individuales para revisar si los «puntos de activación» coinciden con su intuición.
Nota: este artículo no requiere que genere un GIF. Un enfoque más recomendado es: primero hacer que los gráficos funcionen en una computadora portátil y simplemente tomar capturas de pantalla; Si desea escribir una publicación de blog o hacer una presentación, grabe los pasos clave en un GIF.
4.3. 1. 目标(我们这篇文章要完成什么)
Antes de comenzar, aclaremos el objetivo para que en cada paso sepamos qué problema estamos resolviendo.
Obtenga el
HistoryPanelpara un pequeño conjunto de acciones individuales +000300.SHDeducir un factor de sincronización interpretable (ejemplo: MACD)
Utilice operaciones de comparación para obtener condiciones booleanas y utilice
where()para generar la máscara de investigaciónUtilice
portfolio(mask=...)para agregar en una curva de cartera y compararla con el punto de referenciaUtilice
normalize/cum_returnpara obtener rendimientos acumulados y obtener un resumen de CAGRUtilice
plot(highlight=...)para explicar los «puntos desencadenantes» en el gráficoAl final, proporcionamos un fragmento de código completo que «se ejecuta como una única función».
Aclare el límite del alcance: este no es un motor de backtesting comercial
Todo el artículo mantiene el mismo ritmo: primero explique qué pretende resolver esta sección -> luego cubra los principios mínimos necesarios -> finalmente proporcione el código clave y el resultado esperado. El código repetido se omitirá según corresponda, pero se garantiza que cada sección será reproducible si se sigue el artículo de principio a fin.
4.4. 1.1 Requisito previo para la reproducción: ¿Los datos ya están preparados localmente?
Los ejemplos de este artículo suponen de forma predeterminada que ha configurado la fuente de datos localmente y que qt.get_kline() puede recuperar correctamente los datos de la barra diaria para 20220101–20221231.
Si su entorno aún no tiene datos, los síntomas más comunes son: qt.get_kline() devuelve un panel vacío o obtiene todos los NaN cuando calcula los indicadores más adelante. Para evitar “solo darte cuenta de que no hay datos a la mitad”, se recomienda que después de terminar la Sección 2 te asegures de verificar:
Si la duración de
hp.shapees mayor que 100 (un año de barras diarias suele ser de alrededor de 200);¿
hp.htypesincluye al menosopen/high/low/close;Si
hp.hdatescubre continuamente el período que deseas estudiar.
Si primero necesita descargar datos en su propio entorno, primero complete los capítulos sobre «Descarga de datos y configuración de la fuente de datos»; Este artículo no entrará en detalles sobre la canalización de datos y se centra en el flujo de trabajo de investigación en torno a
HistoryPanel.
4.5. 2. 准备数据:三只个股 + 一个基准指数(HS300)
2.1 Qué pretende resolver esta sección
Primero, limpiemos los datos: usemos 3 acciones individuales para la sincronización transversal y también agreguemos 000300.SH (CSI 300) al panel como punto de referencia para comparaciones posteriores de portfolio(..., benchmark=...).
El paso más crítico aquí es: asegúrese de que las columnas OHLC estén completas. Porque ya sea que estés trazando velas o haciendo reconocimiento de patrones más adelante, no puedes evitar open/high/low/close.
2.2 Principios mínimos necesarios
HistoryPanel son datos 3D: (share, time, htype). El where/mask/portfolio/cum_return posterior se basará en el requisito previo de «alineación de formas». Entonces, desde el principio imprimimos shape/shares/htypes y hacemos una validación de campo mínima, lo que puede bloquear por adelantado la mayoría de los errores de «solo a la mitad de la escritura».
2.3 Código ejecutable + resultados esperados
import qteasy as qt
stocks = ['000001.SZ', '600519.SH', '300750.SZ']
benchmark = '000300.SH'
shares = stocks + [benchmark]
hp = qt.get_kline(
shares=shares,
start='20220101',
end='20221231',
freq='D',
as_panel=True,
)
print('hp.shape:', hp.shape)
print('hp.shares:', hp.shares)
print('hp.htypes:', hp.htypes)
print('last_date:', hp.hdates[-1])
required = {'open', 'high', 'low', 'close'}
missing = [c for c in required if c not in set(hp.htypes)]
if missing:
raise ValueError(f'Missing required OHLC columns in htypes: {missing}')
if benchmark not in hp.shares:
raise ValueError(f'Benchmark {benchmark} not found in shares: {hp.shares}')
# 额外做一个“是否真的有数据”的快速检查(避免空面板/全 NaN 继续往下跑)
if hp.shape[1] < 50:
raise ValueError(
'Not enough data points loaded (too few hdates). '
'Please check your local datasource and date range.'
)
Deberías ver:
hp.sharescontiene 3 acciones individuales +000300.SHhp.htypesdebe incluir al menosopen/high/low/close(y posiblemente tambiénvol).
4.6. 3. 派生择时因子:MACD(把信号落成一列可复用的数据)
3.1 Qué pretende resolver esta sección
Primero elegimos un factor de sincronización que sea bastante común, lo suficientemente interpretable y también “inmediatamente utilizable”: MACD. El objetivo de esta sección es simple: calcular MACD y convertirlo en una nueva columna de HistoryPanel, para que el filtrado condicional posterior pueda compararse directamente con la columna.
3.2 Principios mínimos necesarios
hp.kline.macd() devuelve un nuevo HistoryPanel y agrega tres columnas a htypes:
macd_12_26_9macd_signal_12_26_9macd_hist_12_26_9
Presta atención a este naming: por defecto viene con un sufijo; no es el simple macd_hist. En este paso imprimimos los nombres de las columnas, de modo que cuando escribamos las condiciones más adelante no tengamos que adivinar y equivocarnos.
Además, cuando muchas personas realizan una investigación factorial por primera vez, inconscientemente pueden “recalcular indicadores” en diferentes lugares. Puede verse bien a corto plazo, pero una vez que comienzas a agregar más columnas y a dibujar más gráficos, es fácil terminar en un lío de «columnas del mismo nombre sobrescritas/múltiples copias de columnas sinónimas». Un enfoque más sólido es: primero calcular los indicadores en columnas, escribirlos explícitamente en htypes y luego hacer que las condiciones, la agregación y la visualización posteriores dependan solo de esas columnas.
3.3 Código ejecutable + resultado esperado
hp_macd = hp.kline.macd(price_htype='close', fastperiod=12, slowperiod=26, signalperiod=9)
print('new htypes (tail):', hp_macd.htypes[-6:])
Se esperaba: verá aparecer macd_hist_12_26_9 en htypes.
4.7. 4. 因子阈值 -> bool 条件 -> where() 研究 mask
4.1 Qué pretende resolver esta sección
Ahora explicamos claramente la regla del tiempo: por ejemplo, «cuando el histograma MACD es mayor que 0 es largo; cuando es menor o igual a 0 es corto». Lo que este paso debe ofrecer son dos máscaras:
mask_long: qué puntos de la grilla participan en la agregación de carteras largasmask_short: qué puntos de la grilla participan en la agregación de cartera corta
4.2 Principios mínimos necesarios
A partir de 2.2.8, HistoryPanel admite realizar operaciones de comparación directamente: por ejemplo, hp_macd > 0 devuelve numpy.ndarray(bool). Y hp.where(condition) normaliza varias condiciones transmisibles en una máscara (M,L,N) con la misma forma que hp.values.
Este paso es crucial: no estamos «eliminando datos», sino «definiendo la convención de investigación»: los puntos de la cuadrícula donde la máscara es False se considerarán faltantes en portfolio/cum_return (no participarán en la agregación o causarán que la ruta se rompa). Por tanto, la mascarilla es “la propia regla de investigación”.
Aquí, expliquemos la «forma» un poco más claramente:
La regla de tiempo en su cabeza es «un Verdadero/Falso por acción por día», por lo que la forma de condición más natural es
(M, L).Pero los valores de
HistoryPanelson(M, L, N), con N columnas de campo.Lo que hace
where()es: expandir/difundir su condición en(M, L, N), de modo que cualquier cálculo posterior que deba filtrar por punto de cuadrícula tenga un punto de entrada único y unificado.
Una vez que adquiera el hábito de «ejecutar todas las condiciones a través de where() primero», será menos probable que se encuentre con problemas de forma más adelante cuando introduzca las condiciones en portfolio(mask=...) y cum_return(mask=...).
4.3 Código ejecutable + resultado esperado
import numpy as np
factor_col = 'macd_hist_12_26_9'
# 取单列子面板(形状 (M,L,1)),再与标量比较得到 bool ndarray
cond_long = (hp_macd[factor_col] > 0.0) # numpy.ndarray(bool)
cond_short = (hp_macd[factor_col] <= 0.0) # numpy.ndarray(bool)
# 规整为 (M,L,N) 研究 mask
mask_long = hp.where(cond_long)
mask_short = hp.where(cond_short)
print('cond_long shape:', getattr(cond_long, 'shape', None))
print('mask_long shape:', mask_long.shape)
print('mask_long dtype:', mask_long.dtype)
# 只做一个直觉检查:最后一天 long 有多少格点为 True
print('true_count_last_day(long):', int(mask_long[:, -1, 0].sum()))
Esperado:
mask_long.shape == hp.shapemask_long.dtype == bool
4.8. 5. 用 portfolio(mask=...) 聚合组合曲线,并与 benchmark 对比
5.1 Qué pretende resolver esta sección
No estamos haciendo una prueba retrospectiva de operaciones, solo una “agregación aproximada” orientada a la investigación: para cada día, agregue las acciones que cumplen con los criterios (largo/corto) en una única curva de cartera, luego extraiga 000300.SH como punto de referencia para comparar.
5.2 Los principios mínimos necesarios
Algunos puntos clave de HistoryPanel.portfolio() (que usamos):
mask=: sigue las mismas reglas de forma quewhere(); Los puntos de la cuadrícula que son falsos no participan en la agregación.benchmark=+benchmark_output='tag_along': agrega la fila de referencia a la salidaEl resultado sigue siendo un
HistoryPanely la línea de tiempo permanece sin cambios.
Para reiterar: esta es una agregación orientada a la investigación; no incluye costos de transacción, no tiene restricciones de capital y no ejecuta reequilibrio.
Se puede considerar como una “canasta de igual peso según una convención de investigación”: cada día, las acciones que cumplen con los criterios se promedian con el mismo peso en una sola curva. Su propósito no es simular el comercio real, sino responder una pregunta más fundamental:
Si utilizo este criterio para definir un “conjunto de tenencias”, ¿exhibe alguna diferencia sistemática de desempeño durante el período de la muestra?
Sólo cuando la respuesta a esta pregunta sea «sí», vale la pena seguir invirtiendo esfuerzos y actualizarla para convertirla en una verdadera prueba retrospectiva de la estrategia comercial.
5.3 Código ejecutable + resultados esperados
benchmark = '000300.SH'
pf_long = hp.portfolio(
htypes='close',
mode='equal',
mask=mask_long,
benchmark=benchmark,
benchmark_output='tag_along',
new_share_name='LONG',
)
pf_short = hp.portfolio(
htypes='close',
mode='equal',
mask=mask_short,
benchmark=benchmark,
benchmark_output='tag_along',
new_share_name='SHORT',
)
print('pf_long.shares:', pf_long.shares)
print('pf_long.htypes:', pf_long.htypes)
print('pf_long.shape:', pf_long.shape)
Se esperaba: pf_long.shares contendrá dos filas: LONG y 000300.SH.
4.9. 6. normalize / cum_return + CAGR:把曲线变成可比较的年化摘要
6.1 Qué pretende resolver esta sección
Con dos curvas una al lado de la otra, el ojo puede ver la tendencia, pero es difícil resumirla rápidamente: «Durante este período de investigación, ¿cuánto más fuerte es el largo que el punto de referencia, exactamente?» Entonces hacemos dos cosas:
normalize: alinear el punto inicial (para una comparación visual más sencilla)cum_return + CAGR: producir una tabla resumen anualizada reutilizable
6.2 El principio de necesidad mínima
normalize(base_index=0)escala el punto de referencia a 1.0 (convención de investigación), lo cual es ideal para trazar y comparar en el mismo gráficocum_return(method='simple')genera rendimientos acumuladoscumret_*La esencia de la CAGR es una “tasa de crecimiento anualizada equivalente”:
[ \text{CAGR}=(1+R)^{1/T}-1 ]
Donde (R) es el rendimiento acumulado durante el intervalo (valor final) y (T) es el número de años.
Agreguemos una oración más aquí sobre «por qué calcular CAGR»: muy a menudo comparamos períodos de diferentes duraciones (medio año, un año, dos años). Si sólo nos fijamos en el rendimiento acumulado, es fácil concluir que «cuanto más largo es el período, más impresionante parece». CAGR lo convierte en “crecimiento anual equivalente”, lo que hace que los resultados de diferentes horizontes de investigación sean más fáciles de comparar uno al lado del otro.
6.3 Código ejecutable + resultados esperados
import pandas as pd
def _years_between(hdates) -> float:
idx = pd.DatetimeIndex(hdates)
days = (idx[-1] - idx[0]).days
return max(1e-9, days / 365.25)
def _cagr_from_cumret(cumret_end: float, years: float) -> float:
return (1.0 + cumret_end) ** (1.0 / years) - 1.0
years = _years_between(pf_long.hdates)
cr_long = pf_long.cum_return(htypes='close', method='simple') # 输出列 cumret_close
cr_short = pf_short.cum_return(htypes='close', method='simple')
cumret_long_end = float(cr_long.values[cr_long.shares.index('LONG'), -1, 0])
cumret_short_end = float(cr_short.values[cr_short.shares.index('SHORT'), -1, 0])
# benchmark 行同样在 shares 里(tag_along)
cumret_bm_long_end = float(cr_long.values[cr_long.shares.index('000300.SH'), -1, 0])
cumret_bm_short_end = float(cr_short.values[cr_short.shares.index('000300.SH'), -1, 0])
summary = pd.DataFrame(
{
'cum_return_end': [cumret_long_end, cumret_short_end, cumret_bm_long_end],
'CAGR': [
_cagr_from_cumret(cumret_long_end, years),
_cagr_from_cumret(cumret_short_end, years),
_cagr_from_cumret(cumret_bm_long_end, years),
],
},
index=['LONG', 'SHORT', '000300.SH'],
)
print(summary)
Debería poder ver una tabla de resumen de 3 filas. Se recomienda prestar atención al menos a dos cosas:
LONG frente a 000300.SH: ¿realmente obtuvo un rendimiento superior (mayor CAGR y/o mayor rentabilidad acumulada)?
Rendimiento CORTO: No necesariamente tiene que “perder dinero”, pero nos ayuda a juzgar si la condición realmente separa la muestra (es decir, si la diferencia entre los extremos LARGO y CORTO es obvia).
4.10. 7. 可视化解释:用 plot(highlight=...) 把“触发点”标回图上
7.1 Qué pretende resolver esta sección
Ya tenemos la curva de retorno, pero todavía nos falta la última pieza: explicación. En este paso, haremos algo «fácil de revisar»: en el gráfico de velas de una sola acción, resaltaremos los puntos de activación para que los lectores puedan ver de un vistazo «cuándo se activó la condición de tiempo».
7.2 El principio de necesidad mínima
HistoryPanel.plot(highlight=...) admite dos usos comunes:
Taquigrafía: ⟦CÓDIGO0⟧
Explícito: ⟦CÓDIGO0⟧
Una cosa a tener en cuenta: en la ruta de representación estática, condition está más orientado hacia un eje de tiempo 1D (utilizado para marcadores de dispersión en el gráfico). Entonces, aquí es más sólido usar un bool 1D en un eje de tiempo de stock único para resaltar.
7.3 Código ejecutable + resultado esperado
import numpy as np
primary_share = '000001.SZ'
# 从 cond_long(M,L,1 或 M,L,N 的 bool)里抽出该 share 的时间轴 1D 条件
si = hp_macd.shares.index(primary_share)
cond_1d = np.asarray(cond_long[si, :, 0], dtype=bool).ravel()
# 为了让图更清爽,这里只看最后 200 个交易日(你也可以改成全区间)
hp_one = hp.loc[-200:]
fig = hp_one.plot(
shares=[primary_share],
interactive=True,
highlight={'condition': cond_1d[-200:], 'style': {'marker': 'x', 's': 50}},
)
fig
Resultado esperado: verá una serie de puntos resaltados en el gráfico (correspondientes a los momentos en que macd_hist_12_26_9 > 0). El valor de este paso es: cuando vea que un segmento de la curva de la cartera se deteriora repentinamente, puede volver al gráfico de una sola acción para confirmar rápidamente si los puntos de activación están agrupados en un rango entrecortado, si está recibiendo «latigazos» de un lado a otro, y luego decidir si agregará condiciones de filtrado a continuación (por ejemplo, filtros de tendencia, filtros de volatilidad, etc.).
4.11. 8. 完整代码(单函数可跑版本)
A continuación se muestra una versión completa “ejecutable con una sola función” que puede copiar en una computadora portátil y ejecutar con un solo clic. Hace tres cosas:
Ejecute el ciclo de investigación completo de
MACD -> mask -> portfolio -> CAGR;Trazar las curvas normalizadas de
LONG/SHORT/benchmark;Trace gráficos de acciones individuales y resalte los puntos desencadenantes (para una explicación y revisión más sencillas).
import numpy as np
import pandas as pd
import qteasy as qt
def demo_vertical_timing(
stocks: list,
benchmark: str = '000300.SH',
start: str = '20220101',
end: str = '20221231',
primary_share: str = '000001.SZ',
):
\"\"\"演示纵向择时研究闭环:MACD 阈值 -> mask -> 组合曲线 -> CAGR -> 高亮解释。
Parameters
----------
stocks : list
个股代码列表(建议 3~15 只,太多不利于解释)。
benchmark : str, default '000300.SH'
基准指数代码(示例使用沪深 300)。
start : str, default '20220101'
起始日期(YYYYMMDD)。
end : str, default '20221231'
结束日期(YYYYMMDD)。
primary_share : str, default '000001.SZ'
用于做“触发点高亮解释”的单只股票代码。
Returns
-------
dict
结果对象集合,便于你在 Notebook 里继续查看:
- hp: 原始面板
- hp_macd: 含 MACD 列的面板
- pf_long/pf_short: 组合面板(含 benchmark 行)
- summary: CAGR 摘要表(DataFrame)
- fig_pf: 组合对比图
- fig_one: 单股高亮图
\"\"\"
shares = list(stocks) + [benchmark]
hp = qt.get_kline(
shares=shares,
start=start,
end=end,
freq='D',
as_panel=True,
)
required = {'open', 'high', 'low', 'close'}
missing = [c for c in required if c not in set(hp.htypes)]
if missing:
raise ValueError(f'Missing required OHLC columns in htypes: {missing}')
if benchmark not in hp.shares:
raise ValueError(f'Benchmark {benchmark} not found in shares: {hp.shares}')
if hp.shape[1] < 50:
raise ValueError(
'Not enough data points loaded (too few hdates). '
'Please check your local datasource and date range.'
)
if primary_share not in hp.shares:
raise ValueError(f'primary_share "{primary_share}" not found in shares')
# 1) 派生因子:MACD(默认 12_26_9)
hp_macd = hp.kline.macd(price_htype='close', fastperiod=12, slowperiod=26, signalperiod=9)
factor_col = 'macd_hist_12_26_9'
if factor_col not in hp_macd.htypes:
raise ValueError(f'Required factor htype "{factor_col}" not found after macd()')
# 2) 条件 -> mask(研究口径)
cond_long = (hp_macd[factor_col] > 0.0)
cond_short = (hp_macd[factor_col] <= 0.0)
mask_long = hp.where(cond_long)
mask_short = hp.where(cond_short)
# 3) 组合聚合 + benchmark
pf_long = hp.portfolio(
htypes='close',
mode='equal',
mask=mask_long,
benchmark=benchmark,
benchmark_output='tag_along',
new_share_name='LONG',
)
pf_short = hp.portfolio(
htypes='close',
mode='equal',
mask=mask_short,
benchmark=benchmark,
benchmark_output='tag_along',
new_share_name='SHORT',
)
# 4) cum_return -> CAGR 摘要
def _years_between(hdates) -> float:
idx = pd.DatetimeIndex(hdates)
days = (idx[-1] - idx[0]).days
return max(1e-9, days / 365.25)
def _cagr_from_cumret(cumret_end: float, years: float) -> float:
return (1.0 + cumret_end) ** (1.0 / years) - 1.0
years = _years_between(pf_long.hdates)
cr_long = pf_long.cum_return(htypes='close', method='simple')
cr_short = pf_short.cum_return(htypes='close', method='simple')
cumret_long_end = float(cr_long.values[cr_long.shares.index('LONG'), -1, 0])
cumret_short_end = float(cr_short.values[cr_short.shares.index('SHORT'), -1, 0])
cumret_bm_end = float(cr_long.values[cr_long.shares.index(benchmark), -1, 0])
summary = pd.DataFrame(
{
'cum_return_end': [cumret_long_end, cumret_short_end, cumret_bm_end],
'CAGR': [
_cagr_from_cumret(cumret_long_end, years),
_cagr_from_cumret(cumret_short_end, years),
_cagr_from_cumret(cumret_bm_end, years),
],
},
index=['LONG', 'SHORT', benchmark],
)
print('\\n[CAGR summary]')
print(summary)
# 5) 图:组合曲线对比(先 normalize,便于肉眼比较)
pf_view = pf_long.normalize(htypes='close', base_index=0)
fig_pf = pf_view.plot(interactive=True)
# 6) 图:单股触发点解释(1D 时间轴条件)
si = hp_macd.shares.index(primary_share)
cond_1d = np.asarray(cond_long[si, :, 0], dtype=bool).ravel()
lookback = min(200, len(cond_1d))
fig_one = hp.loc[-lookback:].plot(
shares=[primary_share],
interactive=True,
highlight={'condition': cond_1d[-lookback:], 'style': {'marker': 'x', 's': 50}},
)
return {
'hp': hp,
'hp_macd': hp_macd,
'pf_long': pf_long,
'pf_short': pf_short,
'summary': summary,
'fig_pf': fig_pf,
'fig_one': fig_one,
}
res = demo_vertical_timing(
stocks=['000001.SZ', '600519.SH', '300750.SZ'],
benchmark='000300.SH',
start='20220101',
end='20221231',
primary_share='000001.SZ',
)
res['fig_pf']
Nota: el
normalizeanterior es sólo para hacer la comparación visual más intuitiva; para las estadísticas aún debe confiar encum_returno la serie de precios brutos.
4.12. 9. 小结与边界
En este punto, ya hemos recorrido un ciclo de investigación completo para “cronometraje en un pequeño conjunto de instrumentos”: datos -> factor -> condición bool -> where máscara -> portfolio + benchmark -> cum_return + CAGR -> plot(highlight) explicación.
Nota: el portfolio/cum_return aquí son todos cálculos orientados a la investigación y no incluyen semántica de backtesting completa, como costos de transacción, deslizamiento, liquidación, restricciones de capital, etc. Si desea migrar la lógica de investigación a una prueba retrospectiva de estrategia real, se recomienda generar el factor/condición como columnas o señales de datos utilizables por la estrategia, y entregárselos al Operator/Backtester para manejar la semántica de la capa comercial.
4.13. Apéndice: índice de figuras (recomendado generar/captura de pantalla en el Notebook)
Figura |
Ubicación sugerida |
lo que veras |
|---|---|---|
|
§0 Inicio mínimo ejecutable |
Demostrar que el oleoducto |
|
§0.5 o §6 |
Comparación de curvas normalizadas de |
|
§7 |
Resalte los puntos de activación en un gráfico de una sola acción (para una revisión y explicación más sencilla) |