Skip to content

🇬🇧 English · 🇪🇸 Español

Investigación de carteras y análisis técnico y fundamental

Estas incorporaciones experimentales de 6.1.0, aún no publicada, amplían puremacro.asset_pricing. Funcionan sin conexión con datos proporcionados por el usuario y NumPy/SciPy/pandas. Siguen disponibles las pruebas de riesgo y valoración de activos. No requieren intermediario, suscripción de datos ni nuevas dependencias de ejecución. Los ejemplos usan datos sintéticos; sus rendimientos no acreditan rentabilidad de inversión.

El plan de hitos registra alcance y evidencia. Los cuadernos ejecutables son notebooks/investment_research.py y notebooks/investment_research_es.py, con sus archivos .ipynb ejecutados.

Evaluación histórica y momento de ejecución

Un rendimiento fechado t corresponde al intervalo que termina en el cierre t. La política observa hasta ese cierre y decide. execution_lag=2, el valor predeterminado, ejecuta al cierre siguiente: las nuevas posiciones reciben por primera vez el rendimiento fechado t+2. execution_lag=1 supone explícitamente que la decisión podía ejecutarse en su propio cierre. Los rezagos cuentan filas; el índice proporcionado define el calendario de negociación.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from puremacro import asset_pricing as ap

rng = np.random.default_rng(20261101)
dates = pd.date_range("2020-01-01", periods=300, freq="B")
returns = pd.DataFrame(
    rng.normal([.0002, .0003, .0001], [.008, .014, .006], (300, 3)),
    index=dates, columns=["equity_a", "equity_b", "bond"])
prices = 100 * (1 + returns).cumprod()

def minimum_variance(history):
    covariance = ap.estimate_covariance(history, method="ledoit_wolf")
    return ap.optimize_portfolio(covariance, max_weight=.7).as_series()

policies = {
    "equal_weight": lambda history: ap.equal_weight(history.columns),
    "minimum_variance": minimum_variance,
}
targets = {name: ap.walk_forward_weights(
    returns, policy, min_history=60, window=120, rebalance_every=20)
    for name, policy in policies.items()}
targets["momentum"] = ap.walk_forward_weights(
    prices, lambda history: ap.momentum_weights(history, lookback=40, top_n=2),
    min_history=60, window=120, rebalance_every=20)
runs = {name: ap.backtest_portfolio(returns, weights, cost_bps=15)
        for name, weights in targets.items()}
wealth = pd.DataFrame({name: run.ledger.wealth for name, run in runs.items()})
ax = wealth.plot(title="Trayectorias sintéticas después de costes", ylabel="riqueza")
plt.show()
print(runs["minimum_variance"].ledger.tail(2))

La cartera comienza en efectivo. Una fila de objetivos contiene todos los pesos o solo NaN (sin rebalanceo); los pesos son no negativos y suman como máximo uno. El resto es efectivo. Las posiciones cambian de peso entre rebalanceos. Compras y ventas pagan cost_bps sobre el importe del activo; la contrapartida de efectivo no paga comisión. Con pesos actuales v, objetivos después de costes w y tasa de coste c, se resuelve q + c * sum(abs(q*w - v)) = 1. La riqueza después de negociar es q veces la anterior, sin financiar simultáneamente posiciones y comisiones con el mismo efectivo.

ledger informa rendimientos netos, riqueza, rotación, coste, peso del efectivo y residuo contable. Rotación y coste son fracciones de la riqueza previa a negociar. weights contiene pesos después de costes y antes del rendimiento; trades, importes con signo divididos por la riqueza previa. gross_return describe esas mismas posiciones después de costes, no otra estrategia simulada sin comisiones.

Se admiten rendimientos ausentes únicamente sin exposición al activo. La ausencia de un rendimiento de una posición, exposición no elegible o agotamiento de riqueza produce un error. El marco booleano eligibility debe conocerse al inicio de cada intervalo. Deben proporcionarse rendimientos reales de exclusión de cotización, ajustes corporativos y decisiones oportunas de liquidación: el motor no inventa precios ni reconstruye un universo sin sesgo de supervivencia. Los rendimientos de activos y efectivo deben usar una moneda común de cartera; no se convierten divisas. Incluya dividendos y otras distribuciones en los rendimientos totales proporcionados cuando formen parte de la evaluación. walk_forward_weights restringe el historial recibido por la función, pero no inspecciona su estado externo ni certifica objetivos proporcionados aparte.

record = runs["minimum_variance"].to_json()
replayed = ap.replay_backtest(record)
pd.testing.assert_frame_equal(replayed.ledger, runs["minimum_variance"].ledger)
print("Huella de las entradas:", replayed.input_sha256)

La repetición conserva entradas fechadas, calendario/zona horaria, objetivos, elegibilidad y costes. Los calendarios festivos personalizados conservan sus fechas observadas exactas, no una regla de frecuencia pandas reutilizable. No guarda funciones ejecutables. La huella detecta cambios accidentales, sin autenticar datos ni fijar la versión del código. Registre también la versión de PureMacro y el entorno. Los atributos del resultado son inmutables, pero sus tablas pandas pueden modificarse; eso no cambia las entradas guardadas.

Asignación y comparación de estrategias

estimate_covariance ofrece covarianza muestral (divisor T−1) y contracción de Ledoit–Wolf hacia trace(S)/N * I (divisor T). No elimina observaciones. Las medias son descriptivas: el optimizador solo las usa cuando se pasan expresamente como expected_returns.

optimize_portfolio minimiza risk_aversion/2 * w' Sigma w - return_reward * mu'w + turnover_penalty * ||w-w_prev||1 con inversión completa, pesos no negativos, límites por activo y un rendimiento mínimo opcional. La penalización L1 usa restricciones lineales auxiliares. Un programa lineal independiente sobre el gradiente acota la diferencia respecto al óptimo convexo. Problemas inviables o soluciones sin certificar generan errores. optimality_gap está en unidades del objetivo; la aceptación compara la brecha normalizada y la violación de restricciones con tolerance. No hay sustitución silenciosa por pesos iguales ni regularización oculta de la covarianza. Una varianza final negativa genera un error, incluso si un autovalor negativo pequeño queda dentro de la tolerancia de redondeo de la descomposición.

Use pesos reales previos a negociar en previous_weights para penalizar cambios desde las posiciones actuales. Esa preferencia no sustituye las comisiones del registro histórico. Momentos, objetivos y penalizaciones deben compartir unidades temporales. efficient_frontier recibe rendimientos mínimos explícitos; los inferiores al rendimiento de mínima varianza repiten esa cartera.

covariance = ap.estimate_covariance(returns.iloc[:150])
frontier = ap.efficient_frontier(
    covariance, expected_returns=np.array([.0002, .0003, .0001]),
    targets=[.00012, .00018, .00024], max_weight=.7)
print(frontier.round(6))

# La familia candidata y sus parámetros se fijaron antes de esta muestra.
net = pd.DataFrame({name: run.ledger.net_return for name, run in runs.items()})
evaluation = net.iloc[150:]
comparison = ap.compare_strategies(
    evaluation[["minimum_variance", "momentum"]], evaluation.equal_weight,
    block_length=10, n_boot=499, seed=7)
print(comparison.to_dataframe())
print("Valor p global de la prueba del máximo:", comparison.global_pvalue)
print(ap.risk_summary(evaluation, periods_per_year=252).summary())

Se usan bloques circulares conjuntos que preservan dependencia entre estrategias. El máximo centrado, unilateral y sin studentizar contrasta si algún candidato mejora la media aritmética del rendimiento neto del referente. La corrección de Monte Carlo es (exceedances+1)/(B+1). Los valores p cubren la familia incluida; los intervalos son puntuales, no simultáneos. Estacionariedad y dependencia débil apropiada son supuestos, no garantías comprobadas. Incluya todas las configuraciones exploradas en la muestra de evaluación. No se corrigen búsquedas omitidas, cambios de régimen ni costes ausentes. La división ilustrativa es sintética, no una confirmación empírica independiente.

Indicadores técnicos

indicators = ap.technical_indicators(prices)
print(indicators["equity_a"][["rsi", "macd", "momentum"]].tail(3))

Las columnas son (asset, indicator) y conservan los NaN iniciales. SMA y EMA utilizan datos hasta el cierre actual. EMA comienza con una media simple y sigue con alpha=2/(span+1). MACD resta las EMA rápida y lenta inicializadas por separado; su señal empieza con la primera media completa de valores MACD válidos. Otros proveedores pueden inicializar de otra forma. RSI usa alpha=1/period de Wilder y asigna 50 a una serie constante. Las bandas de Bollinger usan desviación poblacional (ddof=0). Los máximos/mínimos de ruptura excluyen el cierre actual. momentum_weights exige precios positivos completos en la ventana final, resuelve empates por nombre de activo y conserva efectivo sin momentum positivo. Son observaciones, no pruebas de predictibilidad; el evaluador aplica el rezago.

Fundamentos disponibles en la fecha de valoración

facts = pd.DataFrame({
    "asset": ["example", "example"], "metric": ["free_cash_flow"] * 2,
    "unit": ["USD_millions"] * 2, "period_type": ["annual"] * 2,
    "period_start": ["2019-01-01"] * 2, "period_end": ["2019-12-31"] * 2,
    "available_at": ["2020-03-01", "2021-03-01"], "value": [100., 80.],
    "source": ["original_filing", "restatement"],
})
historical = ap.fundamentals_asof(facts, "2020-04-01")
print(historical[["asset", "value", "available_at", "source"]])

# El crecimiento previsto es un supuesto explícito del analista.
cash_flows = historical.value.iloc[0] * 1.04 ** np.arange(1, 6)
valuation = ap.discounted_cash_flow(
    cash_flows, .10, terminal_growth=.02, kind="firm",
    net_debt=200, shares_outstanding=10)
print(valuation.summary())
sensitivity = ap.valuation_sensitivity(
    cash_flows, [.08, .10, .12], [.01, .02, .03],
    net_debt=200, shares_outstanding=10)
print(sensitivity)
print("Valor por acción con dividendos:", ap.dividend_discount(2., .10, .03).value_per_share)

Cada dato identifica activo, métrica, unidad, tipo de periodo, fin de periodo, disponibilidad y valor; los flujos también requieren period_start. Los tipos son instant, quarterly, annual y ttm. Primero se selecciona la última revisión disponible de cada intervalo exacto y después el periodo más reciente por activo/métrica/unidad/tipo. Una revisión posterior de un año antiguo no desplaza al año más reciente. Las horas sin zona significan UTC; una fecha sola significa medianoche. Unidades y tipos permanecen separados; duraciones ambiguas generan errores. Se preserva la procedencia proporcionada, pero no se verifica la fuente, normalizan taxonomías SEC, convierten acumulados anuales ni construyen flujos TTM.

DCF descuenta flujos al final de cada periodo. kind="firm" corresponde a FCFF descontado al WACC, seguido de la resta de deuda neta. "equity" representa FCFE al coste del capital propio; "dividend", dividendos totales a ese mismo coste. En esos dos últimos casos no se resta deuda de nuevo. Los vectores de tasas se capitalizan periodo a periodo. El crecimiento terminal usa permanentemente la última tasa y exige -1 < g < r. Flujos, tasas y acciones deben ser coherentes en moneda, inflación, escala y periodo. El ejemplo usa dinero y acciones en millones.

terminal_fraction muestra la dependencia del valor terminal; las cuadrículas son escenarios, no intervalos probabilísticos. Las combinaciones inválidas de tasa y crecimiento conservan su estado. Se mantienen los valores de capital propio negativos. fundamental_ratios calcula múltiplos, márgenes y rendimiento del flujo libre con entradas comparables explícitas; denominadores no positivos producen NaN y un estado. Capex es una salida positiva. EV es capitalización + deuda − efectivo, sin ajustes inferidos de minoritarios o preferentes; ROE usa el capital contable proporcionado, no su promedio.

Chartistas y fundamentalistas en un modelo económico

beliefs = ap.simulate_brock_hommes(
    300, slopes=[0., .9, .9, 1.01], biases=[0., .2, -.2, 0.],
    names=["fundamentalist", "optimist", "pessimist", "trend"],
    intensity=90.5, gross_rate=1.01, risk_scale=1., seed=7)
fig, axes = plt.subplots(2, 1, figsize=(8, 5), sharex=True)
axes[0].plot(beliefs.deviations)
axes[0].set_ylabel("desviación del valor fundamental")
axes[1].plot(beliefs.fractions, label=beliefs.names)
axes[1].set(xlabel="periodo", ylabel="fracción de agentes")
axes[1].legend(ncol=2)
fig.tight_layout()
plt.show()
print(beliefs.to_dataframe().tail(2))

Es el modelo de reglas afines de Brock–Hommes con oferta externa nula, valor fundamental y dividendo esperado constantes y rendimiento bruto libre de riesgo R > 1:

f[h,t] = g[h] * x[t-1] + b[h]
n[h,t] = softmax(intensity * U[h,t-1])
R*x[t] = sum_h n[h,t]*f[h,t] + shock[t]
profit[h,t] = (x[t]-R*x[t-1]) * (g[h]*x[t-2]+b[h]-R*x[t-1]) / risk_scale - cost[h]
U[h,t] = profit[h,t] + memory*U[h,t-1]

Las fracciones determinan el precio actual; la aptitud se actualiza después. Las desviaciones iniciales son x[-3], x[-2], x[-1]; la aptitud inicial predeterminada es su beneficio realizado con aptitud acumulada anterior cero. initial_fitness permite cambiarla. risk_scale es riesgo percibido por aversión al riesgo, no la varianza del choque simulado. Los choques explícitos entran en el lado derecho de la ecuación de equilibrio; noise_std y seed generan una alternativa reproducible.

Los parámetros ilustran la familia de cuatro tipos descrita por Hommes; no se afirma que esta inicialización reproduzca una trayectoria caótica digitalizada de una figura. Costes y memoria son extensiones explícitas. Pendiente y sesgo nulos definen al fundamentalista, no previsión perfecta. Una divergencia genera un error con el periodo correspondiente; no se recortan precios. Los precios implícitos negativos se señalan en la tabla y no deben introducirse silenciosamente en estrategias que exigen precios positivos. Una simulación acotada no prueba estabilidad global ni ajuste empírico.

Validación y fuentes

Las pruebas comparan contabilidad con cálculos manuales y posiciones monetarias independientes; asignación con soluciones analíticas y una malla densa; covarianza con productos exteriores explícitos; indicadores con fórmulas escalares; DCF con valores presentes independientes; y creencias con una recurrencia escalar. Cambios en datos futuros, revisiones y choques futuros comprueban el uso de información. El registro de validación distingue esas pruebas de replicación empírica, ejecución en navegador y publicación.