
Portfolio
- 181 installs
- 173 repo stars
- Updated June 14, 2026
- gauss314/skills
For development and infrastructure management.
About
portfolio is an AI coding tool that enhances development workflows. Builders use it for infrastructure, integration, and platform development within the catalog ecosystem.
- portfolio
- Development
Portfolio by the numbers
- 181 all-time installs (skills.sh)
- Ranked #2,188 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gauss314/skills --skill portfolioAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 181 |
|---|---|
| repo stars | ★ 173 |
| Last updated | June 14, 2026 |
| Repository | gauss314/skills ↗ |
What it does
For development and infrastructure management.
Files
Portfolio — Optimización Cuantitativa de Portafolios
Este skill implementa 3 enfoques de optimización de portafolios desde el material del curso (notebook Clase_08_teoria_2025_portafolio.ipynb y PDF Portafolios 2025 Ucema.pdf):
1. Markowitz / Media-Varianza — Optimización convexa vía scipy.optimize + simulación Monte Carlo + frontera eficiente + CML. 2. Black-Litterman — Combinación bayesiana de retornos de equilibrio de mercado (CAPM inverso) con views del inversor, incluyendo matriz de incertidumbre Ω (método Idzorek). 3. HRP / HERC / NCO — Construcción jerárquica de portafolios mediante clustering (single/complete/average/ward), risk parity y NCO con restricciones.
Todos los scripts usan solo numpy, pandas y scipy. Sin dependencias pesadas. Este skill es autónomo: funciona sin skills/backtesting.
Para ratios de performance post-optimización (Sharpe, Sortino, VaR, drawdowns, etc.) consultar el skill hermana: `skills/backtesting`.
Part of the Gauss314 Skills Repository.
---
File Map
skills/portfolio/
├── SKILL.md ← Este archivo
├── references/
│ ├── PORTFOLIO_THEORY.md ← MPT, Markowitz, frontera eficiente (ES)
│ ├── BLACK_LITTERMAN.md ← BL: prior, views, posterior, omega (ES)
│ ├── HIERARCHICAL.md ← HRP, HERC, NCO, clustering (ES)
│ └── RISK_MEASURES.md ← VaR, CVaR, MAD, MSV, DR, MDD (ES)
├── assets/
│ ├── sample_prices.csv ← Precios multi-activo para ejemplos
│ ├── sample_returns.csv ← Retornos multi-activo
│ ├── sample_mcaps.json ← Market caps para Black-Litterman
│ └── defaults.json ← Parámetros default
├── scripts/
│ ├── __init__.py
│ ├── portfolio.py ← Core: Markowitz, Sharpe, Monte Carlo, frontera
│ ├── black_litterman.py ← BL: prior, posterior, omega, views
│ ├── hierarchical.py ← HRP/HERC/NCO: clustering, risk parity, constraints
│ ├── risk_measures.py ← VaR, CVaR, MAD, MSV, MDD, DR
│ ├── covariance.py ← Covarianza: hist, ledoit-wolf, oas, ewma
│ └── cli.py ← CLI unificada (12 modos)
└── tests/
├── __init__.py
└── test_portfolio.py ← Tests + validación contra notebookQué hace cada script
| Script | Rol | Funciones clave |
|---|---|---|
portfolio.py | Core de optimización Markowitz | max_sharpe_optim, min_variance_optim, random_portfolios, efficient_frontier, cml_portfolio, asset_stats |
black_litterman.py | Black-Litterman completo | market_implied_risk_aversion, market_implied_prior_returns, bl_posterior_returns, omega_idzorek |
hierarchical.py | HRP / HERC / NCO | hrp_portfolio, herc_portfolio, nco_portfolio, nco_with_constraints, hrp_constraints |
risk_measures.py | Medidas de riesgo | var_historic, cvar, max_drawdown, cdar, diversification_ratio, risk_contribution |
covariance.py | Estimación de covarianza | cov_hist, cov_ledoit_wolf, cov_oas, cov_ewma |
---
Quick Start
Markowitz (scipy.optimize)
# Max Sharpe con 3 activos
py scripts/cli.py markowitz --assets assets/sample_returns.csv
# Con tasa libre de riesgo personalizada
py scripts/cli.py markowitz --assets assets/sample_returns.csv --rf 0.05
# Estadísticas individuales
py scripts/cli.py stats --assets assets/sample_returns.csvMonte Carlo
# Simular 10.000 carteras aleatorias
py scripts/cli.py montecarlo --assets assets/sample_returns.csv
# Guardar frontera a CSV
py scripts/cli.py montecarlo --assets assets/sample_returns.csv --save frontier.csvFrontera Eficiente
py scripts/cli.py frontier --assets assets/sample_returns.csv --n 50CML — Leverage y Deleverage
El portafolio tangente (máximo Sharpe) se combina con el activo libre de riesgo para obtener cualquier punto sobre la Capital Market Line (CML), manteniendo el mismo Sharpe ratio.
# Portafolio tangente puro (w=1)
py scripts/cli.py cml --assets assets/sample_returns.csv --weight 1.0
# Deleverage: 60% en tangencia, 40% en Rf (menos riesgo, mismo Sharpe)
py scripts/cli.py cml --assets assets/sample_returns.csv --weight 0.6
# Leverage: pide prestado 50% a Rf, invierte 150% en tangencia (más riesgo, mismo Sharpe)
py scripts/cli.py cml --assets assets/sample_returns.csv --weight 1.5Black-Litterman
# Prior: retornos implícitos de mercado (CAPM inverso)
py scripts/cli.py bl-prior --assets assets/sample_returns.csv --market-prices assets/sample_prices.csv --mcaps assets/sample_mcaps.json
# BL completo con views + optimización
py scripts/cli.py bl --assets assets/sample_returns.csv --market-prices assets/sample_prices.csv --mcaps assets/sample_mcaps.json --views '{"BMA": 0.25, "LOMA": 0.4, "MELI": -0.1}' --confidences "0.3,0.5,0.8" --optimizeHRP / HERC / NCO
# Hierarchical Risk Parity
py scripts/cli.py hrp --assets assets/sample_returns.csv
# Nested Clustered Optimization
py scripts/cli.py nco --assets assets/sample_returns.csv --clusters 3
# NCO con restricciones
py scripts/cli.py nco-con --assets assets/sample_returns.csv --constraints assets/sample_constraints.csv --classes assets/sample_classes.csvRiesgo
# Todas las medidas de riesgo
py scripts/cli.py risk --prices assets/sample_prices.csv
# Medida específica
py scripts/cli.py risk --prices assets/sample_prices.csv --measure var---
Usar como Librería
from scripts.portfolio import *
from scripts.black_litterman import *
from scripts.hierarchical import *
import numpy as np
# --- Markowitz ---
rets = pd.read_csv('assets/sample_returns.csv', index_col=0)
result = max_sharpe_optim(rets, rf=0.045)
print(result['weights'], result['sharpe']) # pesos óptimos, Sharpe
# --- CML: leverage/deleverage ---
# 60% en tangencia, 40% en Rf (deleverage)
cml = cml_portfolio(rets, rf=0.045, weight_tangency=0.6)
print(cml['ret'], cml['vol'], cml['sharpe']) # mismo Sharpe que el tangente
# Leverage: 150% en tangencia (pide prestado 50% a Rf)
cml2 = cml_portfolio(rets, rf=0.045, weight_tangency=1.5)
print(cml2['ret'], cml2['vol'], cml2['sharpe']) # mismo Sharpe
# --- Monte Carlo ---
port_df = random_portfolios(rets, n_portfolios=10000, rf=0.045)
best = port_df.loc[port_df['sharpe'].idxmax()]
print(best['weights']) # mejor combinación Monte Carlo
# --- Black-Litterman ---
import json
with open('assets/sample_mcaps.json') as f:
mcaps = json.load(f)
spy = pd.read_csv('assets/sample_prices.csv')['SPY'].pct_change().dropna()
bl_result = bl_pipeline(rets, spy.values, mcaps,
view_dict={'BMA': 0.25, 'LOMA': 0.4},
view_confidences=[0.3, 0.5], rf=0.045)
print(bl_result['posterior']) # retornos a posteriori
# --- HRP ---
hrp_result = hrp_portfolio(rets, linkage_method='ward')
print(hrp_result['weights']) # pesos HRP---
Dependencias
| Librería | Requerida | Uso |
|---|---|---|
numpy | ✅ | Cómputo vectorizado, álgebra lineal |
pandas | ✅ | CSV I/O, DataFrames |
scipy | ✅ | optimize (Markowitz), cluster.hierarchy (HRP/NCO), stats |
No requiere Riskfolio-Lib, PyPortfolioOpt, sklearn, cvxpy ni arch.
Para visualización (dendrogramas, frontera eficiente) se puede usar matplotlib opcionalmente. Ejemplos de plots están en el notebook de referencia.
---
Referencias Teóricas
- Markowitz (1952): "Portfolio Selection", Journal of Finance.
- Black & Litterman (1992): "Global Portfolio Optimization", Financial Analysts Journal.
- Idzorek (2005): "A Step-by-Step Guide to the Black-Litterman Model".
- Lopez de Prado (2016): "Building Diversified Portfolios that Outperform Out of Sample" (HRP).
- De Prado (2019): "Nested Clustered Optimization", SSRN 3469961.
- Pfitzinger & Katzke (2019): "NCO with Constraints", SSRN 4409173.
- Meucci (2006): "Beyond Black-Litterman: Views on Non-Normal Markets", SSRN 1213325.
- Avramov (2004): "Bayesian Variable Selection in Portfolio Analysis", SSRN 3326617.
Para profundizar en ratios de performance (30+ métricas: Sharpe, Sortino, VaR, cVaR, Kelly, Rachev, Profit Factor, etc.) y backtesting de estrategias: `skills/backtesting`.
---
Notebook de referencia
El contenido teórico y ejemplos numéricos de este skill están basados en:
temp/Clase_08_teoria_2025_portafolio.ipynb— Implementaciones en Python de
Markowitz, Monte Carlo, NCO (Riskfolio-Lib), Black-Litterman (PyPortfolioOpt).
temp/Portafolios 2025 Ucema.pdf— Marco teórico: MPT, CAPM, Fama-French,
clustering, NCO, Black-Litterman.
Las implementaciones flat numpy en scripts/ replican los resultados de esos notebooks sin depender de las librerías mencionadas.
{
"rf": 0.045,
"tau": 0.05,
"n_portfolios": 10000,
"linkage": "ward",
"n_clusters": 4,
"var_confidence": 0.95
}
Assets,Industry
GGAL,ADR Banco
BBAR,ADR Banco
BMA,ADR Banco
SUPV,ADR Banco
LOMA,ADR Otros
CRESY,ADR Otros
TEO,ADR Otros
MELI,Marketplaces
BTC-USD,Cryptos
ETH-USD,Cryptos
BABA,Marketplaces
KO,USA Conservador
VZ,USA Conservador
SQQQ,Shorts
SH,Shorts
SDOW,Shorts
Disabled,Type,Set,Position,Sign,Weight
False,Assets,,TEO,>=,0.05
False,Assets,,SQQQ,<=,0.03
False,All Assets,,,<=,0.15
False,Each asset in a class,Industry,Cryptos,>=,0.02
False,Each asset in a class,Industry,ADR Bancos,<=,0.06
{
"AAPL": 3000000000000,
"GGAL": 8500000000,
"NVDA": 1200000000000,
"BABA": 390000000000,
"KO": 310000000000,
"VZ": 170000000000,
"MELI": 105000000000,
"BTC-USD": 1700000000000,
"ETH-USD": 340000000000,
"LOMA": 1600000000,
"CRESY": 770000000,
"TEO": 6100000000,
"BMA": 5900000000,
"BBAR": 3700000000,
"SUPV": 1000000000,
"SH": 10000000000,
"SDOW": 10000000000,
"SQQQ": 10000000000
}
date,SPY
2023-01-02,99.40613089233098
2023-01-03,98.77184348141851
2023-01-04,96.18543390925005
2023-01-05,95.74865477999974
2023-01-06,96.84570238060807
2023-01-09,95.05023422000421
2023-01-10,96.59555955998019
2023-01-11,97.34383437452226
2023-01-12,96.3161389746172
2023-01-13,96.15033448391321
2023-01-16,98.52277876257226
2023-01-17,99.65268545012216
2023-01-18,99.70569033783559
2023-01-19,99.39059586540323
2023-01-20,99.54468474266503
2023-01-23,98.82298139070247
2023-01-24,95.79006177440678
2023-01-25,98.80928805080964
2023-01-26,99.07939356424296
2023-01-27,99.71276400657358
2023-01-30,99.66604793202248
2023-01-31,98.61252921211555
2023-02-01,98.37723812564377
2023-02-02,96.08358944613688
2023-02-03,99.46191762614272
2023-02-06,97.67612449564518
2023-02-07,96.21426766901277
2023-02-08,95.61921285733318
2023-02-09,97.52364944964275
2023-02-10,97.58390207294072
2023-02-13,95.82949370671767
2023-02-14,97.68866955372198
2023-02-15,98.80523507219424
2023-02-16,99.64631975531671
2023-02-17,100.7621841379531
2023-02-20,104.48866079674843
2023-02-21,108.01336611826476
2023-02-22,106.94923147582334
2023-02-23,109.63016059507773
2023-02-24,110.30619033708682
2023-02-27,111.1795525159747
2023-02-28,108.4362477753809
2023-03-01,108.20273417156687
2023-03-02,106.93426307155572
2023-03-03,108.31408100506809
2023-03-06,110.11236799054309
2023-03-07,106.78143441079284
2023-03-08,106.78885352650194
2023-03-09,108.64454256276512
2023-03-10,108.3840883169413
2023-03-13,109.61971570575469
2023-03-14,110.24274731549733
2023-03-15,110.8535135122366
2023-03-16,110.44745715204496
2023-03-17,108.49360106944945
2023-03-20,106.726367163211
2023-03-21,107.91118529563832
2023-03-22,107.98587205493688
2023-03-23,107.80485780567805
2023-03-24,107.16442850859356
2023-03-27,106.7176473950225
2023-03-28,108.45010594696349
2023-03-29,107.94928710092886
2023-03-30,105.84180590596817
2023-03-31,107.07187724561027
2023-04-03,106.78604027420484
2023-04-04,106.87908480485942
2023-04-05,109.53146083142045
2023-04-06,109.54149345696666
2023-04-07,110.06170557162982
2023-04-10,108.1757604485815
2023-04-11,107.86303998974394
2023-04-12,108.89729221772777
2023-04-13,106.42862351972168
2023-04-14,106.7924504297939
2023-04-17,106.99681859443442
2023-04-18,107.1059373213649
2023-04-19,108.82877728694147
2023-04-20,109.41985618482755
2023-04-21,111.91766340475553
2023-04-24,110.30614209149599
2023-04-25,110.57549258942598
2023-04-26,110.75997585839818
2023-04-27,109.04129491711068
2023-04-28,110.66095726787701
2023-05-01,114.63362665545094
2023-05-02,114.6502392898444
2023-05-03,115.5682671300492
2023-05-04,115.76117005859194
2023-05-05,114.5745175775561
2023-05-08,111.53227553348802
2023-05-09,110.40190768337352
2023-05-10,108.16393984577412
2023-05-11,111.00881015084911
2023-05-12,110.86163846642552
2023-05-15,110.33201450895353
2023-05-16,111.85813771707005
2023-05-17,110.03573474729448
2023-05-18,108.82076965911453
2023-05-19,108.97552305070326
2023-05-22,110.71450187499647
2023-05-23,111.59543101607944
2023-05-24,112.05669914130112
2023-05-25,112.23296454197829
2023-05-26,112.95395856907365
2023-05-29,113.34146574815706
2023-05-30,111.64849503081007
2023-05-31,111.77039105870155
2023-06-01,110.02871281127065
2023-06-02,110.20668920116992
2023-06-05,109.45876207937502
2023-06-06,113.7540430676313
2023-06-07,113.73147471217932
2023-06-08,111.40548813450314
2023-06-09,111.22596894621924
2023-06-12,112.14069288490019
2023-06-13,115.03710501397339
2023-06-14,114.07007386932915
2023-06-15,111.43890717308014
2023-06-16,112.85785180242269
2023-06-19,110.5889414933097
2023-06-20,109.91174943905276
2023-06-21,109.53164015285914
2023-06-22,109.4610695575589
2023-06-23,110.64273686759489
2023-06-26,109.55776117668943
2023-06-27,109.15249822428964
2023-06-28,109.1727964405693
2023-06-29,109.46182789638979
2023-06-30,109.66642872508652
2023-07-03,110.43017039187065
2023-07-04,111.88157789437811
2023-07-05,112.58747594148531
2023-07-06,113.40146003230807
2023-07-07,110.78605608373829
2023-07-10,110.91391215803617
2023-07-11,111.75924367042715
2023-07-12,113.03314173482934
2023-07-13,111.91009254475586
2023-07-14,110.85357814066099
2023-07-17,109.94703001939119
2023-07-18,110.87959834799197
2023-07-19,112.65759984816606
2023-07-20,112.01594500150279
2023-07-21,113.59740491309547
2023-07-24,113.8535369849573
2023-07-25,113.68694476899417
2023-07-26,113.3723811299584
2023-07-27,114.69506288120246
2023-07-28,115.30267723134291
2023-07-31,119.35449663590609
2023-08-01,117.88559950432031
2023-08-02,118.85285126933563
2023-08-03,117.96030491647377
2023-08-04,116.47875129504699
2023-08-07,116.50635920859635
2023-08-08,116.79335120844637
2023-08-09,122.06805871582043
2023-08-10,121.78005140321085
2023-08-11,119.78085672959871
2023-08-14,120.3205029263677
2023-08-15,116.68050108762094
2023-08-16,116.56628577357809
2023-08-17,116.64095630860005
2023-08-18,118.0867439685202
2023-08-21,119.87191605313805
2023-08-22,119.01746680728765
2023-08-23,118.7963938909005
2023-08-24,117.48600728480592
2023-08-25,115.56097367046276
2023-08-28,116.14584930375177
2023-08-29,113.99367218384076
2023-08-30,113.33222349308176
2023-08-31,111.69767897769007
2023-09-01,112.66751930497524
2023-09-04,115.79977545614494
2023-09-05,113.6062822584524
2023-09-06,110.53625084305905
2023-09-07,110.3735173294511
2023-09-08,110.44367834083613
2023-09-11,110.85084759377197
2023-09-12,110.15525443111649
2023-09-13,109.86816918795937
2023-09-14,110.5197061539997
2023-09-15,112.30296958123964
2023-09-18,109.1010561246369
2023-09-19,108.54235674453876
2023-09-20,108.93459033690378
2023-09-21,107.48786654863396
2023-09-22,110.35975356748642
2023-09-25,109.35662911086268
2023-09-26,109.33263995822631
2023-09-27,110.29243180936636
2023-09-28,109.36227963901206
2023-09-29,106.41722381358743
2023-10-02,105.7774780457708
2023-10-03,107.46650656560757
2023-10-04,106.96758079055408
2023-10-05,105.66030631187975
2023-10-06,107.1805048019799
2023-10-09,106.69270113312004
2023-10-10,105.92172079815867
2023-10-11,106.20191913630244
2023-10-12,107.47985479513954
2023-10-13,111.43362651149687
2023-10-16,112.87010873624017
2023-10-17,114.89690831056967
2023-10-18,114.07382013300045
2023-10-19,112.37682829686423
2023-10-20,112.52694819929468
2023-10-23,112.75307843890923
2023-10-24,110.98805580001299
2023-10-25,110.74089265368696
2023-10-26,111.45076637323751
2023-10-27,112.09972053970638
2023-10-30,112.99608305940258
2023-10-31,113.53573417000835
2023-11-01,112.72063443424626
2023-11-02,111.89981774479475
2023-11-03,109.94934014948272
2023-11-06,112.9778002222337
2023-11-07,113.04580569070488
2023-11-08,112.71024261754803
2023-11-09,111.19533390077964
2023-11-10,116.24770335044117
2023-11-13,114.4368757713463
2023-11-14,113.74858847915874
2023-11-15,114.50611198186553
2023-11-16,115.58791344640068
2023-11-17,117.30769759338398
2023-11-20,120.58012916459455
2023-11-21,121.54771103141344
2023-11-22,119.68537289821937
2023-11-23,119.27267163258412
2023-11-24,119.87262441206174
2023-11-27,123.69923640501605
2023-11-28,121.18108828945017
2023-11-29,123.43623066505516
2023-11-30,123.81931766389475
2023-12-01,125.43539503063397
2023-12-04,125.29374301790726
2023-12-05,123.76992953046101
2023-12-06,125.22192045962406
2023-12-07,126.50780328222582
2023-12-08,128.4172652495495
2023-12-11,128.4430065618778
2023-12-12,127.34347290296968
2023-12-13,124.45026298269522
2023-12-14,126.03967533446728
2023-12-15,126.89437953429297
date,GGAL,AAPL,NVDA,SPY
2023-01-03,-0.002262722199336653,0.01903615955136906,0.03944647179920713,-0.026184766609417376
2023-01-04,0.013544679968924722,0.04387852146501059,-0.027097445623984973,-0.018837773963538695
2023-01-05,0.031444861196133544,0.02137453679458079,0.011828798117860417,0.021807211907830748
2023-01-06,-0.004174330654144853,-0.029445187471290546,-0.012434891164565531,-0.018318123480362214
2023-01-09,-0.0041740036692723415,-0.009142631108750021,-0.009199926986321771,0.05458533397225995
2023-01-10,0.032604505085367386,0.026174923526997018,-0.011283734174943882,0.010420274852910083
2023-01-11,0.015974951262380577,-0.013560604548040867,-0.016639818426078756,0.004205541045709582
2023-01-12,-0.00885009304200346,0.009420484606958102,0.0014715141749344074,-0.016529027033805543
2023-01-13,0.011415870212255541,0.016121248457154458,-0.01598978641895732,0.01461192307877468
2023-01-16,-0.008730023953437471,-0.01787688758882222,0.005926629902740066,-0.0109523381160882
2023-01-17,-0.008775860421902149,-0.0006902687760566018,-0.000504634817980687,0.0029445229093556513
2023-01-18,0.0053535246042335505,-0.062300127756762524,-0.004269819227630811,0.0530615576662945
2023-01-19,-0.03706137745785587,-0.01978932195669858,-0.01749640208048653,-0.0014201885708768902
2023-01-20,-0.03342690694615802,-0.004541021270803247,-0.010974759658095579,0.023763421799142037
2023-01-23,-0.01068822125666924,-0.024159046804200446,0.01573026277851275,-0.013471958322238797
2023-01-24,-0.019562739266928975,0.03370374973942125,0.010573855991686365,-0.00019974985727944095
2023-01-25,0.006808016548932594,-0.027711616380516668,-0.01887077954517402,0.0365687842346365
2023-01-26,-0.017505449196837386,-0.008266532479735966,0.0024897403772714544,-0.01196715825660033
2023-01-27,-0.0273646871846166,0.003119667611805932,0.01564892427467801,0.03743256265542172
2023-01-30,0.030261831604863954,0.029759691483430828,-0.0323531722288779,0.01476295029396546
2023-01-31,-0.0040074745656961674,-0.027822854843614353,0.011432056005540758,-0.010691767866820134
2023-02-01,0.0018522774442162326,0.02404787150695209,-0.01267150691470198,0.013234971845653254
2023-02-02,-0.02760673597048846,0.0007049095524360727,0.011983203476058746,0.02015144217168463
2023-02-03,-0.0103338891339525,-0.01894835252619087,-0.01465671233436694,0.013020233841583462
2023-02-06,0.0027221501347702848,0.0097896779201605,-0.034971497696269616,-0.03043183215396894
2023-02-07,-0.022268192044696544,0.00449124947560553,-0.03154266402522776,-0.013944604112998027
2023-02-08,0.008046158100276646,-0.011438415689940418,0.0014627677358058921,-0.004440484476611273
2023-02-09,-0.011446755412568677,0.001897840323439537,0.0057106942343176925,-0.000988180010243478
2023-02-10,-0.005319675142595237,-0.007180369018125465,-0.017432595499218273,0.012997180505289085
2023-02-13,-0.011467869148391818,0.0027741878621208027,0.013360311083638887,0.004062248673774249
2023-02-14,0.03825930292930635,0.013837477263798581,-0.03220055805505617,-0.0258664669742108
2023-02-15,0.00023008197003848707,0.03274503148079444,-0.0008212585553883933,0.008136882963188263
2023-02-16,-0.020442381158813472,-0.023964489943742673,-0.023441208369846644,0.012792852187097914
2023-02-17,0.017095379927977827,0.04410563521000754,-0.012458464826338722,0.011764472362461476
2023-02-20,-0.023633131166121313,-0.03780847335921245,0.0014490222460099567,0.022361977518081
2023-02-21,0.0046882274102826305,-0.0025324897242613442,-0.016569458368319467,0.01732684107543192
2023-02-22,-0.03795437526115364,0.012341884280024917,-0.007165316713335312,0.009730639363494387
2023-02-23,-0.025726993997994607,0.006138601818283629,0.020840039190694215,-0.000902906363694167
2023-02-24,0.004447083775871219,-0.011882825304411537,-0.01097714397659677,-0.032189735504642436
2023-02-27,0.015386503462707868,-0.0036557464356421487,0.017362854218622603,0.009133825492254388
2023-02-28,0.003935087830170714,-0.009316350070788348,-0.021851849306577575,0.0046645992728282515
2023-03-01,-0.0018113232182509487,-0.011223832620921192,0.011157873423826237,0.005949203379254975
2023-03-02,-0.005506855287317647,0.01764592363164641,0.02976577391277102,-0.024724195441858443
2023-03-03,-0.028651959512093317,0.007669571360800154,-0.04775496728406359,-0.02089964183149995
2023-03-06,-0.013800768225617821,-0.013269367212042305,-0.015319351509446855,0.021797219841395377
2023-03-07,-0.008674929186444413,0.01866403328685773,0.012114232585167528,-0.0002910607107433627
2023-03-08,0.02187834094494079,0.006668124017910948,-0.0035545752076898696,0.014230314453586912
2023-03-09,0.007399608586850626,0.01689843251272727,0.007954386832850657,0.001066936294611187
2023-03-10,-0.03416358630141314,0.013178659895374478,-0.01151291700378232,0.0010957226558330202
2023-03-13,0.007006108129555821,-0.015951308791288477,0.002234288059355549,0.019452456814941232
2023-03-14,-0.007175775897340753,-0.010646540890222655,-0.0026101323732820036,-0.009772827063308398
2023-03-15,-0.012953807770637371,0.015565776130296527,0.02414246327579339,0.0024253519579380267
2023-03-16,0.012814942322390888,0.012788487467458198,0.0056040611963283915,-0.008707375076852708
2023-03-17,0.02134459588289439,8.197148019539746e-05,0.0072784130620258125,-0.008156478485782825
2023-03-20,0.019309668300115268,0.0028506029299042446,-0.007707681623069451,-0.005667322264099828
2023-03-21,-0.01615247722312485,0.02639565176463643,-0.009209455272747302,0.004954910602743112
2023-03-22,-0.005668122748964666,-0.01126746895851305,-0.008118033100015132,-0.009033919150598368
2023-03-23,0.007150713753131477,0.011507657081693656,0.00842432947216376,0.02594600895335919
2023-03-24,0.020212462875803805,-0.0035375810127079577,-0.007888411500052372,-0.01724177568877694
2023-03-27,-0.009042354538688735,-0.00384620838413563,0.006315355430772218,-0.0032321980480565227
2023-03-28,-0.003208022796571952,0.022730014854717817,0.04290283859815758,-0.008260315711119404
2023-03-29,-0.02139451919150437,0.01715379210901724,0.018084065778085856,0.029877185431526065
2023-03-30,-0.023151917102505415,0.01691160178173501,-0.006002383925028387,0.0044409273505781055
2023-03-31,0.01689159294917797,0.026966772154518592,0.024827472020812813,0.021361857099124082
2023-04-03,0.028009903325221464,0.0009205002331877932,-0.007632232922220483,-0.0287886842729409
2023-04-04,-0.0009397605797879516,0.014239488693528868,-0.03946272604657208,0.005858097250764249
2023-04-05,0.020783692186185032,-0.005689090615537906,-0.01946969509327956,0.01846095067205389
2023-04-06,0.0077626951962930235,0.007007767336492776,-0.036242756732248416,0.0021479834036939316
2023-04-07,-0.012325802361519034,-0.0021006516239800987,-0.006508993807362695,0.02204917544867646
2023-04-10,0.007757849491086821,0.0024428983253170777,0.0008687447240787183,-0.00979745810104804
2023-04-11,0.031754479536790337,0.012480378456686703,0.0346143475786862,0.029102382311424924
2023-04-12,-0.00021649734326623093,-0.01573923668155275,0.007063376269262012,0.047574992851319386
2023-04-13,0.03230366531862794,0.043257203691351,-0.0038744853139857094,-0.006733995555705197
2023-04-14,-0.05057135542638913,-0.019429121293986995,0.017234948599599065,-0.008374784884827235
2023-04-17,0.01708231221328327,-0.023503167360840904,-0.04278064831428541,0.03000915396051096
2023-04-18,0.0022434541505147454,0.02394438894325801,0.005225898783773664,0.032611926032181104
2023-04-19,-0.005465158396129266,0.016467370663285852,0.016044658978471515,-0.009907791748812955
2023-04-20,0.002337944270148773,0.01306703351628058,-0.028653207798669866,-0.007872583945307543
2023-04-21,-0.038491023719774975,0.013152655323821705,0.02365041923357647,-0.005122527057117576
2023-04-24,-0.003885868155078187,0.00025509707478810206,0.007296418232296764,-0.026043862986485755
2023-04-25,0.007671527965724234,-0.017293802893404542,-0.0077753724597205975,-0.017714263509690964
2023-04-26,0.030514179310170864,0.002018124842128932,0.013242553446253025,-0.019392317527884617
2023-04-27,-0.009816900896978842,-0.01295853987995954,0.04698421687423293,-0.014746146083693956
2023-04-28,-0.015547738384551923,0.020203783073545756,0.004145895646300746,-0.0001936789895379265
2023-05-01,-0.009489825559636178,-0.0024381704522364878,0.0054793688553225195,0.005197756359343941
2023-05-02,0.018986027683403073,-0.015882465994997985,-0.008649593143174261,0.03201170583280022
2023-05-03,0.007100109291738432,-0.005910182582764323,-0.01636155893055191,-0.019278820800567087
2023-05-04,-0.01004441854312288,0.008797098107182544,0.01725387413403845,0.020391572228770194
2023-05-05,0.010823503526625533,-0.010716654134824366,-0.016484298656296303,-0.003772642519461966
2023-05-08,0.0024445339998035998,-0.015817968730733267,0.001933190953247177,-0.0004891545179495083
2023-05-09,0.020071680483322085,0.005388208691194052,-0.009012292568364555,0.01409479789078727
2023-05-10,-0.013449794117726843,0.0054139340817811465,0.010130566757094783,-0.021715195730273584
2023-05-11,-0.006034958967986337,-0.00959255855768748,0.007199031434380165,0.008181481811866398
2023-05-12,-0.007315275228620743,-0.008881094134141954,0.02147820510973819,0.0038363843194697544
2023-05-15,-0.0283603745346257,0.005154236356334163,-0.009653431555052938,0.010402761655343262
2023-05-16,0.006443073409880817,-0.028060468482656087,-0.004885525513286915,0.006303154675952882
2023-05-17,0.005737502221664581,-0.027270532950263604,-0.018894492582492317,0.050856980091423365
2023-05-18,0.0006024505333190877,-0.013773154514147179,-0.008350801914387529,-0.012180015427783797
2023-05-19,-0.004182969576668194,-0.0037618494829678717,0.008078465987278793,-0.010068904816042279
2023-05-22,-0.02742434759689616,0.0067407687111535886,0.015762713660816274,-0.011891540589971217
2023-05-23,-0.00788168182449589,0.030461875287082663,-0.017783283900618452,-0.010553459700844647
2023-05-24,-0.0063341445210927105,0.017809931019682868,0.018053141265692352,-0.012173044220349927
2023-05-25,-0.015425337100998582,-0.002695132191706695,0.027997522670118125,0.02457749808351939
2023-05-26,-0.0027220028471044433,0.00011968300338560667,0.008807255715078233,0.02933203788250993
2023-05-29,0.008617939599035562,-0.019360713952770725,0.03876854088032489,-0.010855574207515706
2023-05-30,0.03896364177366318,0.00012974569639712108,-0.014864204619701615,-0.016017445705033384
2023-05-31,0.003999533127156285,-0.005259294008593929,-0.02409798694215115,0.009977760322162244
2023-06-01,0.00566700487809646,0.006978608999829161,-0.03446642691949131,-0.01048906294272478
2023-06-02,-0.0009884294967533735,-0.015916589615094456,0.030888329360128974,0.013245592198648515
2023-06-05,-0.037167121005516446,0.010946408559647347,0.013680040160548135,0.00456886600285511
2023-06-06,-3.0277050625193702e-05,0.031645167774013494,-0.0006115063719304148,-0.02937480317737906
2023-06-07,0.0017060578624141787,-0.0016738005998370165,0.006118011575235727,0.03194988416707978
2023-06-08,0.05102391089498082,0.008570754838013617,-0.021769333016879777,0.03708879611195193
2023-06-09,-0.003341623602194699,0.01440565543108363,0.05065632423313482,-0.011686944678006794
2023-06-12,0.0065523199836354795,-0.007496171936915985,0.0030891853685697246,-0.0072277842182219
2023-06-13,-0.0001942165316319855,0.004994279682074998,0.0026915115230152775,0.006236675389983182
2023-06-14,-0.02261394408153783,0.0007521307242019937,0.015128628933742672,0.007215039674798973
2023-06-15,0.02363135435829733,0.002456534319124737,0.01017156688961185,0.01376475929974319
2023-06-16,0.015660013379384496,-0.01484884790280172,0.004990089718279078,0.041543857505672355
2023-06-19,0.016454548070899966,0.0009906938985062563,-0.015192894639425747,-0.0030343316318762747
2023-06-20,-0.01753223908572621,0.010514862506284084,0.009978826874381674,-0.015346961352450617
2023-06-21,0.028967514331246003,0.029962992676629163,0.03887717440759264,-0.026722838226867718
2023-06-22,-0.027161333822384726,0.0198804520163276,0.02778746641217844,-0.014019400758596645
2023-06-23,0.012312322047147273,0.04452647541882504,0.0328931338224856,-0.0001625262486542045
2023-06-26,0.04530542194301024,-0.014737278715437396,-0.00967718527882755,0.03706142127872791
2023-06-27,-0.019125468826861836,0.018108417281817335,-0.019107194868545063,-0.009803851797143825
2023-06-28,-0.010767564843850885,0.004175533463442349,-0.0020137081657113365,0.004988158679107846
2023-06-29,0.002496137478364524,0.045291776769959435,0.0016158022497432256,0.0001715567927285111
2023-06-30,-0.009523870998874528,-0.015543892761199829,0.022636227990920244,0.02456472664765097
2023-07-03,-0.030052437885077055,-0.016162400632232288,-0.03279933544897595,0.05236356478887316
2023-07-04,0.0018730113947535987,-0.011422119479426396,0.03157937992935134,-0.010066366984222808
2023-07-05,-0.020532354972713884,-0.041109042061884926,-0.0026566228866856667,-0.009245781316856716
2023-07-06,0.010021733170954672,-0.009965116319745615,-0.00800540608933975,0.02161347683707282
2023-07-07,-0.01772943553733697,-0.014575388695820979,-0.01954848860436631,0.01423824155157205
2023-07-10,0.03200002171429128,0.003514035526085646,-0.032071572950192584,0.038143629454506556
2023-07-11,-0.015050655313563532,0.007362087401928674,0.017108107366746728,0.01225302438116449
2023-07-12,-0.005923616115851571,0.03875555690853627,0.0019682938961029794,-0.006663541302261544
2023-07-13,0.016911755973389342,0.019700010580079086,-0.024981874591792907,0.012389214884331867
2023-07-14,-0.023828788494058895,-0.010977377111791364,-0.025081669849872257,0.022933082277685868
2023-07-17,0.005061967377295806,-0.017316607306112175,-0.006196416521559223,0.01705342091199813
2023-07-18,0.02700094909142492,0.0103920091578944,0.034460909365334524,0.010702345355990683
2023-07-19,-0.031154056426632493,-0.02557201688998645,-0.004680837600392951,0.022073588703731994
2023-07-20,0.00420147873798582,0.03782707382114481,-0.029130152245231855,0.02417346512699292
2023-07-21,0.005713918397656892,0.024381281382752196,-0.004405130107512201,0.02854294047297712
2023-07-24,0.016267353181599553,-0.008844171224152042,-0.004942218245862717,0.013565383847201185
2023-07-25,-0.023947608522821073,-0.03319909162233858,-0.052035033765506666,-0.002838325921117657
2023-07-26,-0.025576370742659726,0.027961225034236037,-0.0005857257260313276,0.0034401776032606524
2023-07-27,0.010998879078638835,-0.0017891943850135705,-0.004110220430691869,0.024936007905928648
2023-07-28,0.006460472871012257,0.025577969379633858,0.014528656998953116,-0.015713940613809707
2023-07-31,0.005525064186000961,-0.030901046563721057,0.03819032141306811,0.00790454342061353
2023-08-01,0.007456627404988758,-0.011421771055283858,0.023298568895876004,-0.00733970805962425
2023-08-02,-0.013015056454593998,0.0006050569675282169,-0.0048658967914060325,0.0010754743669401101
2023-08-03,0.0051583325652715395,0.001440648613911355,-0.021398256179944508,0.02641180667818932
2023-08-04,0.006381726459848247,-0.008465275482786083,0.0533411882107353,0.004331334591813896
2023-08-07,-0.013692422561695339,0.0130413042764006,0.0016857880260039426,0.0014297520854507084
2023-08-08,0.038539594490369256,-0.020636500454763285,0.0007788890148905381,-0.026343904917364935
2023-08-09,0.01002659119075755,-0.0023448362667906064,1.7498410895377958e-05,0.01554465178776332
2023-08-10,-0.023056120203718722,0.002910138891076386,0.004471663396936831,0.01349999666852808
2023-08-11,0.013724398802183568,0.01084718539618712,-0.0023843611228951955,0.04473691157958659
2023-08-14,-0.018814390965241845,0.014841352743838243,-0.010913253753539354,-0.00563960209970904
2023-08-15,0.016374305337383932,-0.021752762520434787,-0.01038290047563939,0.0048949478583570905
2023-08-16,0.02395431522106173,-0.029731346455504126,-0.00015505338230148435,0.00550275853636939
2023-08-17,-0.015787693305927553,0.02639589658281638,-0.01031492787216759,0.03256816763402082
2023-08-18,0.019964193822967324,0.007171875834995456,-0.013662721723435967,-0.0014049228183518059
2023-08-21,0.00879406108089631,-0.0143655472848907,0.0026320623638629304,0.006098953857299971
2023-08-22,0.017085519193174692,0.0320251526760289,-0.004588982643525474,0.012738380880764844
2023-08-23,0.03918407259341383,0.002817454270829245,0.03105222635641769,0.004241150794736193
2023-08-24,-0.004398062392613866,0.024378352954211646,-0.05116408290340113,-0.008393250621791637
2023-08-25,-0.014469026133031804,0.001852082618474693,0.02258132071955976,0.004391413980221248
2023-08-28,-0.01714166934203265,0.04259725290460037,0.02574759103900015,0.02221581114915594
2023-08-29,-0.015691786329035562,0.0362483309979198,-0.0401399675182027,-0.019831032132563764
2023-08-30,-0.0010414914591877489,-0.004469265943566558,-0.0063336094826336176,0.0031643896267365967
2023-08-31,0.0073499185220615715,0.020131376017255898,-0.006904868409653542,-0.013411667577334918
2023-09-01,0.006052056121699678,0.013497802817270976,-0.027271465220377267,0.024701071585669476
2023-09-04,0.017189736925804278,0.028264707187807492,-0.014943553885362615,-0.029519275610362494
2023-09-05,0.0007603267395028812,-0.01862287998029022,-0.02147751847264101,-0.010621624838954569
2023-09-06,0.0300122357533259,0.014322629082722083,0.03618469903450405,0.008076679311308732
2023-09-07,-0.004781667916291754,0.021904956330196068,0.019399336308780546,0.032321841732821044
2023-09-08,0.056438540172606855,-0.03408050787523331,0.0262702379679578,-0.0008146731949115749
2023-09-11,0.013098389048485481,-0.02289891758900453,0.015045502238818287,-0.010547966427472688
2023-09-12,-0.0165054190457804,-0.03948400445061184,-0.021839033849510248,0.03885915156099706
2023-09-13,-0.020700589250346013,-0.004876209191073677,-0.009940666999461945,-0.02805909919226468
2023-09-14,0.010201128649660163,0.014961666835377141,0.010340589388500243,-0.04254458131596239
2023-09-15,-0.003961388623348827,0.0310184921891552,-0.02365820704859989,0.009343671073124504
2023-09-18,0.014889774334418515,0.0019838608615914133,0.014869434849726915,-0.009495712753629149
2023-09-19,0.010014565959718169,0.03362527892501732,-0.0042972482548490065,-0.01972747215803261
2023-09-20,-0.0009561208780103359,-0.02673806464354689,-0.006971998210723029,0.014775219092575176
2023-09-21,-0.0163015423399977,-0.03301050664765892,0.014828060238211815,0.005390490970849537
2023-09-22,-0.02935739213452193,-0.0006107673835531457,0.009429445945455583,-0.010723659778059003
2023-09-25,-0.00839486371708642,0.008214867341773235,-0.006696799140775367,-0.024793551135493086
2023-09-26,0.017784265660202303,-0.00015388312065967025,0.023969351575432674,0.01811120062216731
2023-09-27,0.004793326292093836,-0.04002577323534584,-0.02089977473832616,0.013595614703934888
2023-09-28,-0.024119145740087555,-0.0012815788657453142,0.01290122401773508,-0.0014824174077924246
2023-09-29,0.0039714840414439845,-0.02526475652197857,0.012438750827968859,0.03814216920917435
2023-10-02,0.00824011196251484,0.013990413494232357,-0.00567476612577289,-0.020684768898318495
2023-10-03,-0.017030462588919715,0.00786271498458846,0.007047377149836942,-0.029564659393526593
2023-10-04,0.0035808982703580128,-0.01813121108035398,-0.024224043344850843,-0.013249602298664787
2023-10-05,0.0016655598755532797,-0.00972969557376191,0.019161815927001502,-0.00041163558192702787
2023-10-06,-0.02211128714289623,-0.020471818243603446,-0.0031929344371746105,0.005381215995824862
2023-10-09,0.00768512736745075,-0.0007532980739004547,-0.00990507876107527,-0.004315383017050589
2023-10-10,0.011784588027336174,0.0197962438500916,0.021712544408968526,0.007569613694657029
2023-10-11,0.02240840438989644,-0.019031098688307768,-0.013494988854726775,-0.024232353950449204
2023-10-12,0.021810486916101546,0.010637106310094735,-0.027289939137297825,0.02981100187718666
2023-10-13,-0.026690722274722023,-0.01005426686143418,-0.03016816012831125,-0.0011423705652359173
2023-10-16,-0.018090860423829902,-0.015240132277094975,0.012700169800244021,0.023108883338611275
2023-10-17,0.010859243523523077,-0.0016392621387728568,-0.024795988247974243,0.007381617735081836
2023-10-18,0.010833986180541322,-0.02000209634600847,0.036237001540091685,0.00968163105337072
2023-10-19,0.010859494602741826,-0.010517288558634319,-0.040303881785864304,0.011966376594819028
2023-10-20,0.08064126607643529,-0.023184568065581335,0.035028670568259646,0.009499003048164534
2023-10-23,0.011989110279363002,0.040596912319883494,0.004731507743289454,0.013444024205147231
2023-10-24,0.023482791716152862,0.0012059976704736908,-0.0014332341748874988,0.027453129821833455
2023-10-25,0.019772981400360212,-0.013403867442076,-0.010344505468518173,0.00444025222969513
2023-10-26,0.013619740053560037,0.004791038713966023,0.008518802523124203,0.014788356666425972
2023-10-27,-0.005788566207982293,-0.0017450366437499198,-0.0002526621240449556,-0.001293876105322611
2023-10-30,0.015802950928536896,-0.0039117211987620415,0.02282257672368937,0.029735882198468566
2023-10-31,-0.014845211322566554,0.012865390092306805,0.002788433441825333,-0.012943350982499546
2023-11-01,-0.004227411368547207,0.015773259230009717,0.003512188559914975,0.03719381205820427
2023-11-02,-0.009165013827740265,-0.01005908846429493,-0.006749364276936287,-0.0003031130648367686
2023-11-03,0.0021397688325639486,-0.010955906878596045,-0.0006387084133078202,-0.027723939555154953
2023-11-06,0.047905249818944284,-0.0049885495930063195,0.006678236010048355,0.003066781279473796
2023-11-07,-0.0361747761333171,-0.044517110934006454,-0.03314173665314879,-0.013035327651775619
2023-11-08,0.014326863490437436,-0.02936406697837124,-0.02611661306048585,0.017463607367290024
2023-11-09,-0.03125544349675613,0.028228568635318307,0.015483934756783002,-0.01247402582035495
2023-11-10,-0.008898806463453646,0.03396337449071196,0.003924991445066972,-0.008388288979356728
2023-11-13,0.022529042484574546,-0.00447069733807659,-0.0031746169404368585,-0.0366040750123916
2023-11-14,0.00178719551555373,0.012103804540033014,0.0008690560718911389,-0.00850971205454576
2023-11-15,-0.020834788727156384,0.006747666699952859,0.00747946662228971,-0.04684484964999924
2023-11-16,-0.013711207424001004,0.0640449286141842,-0.010242379499698195,-0.030697032954724146
2023-11-17,0.014191714628273955,0.023155519338209585,-0.014953168734280431,0.015832316908994892
2023-11-20,-0.014008290504419096,-0.0020562348762156457,0.004426674004770259,0.016348196132989212
2023-11-21,0.004840851034524585,-0.01843869707641721,-0.018886821520723962,0.009049855784678984
2023-11-22,0.0014124333437921521,-0.03113396401395374,0.008702705372974906,-0.01866316826706138
2023-11-23,-0.01245380835563592,0.00457972776184512,-0.03299505722093654,-0.00045412397731792176
2023-11-24,0.04433349884405713,-0.014520559795372034,0.02130693173429976,0.0004280408015193693
2023-11-27,0.013265598009577273,-0.02755822251517248,0.010001634984367236,-0.022412320829296783
2023-11-28,-0.039213300758865866,-0.012354506317385217,0.005636419863668252,0.0310399633660412
2023-11-29,0.004238041500497003,-0.020909265620225215,0.020358279139155,0.01821108146223871
2023-11-30,-0.012654973088295773,0.034835868209067034,0.03438752563919145,-0.00391161310916166
2023-12-01,0.017703549214763692,0.018298192461576823,0.021004964234377033,0.0010382553942418493
2023-12-04,-0.015233197695216805,0.0003406051664391896,-0.03566591636925631,0.004678566634805037
2023-12-05,-0.0017931192666060802,0.03055643319818846,-0.02477936310599882,-0.03953208073569825
2023-12-06,0.010656121898257709,0.002049463438093646,-0.011924701966625428,-0.004433689699381627
2023-12-07,0.017974739412075058,-0.0165865863544169,0.0010223432411613498,-0.013053736158096796
2023-12-08,-0.023231815767952746,0.03144680487043572,0.010912289820036003,-0.019342878810869824
2023-12-11,-0.006170905982641139,0.011342039550578553,-0.01391712507450582,-0.005108910752989804
2023-12-12,-0.008958537249568299,-0.020041370580378026,0.004244316997754183,0.03712631562986246
2023-12-13,-0.01248795484059595,-0.0033013122074569967,-0.014501484415849641,0.013405921481387972
2023-12-14,0.03645795202961866,-0.016868471928146556,-0.011661823664735782,-0.010864134147865578
2023-12-15,0.008636717297652474,-0.02679058574953641,-0.02725491706323191,0.012023362048644293
Modelo Black-Litterman
Problema que resuelve
Los modelos clásicos (Markowitz, NCO) tienen dos limitaciones importantes: 1. Las restricciones son una forma indirecta de introducir una view propia, pero limitan la optimización arbitrariamente. 2. No permiten diferenciar la incertidumbre por activo (a veces estamos más convencidos de ciertas views que de otras).
Black & Litterman (Goldman Sachs) desarrollan un modelo bayesiano que:
- Parte de una condición de equilibrio del mercado (retornos implícitos
del portafolio de mercado vía CAPM inverso).
- Permite al inversor introducir views con incertidumbre.
- Genera un vector de retornos a posteriori combinando ambas fuentes.
El Modelo
Distribución a priori (equilibrio de mercado)
N ~ (Pi, tau * Sigma)Donde:
- Pi: retornos implícitos de equilibrio
- Sigma: matriz de covarianza
- tau: escalar de incertidumbre (típicamente 0.01-0.05)
Pi = delta Sigma w_mkt
- delta: aversión al riesgo implícita del mercado
- w_mkt: ponderaciones del portafolio de mercado (market caps)
Distribución de las Views
N ~ (Q, Omega)Donde:
- Q: vector de retornos esperados según las views (Kx1)
- Omega: matriz de incertidumbre de las views (KxK)
Distribución a posteriori
E(R) = [(tau*Sigma)^-1 + P^T Omega^-1 P]^-1 *
[(tau*Sigma)^-1 * Pi + P^T Omega^-1 * Q]Sigma_post = Sigma + [(tau*Sigma)^-1 + P^T Omega^-1 P]^-1
Donde P es la matriz de mapeo entre views y activos (KxN).
Views
Absolutas
P = [1, 0, 0, ...] → Activo i tendrá retorno Q[i]Ejemplo: BMA: +25%, LOMA: +40%, MELI: -10%
Relativas
P = [1, -1, 0, ...] → Activo i > Activo j en Q[k]Ejemplo: GGAL > SUPV + 11%, GGAL > BBAR + 7%
Matriz Omega (Idzorek)
Omega se construye proporcional a P * Sigma * P^T, escalado por las confidencias del inversor:
Omega[i,i] = (P * Sigma * P^T)[i,i] * (1 - conf_i) / conf_i * tauDonde conf_i está entre 0 (mínima confianza) y 1 (máxima confianza).
Pipeline Completo
1. Calcular delta = (E(Rm) - Rf) / sigma_m^2 2. Calcular Pi = delta Sigma w_mkt 3. Definir views (absolutas vía view_dict, relativas vía view_pairs) 4. Construir Omega vía Idzorek con confidencias 5. Calcular retornos posteriores 6. Usar retornos posteriores en Markowitz / NCO
Extensiones
- Entropy Pooling (Meucci, 2006): generaliza BL usando máxima entropía,
permite views sobre distribuciones completas (no solo medias).
- Avramov (2004): modelo bayesiano de factores con clustering jerárquico.
- Dynamic BL: adapta el modelo a múltiples periodos con views cambiantes.
Métodos Jerárquicos: HRP, HERC, NCO
Motivación
Markowitz tiene un problema grave: las soluciones óptimas son muy inestables con ruido (típico de la bolsa). Clusterizar grupos de activos y construir sub-portafolios permite promediar errores de ruido blanco dentro de clusters de activos similares.
Por ejemplo, en lugar de calcular covarianzas GGAL↔BTC, GGAL↔ETH, BBAR↔BTC, etc. (ruidosas), armamos un sub-portafolio de ADRs y otro de cryptos: la covarianza entre ambos portafolios es más robusta.
Distancia y Clustering
Correlación a Distancia
d = sqrt(2 * (1 - rho))Es una métrica de distancia Euclídea propia.
Métodos de Linkage
| Método | Descripción | Cuándo usar |
|---|---|---|
single | Menor distancia entre elementos | Conservador, evita agrupaciones agresivas |
complete | Mayor distancia entre elementos | Clusters bien separados |
average | Promedio de distancias | Balance |
ward | Minimiza varianza intra-cluster | Recomendado para finanzas |
centroid | Distancia entre centroides | Fácil interpretación |
median | Similar a centroid pero robusto | Con ruido/outliers |
Medidas de Codependencia
| Medida | Descripción |
|---|---|
pearson | Correlación lineal |
spearman | Correlación de rangos (monótona) |
kendall | Concordancia de pares |
distance | Distancia Euclídea entre retornos |
mutual_info | Dependencia total (lineal + no lineal) |
HRP — Hierarchical Risk Parity (Lopez de Prado, 2016)
Algoritmo: 1. Calcular matriz de correlación → distancia → linkage → cuasi-diagonal 2. Recursively bisect el dendrograma 3. En cada división, asignar pesos inversamente proporcionales a la varianza de cada sub-portafolio 4. Sin optimización cuadrática — solo operaciones O(N)
Ventaja: extremadamente robusto, no sufre de inestabilidad de Markowitz.
HERC — Hierarchical Equal Risk Contribution
Similar a HRP pero en cada bisect asigna pesos basados en riesgo igualitario:
alpha = risk_right / (risk_left + risk_right)NCO — Nested Clustered Optimization (De Prado, 2019)
Pipeline: 1. Clusterizar activos en K grupos 2. Intra-cluster: optimizar Markowitz dentro de cada cluster 3. Inter-cluster: optimizar la asignación entre clusters
Esto reduce la dimensionalidad del problema de optimización y promedia ruido dentro de clusters.
NCO con Restricciones (Pfitzinger & Katzke, 2019)
Extensión que permite incluir restricciones de pesos a priori, manteniendo la robustez del NCO base:
- Por activo:
TEO >= 5%,SQQQ <= 3% - Por clase:
Cryptos >= 2%,ADR Bancos <= 6% - Global:
All assets <= 15%
Métodos de Asignación Intra-cluster
| Método | Descripción |
|---|---|
| CHI-MD | Maximiza diversificación relativa |
| CHI-ERC | Maximiza Sharpe |
| HMV | Mínima varianza jerárquica |
| CMV | Mínima varianza por cluster |
| HRP | Hierarchical Risk Parity |
| CEW | Equal Weight por cluster |
Métricas de Diversificación
| Métrica | Descripción |
|---|---|
| DR | Diversification Ratio |
| DD | Diversification Delta |
| RCE | Risk Concentration Equivalent |
| NBE | Effective Number of Bets |
Teoría de Portafolios (MPT)
Supuestos Básicos
1. Dado un nivel de retorno buscado, los inversores prefieren el portafolio menos volátil (la volatilidad como riesgo). 2. Dada una aversión al riesgo (volatilidad tolerada), los inversores buscan el portafolio de máximo retorno para ese nivel de volatilidad. 3. El riesgo es el costo a pagar por el retorno buscado: no existe retorno sin riesgo (o a largo plazo es despreciable).
Fórmulas Base
Portafolio de 2 activos
Retorno esperado:
E(Rp) = w_i * E(Ri) + w_j * E(Rj)Varianza:
V(Rp) = w_i^2 * sigma_i^2 + w_j^2 * sigma_j^2 + 2 * w_i * w_j * rho_ij * sigma_i * sigma_jPortafolio de N activos (notación matricial)
Retorno esperado:
E(Rp) = w^T * muVarianza:
V(Rp) = w^T * Sigma * wMarkowitz (1952)
Harry Markowitz plantea que un inversor racional construye su portafolio maximizando el retorno para una varianza asumida, moviéndose dentro de curvas de indiferencia e isovarianza.
Frontera Eficiente
Conjunto de portafolios que ofrecen el máximo retorno para cada nivel de riesgo (o mínimo riesgo para cada nivel de retorno).
Limitaciones de Markowitz
1. Inestabilidad: las soluciones óptimas son muy sensibles al ruido (típico de mercados financieros). 2. Retornos desconocidos: no conocemos los retornos ni varianzas a posteriori. 3. Supuestos fuertes: normalidad de retornos, homocedasticidad.
Soluciones a las limitaciones
- Clustering → NCO, HRP, HERC (reducir ruido promediando errores dentro
de clusters de activos similares).
- Black-Litterman → Incorporar visión personal con incertidumbre.
- Shrinkage → Ledoit-Wolf, OAS para covarianzas más robustas.
CAPM
E[Ri] = Rf + beta_i * (E[Rm] - Rf)Relaciona el rendimiento esperado de un activo con su riesgo sistemático (beta) respecto al mercado.
Fama-French (3 factores)
Agrega al CAPM los factores:
- SMB (Small Minus Big): tamaño
- HML (High Minus Low): valor libros/precio
Capital Market Line (CML)
Recta tangente desde la tasa libre de riesgo (Rf) al portafolio óptimo sobre la frontera eficiente. El portafolio tangente maximiza el Sharpe ratio.
E(Rp) = Rf + Sharpe_t * sigma_pLeverage y Deleverage sobre la CML
El portafolio tangente (máximo Sharpe) define la pendiente de la CML. Cualquier punto sobre esta recta se obtiene combinando linealmente el activo libre de riesgo (Rf) con el portafolio tangente — sin cambiar el Sharpe ratio.
Sea w el peso asignado al portafolio tangente (y 1-w al activo libre de riesgo):
E(Rp) = (1-w) * Rf + w * E(Rt) = Rf + w * (E(Rt) - Rf)
sigma_p = w * sigma_t
Sharpe_p = (E(Rp) - Rf) / sigma_p = Sharpe_tCasos según w:
| w | Qué significa | Efecto |
|---|---|---|
| 0 < w < 1 | Deleverage: parte en Rf, parte en tangencia | Menor riesgo y retorno, mismo Sharpe |
| w = 1 | Portafolio tangente puro | Riesgo y retorno del tangente |
| w > 1 | Leverage: pide prestado a Rf para invertir más del 100% en tangencia | Mayor riesgo y retorno, mismo Sharpe |
Esto es conceptualmente distinto a moverse sobre la frontera eficiente (solo activos riesgosos, sin Rf). La CML domina a la frontera eficiente porque para cualquier nivel de riesgo ofrece mayor retorno (o menor riesgo para el mismo retorno).
Medidas de Riesgo
Medidas de Dispersión
| Medida | Fórmula | Descripción |
|---|---|---|
| Volatilidad (MV) | sqrt(w^T Sigma w) | Desviación estándar del portafolio |
| MAD | mean(\ | r - mean(r)\ |
| MSV | sqrt(mean(r_neg^2)) | Semi-desviación (solo pérdidas) |
Value at Risk (VaR)
Pérdida máxima esperada con nivel de confianza (1-alpha):
| Método | Descripción |
|---|---|
var_historic | Quantil empírico de los retornos |
var_gaussian | VaR paramétrico asumiendo normalidad |
Conditional VaR (CVaR / Expected Shortfall)
Pérdida promedio en el peor (alpha)% de los casos:
CVaR = mean(r[r <= VaR(alpha)])Siempre es más negativo (peor) que el VaR.
Drawdown
| Medida | Descripción |
|---|---|
| MDD | Máxima caída desde pico: min(P/cummax(P) - 1) |
| CDaR | Promedio del peor alpha% de drawdowns |
| Calmar | Retorno anualizado / \ |
Diversificación
| Medida | Descripción |
|---|---|
| DR = sum(w_i sigma_i) / sigma_p | Diversification Ratio (>=1) |
| Risk Contribution | Contribución marginal al riesgo |
| Risk Contribution % | RC como fracción del riesgo total |
Medidas en el Notebook (Riskfolio-Lib)
| Clave | Descripción |
|---|---|
| 'vol' | Desviación estándar |
| 'MV' | Varianza |
| 'KT' | Raíz cuadrada de Curtosis |
| 'MAD' | Desviación media absoluta |
| 'MSV' | Semi desviación estándar |
| 'SKT' | Raíz cuadrada de semicurtosis |
| 'FLPM' | Omega ratio |
| 'SLPM' | Sortino ratio |
| 'VaR' | Valor en riesgo |
| 'CVaR' | Valor en riesgo condicional |
| 'TG' | Gini de cola |
| 'EVaR' | Valor en riesgo entrópico |
| 'RLVaR' | Valor en riesgo relativista |
| 'WR' | Peor realización (Minimax) |
| 'RG' | Rango de rendimientos |
| 'CVRG' | Rango de CVaR |
| 'MDD' | Calmar |
| 'DaR' | Drawdown al riesgo |
| 'CDaR' | Drawdown condicional |
| 'EDaR' | Drawdown entrópico |
| 'UCI' | Índice de úlcera |
| 'DR' | Diversification Ratio |
| 'DD' | Diversification Delta |
| 'RCE' | Risk Concentration Equivalent |
| 'NBE' | Effective Number of Bets |
"""
Black-Litterman model: prior, views, posterior.
Flat functions, vectorized with numpy. No classes, no objects.
The BL model combines:
1. Market equilibrium returns (prior) via reverse-optimization of CAPM
2. Investor views (absolute or relative) with confidences
3. Bayesian posterior returns for portfolio optimization
References:
- Black & Litterman (1992), "Global Portfolio Optimization"
- Idzorek (2005), "A Step-by-Step Guide to the Black-Litterman Model"
- Meucci (2006), "Beyond Black-Litterman: Views on Non-Normal Markets"
Part of the Gauss314 Skills Repository: https://github.com/gauss314/skills
"""
from __future__ import annotations
import numpy as np
import pandas as pd
try:
from . import covariance as cov_lib
except (ImportError, ValueError):
import covariance as cov_lib
def market_implied_risk_aversion(market_returns, rf=0.0, periods=252):
"""Market-implied risk aversion delta.
delta = (E(Rm) - rf) / sigma_m^2
Parameters
----------
market_returns : array-like
Historical returns of the market portfolio (e.g. SPY).
rf : float
Risk-free rate (annualized).
periods : int
Trading periods per year (252 for daily).
Returns
-------
float : risk aversion coefficient
"""
r = np.asarray(market_returns, dtype=float)
r = r[~np.isnan(r)]
excess = np.mean(r) * periods - rf
var_m = np.var(r, ddof=1) * periods
if var_m == 0:
return 1.0
return excess / var_m
def market_implied_prior_returns(market_caps, delta, cov, rf=0.0):
"""Market-implied equilibrium returns (prior Pi).
Pi = delta * Sigma * w_mkt + rf
where w_mkt = market_caps / sum(market_caps).
Note: Pi is excess return; rf is added to get total return.
Parameters
----------
market_caps : dict or array-like
Market capitalizations per asset (same order as cov columns).
delta : float
Risk aversion coefficient.
cov : (N, N) array-like
Covariance matrix of asset returns.
rf : float
Risk-free rate.
Returns
-------
array : prior expected returns (annualized)
"""
mcaps = np.asarray(list(market_caps.values()) if isinstance(market_caps, dict) else market_caps, dtype=float)
C = np.asarray(cov, dtype=float)
w_mkt = mcaps / mcaps.sum()
return delta * C @ w_mkt + rf
def _to_views_matrix(assets, view_dict):
"""Build P and Q for absolute views.
Parameters
----------
assets : list of str
Asset names in order.
view_dict : dict
{asset_name: expected_return}
Returns
-------
P : (K, N) array
Q : (K,) array
asset_indices : list of int
Indices of viewed assets
"""
N = len(assets)
asset_map = {a: i for i, a in enumerate(assets)}
K = len(view_dict)
P = np.zeros((K, N))
Q = np.zeros(K)
indices = []
for idx, (asset, ret) in enumerate(view_dict.items()):
if asset in asset_map:
P[idx, asset_map[asset]] = 1
Q[idx] = ret
indices.append(asset_map[asset])
return P, Q, indices
def _to_relative_views_matrix(assets, view_pairs):
"""Build P and Q for relative views.
view_pairs: list of (asset_i, asset_j, expected_outperformance)
e.g. [('GGAL', 'SUPV', 0.11)] means GGAL > SUPV by 11%.
"""
N = len(assets)
asset_map = {a: i for i, a in enumerate(assets)}
K = len(view_pairs)
P = np.zeros((K, N))
Q = np.zeros(K)
for idx, (a, b, val) in enumerate(view_pairs):
if a in asset_map and b in asset_map:
P[idx, asset_map[a]] = 1
P[idx, asset_map[b]] = -1
Q[idx] = val
return P, Q
def omega_idzorek(cov, P, view_confidences, tau=0.05):
"""Omega matrix via Idzorek's method.
Maps view confidences (0 to 1) to uncertainty matrix Omega.
Uses the proportionality of Omega to P @ Sigma @ P^T.
Parameters
----------
cov : (N, N) array-like
Covariance matrix.
P : (K, N) array
Views mapping matrix.
view_confidences : list of float
Confidences between 0 and 1 for each view.
tau : float
Scaling parameter (usually 0.01 to 0.05).
Returns
-------
Omega : (K, K) array
"""
C = np.asarray(cov, dtype=float)
P_C_PT = P @ C @ P.T
diag = np.diag(P_C_PT)
omega = np.diag(diag)
for i, conf in enumerate(view_confidences):
if conf > 0:
omega[i, i] = omega[i, i] * (1 - conf) / conf
else:
omega[i, i] = np.inf
return omega * tau
def bl_posterior_returns(prior, P, Q, omega, cov, tau=0.05):
"""Black-Litterman posterior expected returns.
E(R) = [(tau*Sigma)^-1 + P^T Omega^-1 P]^-1 *
[(tau*Sigma)^-1 * Pi + P^T Omega^-1 * Q]
Parameters
----------
prior : (N,) array
Prior expected returns (Pi).
P : (K, N) array
Views mapping matrix.
Q : (K,) array
Views expected returns vector.
omega : (K, K) array
Views uncertainty matrix.
cov : (N, N) array
Covariance matrix (Sigma).
tau : float
Scaling parameter.
Returns
-------
(N,) array : posterior expected returns
"""
pi = np.asarray(prior, dtype=float)
C = np.asarray(cov, dtype=float)
tau_C_inv = np.linalg.inv(tau * C)
omega_inv = np.linalg.inv(omega)
P_T_omega_inv = P.T @ omega_inv
M_inv = np.linalg.inv(tau_C_inv + P_T_omega_inv @ P)
return M_inv @ (tau_C_inv @ pi + P_T_omega_inv @ Q)
def bl_cov(cov, prior, P, omega, tau=0.05):
"""Posterior covariance matrix under BL.
Sigma_post = Sigma + [(tau*Sigma)^-1 + P^T Omega^-1 P]^-1
"""
C = np.asarray(cov, dtype=float)
tau_C_inv = np.linalg.inv(tau * C)
omega_inv = np.linalg.inv(omega)
M_inv = np.linalg.inv(tau_C_inv + P.T @ omega_inv @ P)
return C + M_inv
def bl_pipeline(returns, market_returns, market_caps, view_dict=None,
view_confidences=None, relative_views=None, rf=0.0, tau=0.05,
cov=None):
"""Full Black-Litterman pipeline: risk aversion -> prior -> posterior.
Parameters
----------
returns : (T, N) DataFrame
Historical asset returns.
market_returns : array-like
Market benchmark returns (e.g. SPY).
market_caps : dict
{asset_name: market_cap}
view_dict : dict or None
Absolute views {asset: expected_return}
view_confidences : list or None
Confidences for absolute views.
relative_views : list or None
Relative views [(asset_i, asset_j, outperformance), ...]
rf : float
Risk-free rate.
tau : float
BL scaling parameter.
Returns
-------
dict with 'prior', 'posterior', 'omega', 'delta', 'P', 'Q',
'posterior_cov', 'view_dict'
"""
X = np.asarray(returns, dtype=float)
assets = list(returns.columns) if isinstance(returns, pd.DataFrame) else [f'A{i}' for i in range(X.shape[1])]
if cov is None:
cov = cov_lib.cov_ledoit_wolf(X) * 252
# Ensure cov is numpy array
if hasattr(cov, 'values'):
cov = np.asarray(cov.values, dtype=float)
delta = market_implied_risk_aversion(market_returns, 0.0)
# Filter market_caps to only include assets present in returns
mcaps_filtered = {k: v for k, v in market_caps.items() if k in assets}
prior = market_implied_prior_returns(mcaps_filtered, delta, cov, rf)
if view_dict is None and relative_views is None:
return {
'prior': prior, 'delta': delta, 'cov': cov,
'assets': assets,
'message': 'No views provided. Prior only.'
}
# Combine absolute + relative views
P_list, Q_list, conf_list = [], [], []
if view_dict:
P_abs, Q_abs, _ = _to_views_matrix(assets, view_dict)
P_list.append(P_abs)
Q_list.append(Q_abs)
if view_confidences:
conf_list.extend(view_confidences)
else:
conf_list.extend([0.5] * len(view_dict))
if relative_views:
P_rel, Q_rel = _to_relative_views_matrix(assets, relative_views)
P_list.append(P_rel)
Q_list.append(Q_rel)
conf_list.extend([0.5] * len(relative_views))
P = np.vstack(P_list)
Q = np.concatenate(Q_list)
omega = omega_idzorek(cov, P, conf_list, tau)
posterior = bl_posterior_returns(prior, P, Q, omega, cov, tau)
post_cov = bl_cov(cov, prior, P, omega, tau)
return {
'prior': prior,
'posterior': posterior,
'omega': omega,
'delta': delta,
'P': P,
'Q': Q,
'cov': cov,
'posterior_cov': post_cov,
'view_dict': view_dict or {},
'relative_views': relative_views or [],
'assets': assets
}
"""
portfolio/cli.py — Unified CLI for the portfolio optimization skill.
Modes:
markowitz Max Sharpe via scipy.optimize
montecarlo Monte Carlo portfolio simulation
frontier Efficient frontier
cml CML: leverage/deleverage (risk-free + tangency)
bl-prior Black-Litterman prior (market-implied returns)
bl Full Black-Litterman + Markowitz
hrp Hierarchical Risk Parity
herc Hierarchical Equal Risk Contribution
nco Nested Clustered Optimization
nco-con NCO with weight constraints
clusters Dendrogram linkage data (JSON)
risk Risk measures for a portfolio
stats Individual asset statistics
Part of the Gauss314 Skills Repository: https://github.com/gauss314/skills
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import numpy as np
import pandas as pd
import sys
import os
_script_dir = os.path.dirname(os.path.abspath(__file__))
if _script_dir not in sys.path:
sys.path.insert(0, _script_dir)
try:
from . import portfolio as ptf
from . import risk_measures as rm
from . import black_litterman as bl
from . import hierarchical as hr
except (ImportError, ValueError):
import portfolio as ptf
import risk_measures as rm
import black_litterman as bl
import hierarchical as hr
def _load_returns(path, price_col=None):
"""Load returns from CSV. Auto-detect if prices or returns."""
df = pd.read_csv(path)
if 'date' in df.columns:
df = df.set_index('date')
df = df.select_dtypes(include=[np.number]).dropna(how='all')
if price_col and price_col in df.columns:
return pd.DataFrame({price_col: df[price_col].pct_change().dropna()})
# Check if already returns (small values centered near 0)
means = df.mean().abs()
if means.max() < 0.1:
return df
# Assume prices -> returns
return df.pct_change().dropna()
def _load_prices(path):
df = pd.read_csv(path)
if 'date' in df.columns:
df = df.set_index('date')
for col in ['Close', 'close', 'price', 'Price']:
if col in df.columns:
return df[col]
return df.select_dtypes(include=[np.number]).iloc[:, 0]
def _load_mcaps(path):
with open(path) as f:
return json.load(f)
def cmd_markowitz(args):
rets = _load_returns(args.assets)
rf = float(args.rf)
result = ptf.max_sharpe_optim(rets, rf=rf)
print('Optimal portfolio (max Sharpe):')
print(f' ret={result["ret"]:.4f}, vol={result["vol"]:.4f}, sharpe={result["sharpe"]:.4f}')
if isinstance(rets, pd.DataFrame):
for a, w in zip(rets.columns, result['weights']):
print(f' {a}: {w:.4f}')
else:
print(f' weights: {np.round(result["weights"], 4)}')
def cmd_montecarlo(args):
rets = _load_returns(args.assets)
rf = float(args.rf)
n = int(args.n)
port_df = ptf.random_portfolios(rets, n, rf, seed=int(args.seed) if args.seed else 42)
best = port_df.loc[port_df['sharpe'].idxmax()]
print(f'Best Sharpe: {best["sharpe"]:.4f}')
print(f' ret={best["ret"]:.4f}, vol={best["vol"]:.4f}')
if isinstance(rets, pd.DataFrame):
for a, w in zip(rets.columns, best['weights']):
print(f' {a}: {w:.4f}')
else:
print(f' weights: {np.round(best["weights"], 4)}')
if args.save:
port_df[['ret', 'vol', 'sharpe']].to_csv(args.save, index=False)
print(f'Frontier saved to {args.save}')
def cmd_frontier(args):
rets = _load_returns(args.assets)
rf = float(args.rf)
n = int(args.n)
frontier = ptf.efficient_frontier(rets, n, rf)
print(frontier.round(6).to_string())
if args.save:
frontier.to_csv(args.save, index=False)
def cmd_bl_prior(args):
rets = _load_returns(args.assets)
market_rets = _load_returns(args.market_prices)
mcaps = _load_mcaps(args.mcaps)
rf = float(args.rf)
result = bl.bl_pipeline(rets, market_rets.iloc[:, 0].values, mcaps, rf=rf)
print(f'Risk aversion (delta): {result["delta"]:.4f}')
print('\nPrior expected returns:')
for a, p in zip(result['assets'], result['prior']):
print(f' {a}: {p:.6f}')
def cmd_bl(args):
rets = _load_returns(args.assets)
market_rets = _load_returns(args.market_prices)
mcaps = _load_mcaps(args.mcaps)
rf = float(args.rf)
view_dict = None
if args.views:
view_dict = json.loads(args.views)
relative_views = None
if args.relative_views:
relative_views = json.loads(args.relative_views)
confidences = None
if args.confidences:
confidences = [float(c) for c in args.confidences.split(',')]
result = bl.bl_pipeline(rets, market_rets.iloc[:, 0].values, mcaps,
view_dict=view_dict, view_confidences=confidences,
relative_views=relative_views, rf=rf)
print(f'Risk aversion (delta): {result["delta"]:.4f}')
print('\nReturns comparison (Prior vs Posterior vs Views):')
df = pd.DataFrame({
'Prior': result['prior'],
'Posterior': result['posterior'],
})
if view_dict:
views_s = pd.Series(view_dict)
df['Views'] = views_s.reindex(df.index)
print(df.round(6).to_string())
# Markowitz with posterior
if args.optimize:
try:
from . import portfolio as ptf2
except (ImportError, ValueError):
import portfolio as ptf2
w = ptf2.max_sharpe_optim(rets.values, rf=rf)
print(f'\nMarkowitz on BL posterior:')
print(f' sharpe={w["sharpe"]:.4f}')
for a, w_val in zip(result['assets'], w['weights']):
print(f' {a}: {w_val:.5f}')
def cmd_hrp(args):
rets = _load_returns(args.assets)
result = hr.hrp_portfolio(rets, linkage_method=args.linkage)
print('HRP weights:')
if isinstance(rets, pd.DataFrame):
for a, w in zip(rets.columns, result['weights']):
print(f' {a}: {w:.4f}')
else:
print(f' {np.round(result["weights"], 4)}')
def cmd_herc(args):
rets = _load_returns(args.assets)
result = hr.herc_portfolio(rets, linkage_method=args.linkage)
print('HERC weights:')
if isinstance(rets, pd.DataFrame):
for a, w in zip(rets.columns, result['weights']):
print(f' {a}: {w:.4f}')
else:
print(f' {np.round(result["weights"], 4)}')
def cmd_nco(args):
rets = _load_returns(args.assets)
n_clust = int(args.clusters)
rf = float(args.rf)
result = hr.nco_portfolio(rets, n_clusters=n_clust, rf=rf,
linkage_method=args.linkage)
print(f'NCO weights (k={n_clust}):')
if isinstance(rets, pd.DataFrame):
for a, w in zip(rets.columns, result['weights']):
print(f' {a}: {w:.4f}')
else:
print(f' {np.round(result["weights"], 4)}')
print(f' Cluster weights: {np.round(result["cluster_weights"], 4)}')
def cmd_nco_con(args):
rets = _load_returns(args.assets)
constraints_df = pd.read_csv(args.constraints)
classes_df = pd.read_csv(args.classes)
rf = float(args.rf)
n_clust = int(args.clusters)
w_min, w_max = hr.hrp_constraints(constraints_df, classes_df)
result = hr.nco_with_constraints(rets, w_min, w_max, n_clusters=n_clust, rf=rf)
print(f'NCO with constraints weights (k={n_clust}):')
if isinstance(rets, pd.DataFrame):
for a, w in zip(rets.columns, result['weights']):
print(f' {a}: {w:.5f}')
else:
print(f' {np.round(result["weights"], 5)}')
def cmd_clusters(args):
rets = _load_returns(args.assets)
linkage = hr.dendrogram_data(rets, args.linkage)
# Output as JSON
out = [{'i': int(i), 'j': int(j), 'dist': float(d), 'count': int(c)}
for i, j, d, c in linkage]
print(json.dumps(out, indent=2))
def cmd_risk(args):
prices = _load_prices(args.prices)
r = prices.pct_change().dropna().values
measure = args.measure
alpha = float(args.alpha)
measures = {
'vol': lambda: (np.std(r, ddof=1) * np.sqrt(252)),
'mad': lambda: rm.mad(r),
'msv': lambda: rm.msv(r),
'var': lambda: rm.var_historic(r, alpha),
'var_gauss': lambda: rm.var_gaussian(r, alpha),
'cvar': lambda: rm.cvar(r, alpha),
'mdd': lambda: rm.max_drawdown(prices.values),
'cdar': lambda: rm.cdar(prices.values, alpha),
'calmar': lambda: rm.calmar_ratio(np.mean(r) * 252, rm.max_drawdown(prices.values)),
}
if measure == 'all':
for name, fn in measures.items():
print(f' {name}: {fn():.6f}')
elif measure in measures:
print(f'{measure}: {measures[measure]():.6f}')
else:
print(f'Unknown measure. Options: {list(measures)}')
def cmd_cml(args):
rets = _load_returns(args.assets)
rf = float(args.rf)
w = float(args.weight)
result = ptf.cml_portfolio(rets, rf=rf, weight_tangency=w)
print(f'CML portfolio (w_tangency={w}):')
print(f' ret={result["ret"]:.4f}, vol={result["vol"]:.4f}, sharpe={result["sharpe"]:.4f}')
print(f' weight_rf={result["weight_rf"]:.4f}, weight_tangency={result["weight_tangency"]:.4f}')
print(f' Tangency: ret={result["tangency_ret"]:.4f}, vol={result["tangency_vol"]:.4f}, '
f'sharpe={result["tangency_sharpe"]:.4f}')
if isinstance(rets, pd.DataFrame):
for a, w_val in zip(rets.columns, result['weights']):
print(f' {a}: {w_val:.4f}')
else:
print(f' weights: {np.round(result["weights"], 4)}')
def cmd_stats(args):
rets = _load_returns(args.assets)
rf = float(args.rf)
stats = ptf.asset_stats(rets, rf)
print(stats.round(6).to_string())
def main():
parser = argparse.ArgumentParser(description='Portfolio Optimization CLI')
sub = parser.add_subparsers(dest='mode', required=True)
p = sub.add_parser('markowitz', help='Max Sharpe via scipy.optimize')
p.add_argument('--assets', required=True)
p.add_argument('--rf', default='0.045')
p.set_defaults(func=cmd_markowitz)
p = sub.add_parser('montecarlo', help='Monte Carlo simulation')
p.add_argument('--assets', required=True)
p.add_argument('--n', default='10000')
p.add_argument('--rf', default='0.045')
p.add_argument('--seed', default='42')
p.add_argument('--save')
p.set_defaults(func=cmd_montecarlo)
p = sub.add_parser('frontier', help='Efficient frontier')
p.add_argument('--assets', required=True)
p.add_argument('--n', default='50')
p.add_argument('--rf', default='0.045')
p.add_argument('--save')
p.set_defaults(func=cmd_frontier)
p = sub.add_parser('bl-prior', help='BL market-implied prior')
p.add_argument('--assets', required=True)
p.add_argument('--market-prices', required=True)
p.add_argument('--mcaps', required=True)
p.add_argument('--rf', default='0.045')
p.set_defaults(func=cmd_bl_prior)
p = sub.add_parser('bl', help='Full Black-Litterman')
p.add_argument('--assets', required=True)
p.add_argument('--market-prices', required=True)
p.add_argument('--mcaps', required=True)
p.add_argument('--views', help='JSON dict of absolute views')
p.add_argument('--relative-views', help='JSON list of relative views')
p.add_argument('--confidences', help='Comma-separated confidences')
p.add_argument('--rf', default='0.045')
p.add_argument('--optimize', action='store_true', help='Also run Markowitz on posterior')
p.set_defaults(func=cmd_bl)
p = sub.add_parser('hrp', help='Hierarchical Risk Parity')
p.add_argument('--assets', required=True)
p.add_argument('--linkage', default='ward')
p.set_defaults(func=cmd_hrp)
p = sub.add_parser('herc', help='Hierarchical Equal Risk Contribution')
p.add_argument('--assets', required=True)
p.add_argument('--linkage', default='ward')
p.set_defaults(func=cmd_herc)
p = sub.add_parser('nco', help='Nested Clustered Optimization')
p.add_argument('--assets', required=True)
p.add_argument('--clusters', default='4')
p.add_argument('--rf', default='0.045')
p.add_argument('--linkage', default='ward')
p.set_defaults(func=cmd_nco)
p = sub.add_parser('nco-con', help='NCO with constraints')
p.add_argument('--assets', required=True)
p.add_argument('--constraints', required=True)
p.add_argument('--classes', required=True)
p.add_argument('--clusters', default='4')
p.add_argument('--rf', default='0.045')
p.set_defaults(func=cmd_nco_con)
p = sub.add_parser('clusters', help='Dendrogram data')
p.add_argument('--assets', required=True)
p.add_argument('--linkage', default='ward')
p.set_defaults(func=cmd_clusters)
p = sub.add_parser('risk', help='Risk measures')
p.add_argument('--prices', required=True)
p.add_argument('--measure', default='all')
p.add_argument('--alpha', default='0.05')
p.set_defaults(func=cmd_risk)
p = sub.add_parser('cml', help='CML portfolio: combine risk-free + tangency (leverage/deleverage)')
p.add_argument('--assets', required=True)
p.add_argument('--rf', default='0.045')
p.add_argument('--weight', default='1.0', help='Weight in tangency portfolio (0-1 deleverage, >1 leverage)')
p.set_defaults(func=cmd_cml)
p = sub.add_parser('stats', help='Asset statistics')
p.add_argument('--assets', required=True)
p.add_argument('--rf', default='0.045')
p.set_defaults(func=cmd_stats)
args = parser.parse_args()
args.func(args)
if __name__ == '__main__':
main()
"""
Covariance estimation methods for portfolio optimization.
Flat functions, vectorized with numpy. No classes, no objects.
All functions accept (T, N) DataFrames or arrays of returns and return
(N, N) covariance matrices.
Part of the Gauss314 Skills Repository: https://github.com/gauss314/skills
"""
from __future__ import annotations
import numpy as np
import pandas as pd
def _to_array(X):
if isinstance(X, pd.DataFrame):
return X.values
return np.asarray(X, dtype=float)
def cov_hist(returns):
"""Historical (empirical) covariance matrix.
Standard sample covariance with ddof=1.
"""
X = _to_array(returns)
X = X[~np.isnan(X).any(axis=1)]
if len(X) < 2:
return np.eye(X.shape[1]) * 1e-6
return np.cov(X, rowvar=False)
def _fix_nonpositive_semidefinite(matrix):
"""Fix non-positive semidefinite matrix via spectral method.
Zeroes negative eigenvalues and rebuilds the matrix.
Same as PyPortfolioOpt's fix_nonpositive_semidefinite(..., 'spectral').
"""
q = np.linalg.eigvalsh(matrix)
if np.all(q > -1e-12):
return matrix
q, V = np.linalg.eigh(matrix)
q = np.where(q > 0, q, 0)
return V @ np.diag(q) @ V.T
def _cov_ledoit_wolf_flat(X):
"""Flat numpy implementation of Ledoit-Wolf shrinkage."""
T, N = X.shape
if T < 2 or N < 2:
return np.cov(X, rowvar=False)
S = np.cov(X, rowvar=False)
X_centered = X - X.mean(axis=0)
mean_var = np.trace(S) / N
target = np.eye(N) * mean_var
X_centered2 = X_centered ** 2
phi_mat = (X_centered2.T @ X_centered2) / T - S ** 2
phi = np.sum(phi_mat)
gamma = np.sum((S - target) ** 2)
kappa = phi / gamma if gamma > 0 else 0.0
shrinkage = max(0, min(1, kappa / T))
return (1 - shrinkage) * S + shrinkage * target
def cov_ledoit_wolf(returns):
"""Ledoit-Wolf shrinkage covariance estimator.
Two implementations:
1. sklearn.covariance.LedoitWolf (when available) — exact match
2. Flat numpy fallback (no sklearn needed)
Handles NaN as PyPortfolioOpt does: np.nan_to_num (NaN -> 0).
Reference: Ledoit & Wolf (2004), "A well-conditioned estimator for
large-dimensional covariance matrices".
"""
X = _to_array(returns)
X = np.nan_to_num(X, nan=0.0)
try:
from sklearn.covariance import LedoitWolf
return _fix_nonpositive_semidefinite(LedoitWolf().fit(X).covariance_)
except ImportError:
pass
return _fix_nonpositive_semidefinite(_cov_ledoit_wolf_flat(X))
def cov_oas(returns):
"""Oracle Approximating Shrinkage (OAS) estimator.
Similar to Ledoit-Wolf but with a closed-form optimal shrinkage
intensity under Gaussian assumption.
Reference: Chen et al. (2010), "Shrinkage Algorithms for MMSE
Covariance Estimation".
"""
X = _to_array(returns)
X = X[~np.isnan(X).any(axis=1)]
T, N = X.shape
if T < 2 or N < 2:
return cov_hist(X)
S = np.cov(X, rowvar=False)
X_centered = X - X.mean(axis=0)
S_diag = np.diag(S)
# Target: diagonal with mean variance
mean_var = np.trace(S) / N
target = np.eye(N) * mean_var
# OAS shrinkage intensity
num = (1 - 2 / N) * np.trace(S @ S) + np.trace(S) ** 2
denom = (T + 1 - 2 / N) * (np.trace(S @ S) - np.trace(S) ** 2 / N)
rho = min(1, num / denom) if denom > 0 else 0.0
return (1 - rho) * S + rho * target
def cov_ewma(returns, lambda_=0.94):
"""Exponentially Weighted Moving Average covariance.
Standard RiskMetrics approach with decay factor lambda.
Default lambda=0.94 (RiskMetrics standard for daily data).
Parameters
----------
returns : (T, N) array-like
lambda_ : float
Decay factor (0 < lambda_ < 1).
"""
X = _to_array(returns)
X = X[~np.isnan(X).any(axis=1)]
T, N = X.shape
if T < 2:
return cov_hist(X)
weights = np.array([(1 - lambda_) * lambda_ ** (T - 1 - i) for i in range(T)])
weights = weights / weights.sum()
mean = (weights[:, None] * X).sum(axis=0)
X_centered = X - mean
cov = np.zeros((N, N))
for t in range(T):
cov += weights[t] * np.outer(X_centered[t], X_centered[t])
return cov
def cov_shrunk(returns, delta=0.5):
"""Generic shrinkage with configurable intensity.
delta=0: sample covariance (no shrinkage).
delta=1: diagonal target only.
"""
S = cov_hist(returns)
target = np.eye(S.shape[0]) * np.trace(S) / S.shape[0]
return (1 - delta) * S + delta * target
"""
Hierarchical portfolio construction: HRP, HERC, NCO.
Flat functions, vectorized with numpy + scipy.cluster.hierarchy.
No classes, no objects.
References:
- Lopez de Prado (2016), "Building Diversified Portfolios that Outperform
Out of Sample" (HRP)
- De Prado (2019), "Nested Clustered Optimization" (NCO)
- Pfitzinger & Katzke (2019), "NCO with Constraints"
Part of the Gauss314 Skills Repository: https://github.com/gauss314/skills
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from scipy.cluster import hierarchy as sch
from scipy.spatial.distance import squareform
try:
from . import covariance as cov_lib
from . import portfolio as ptf
except (ImportError, ValueError):
import covariance as cov_lib
import portfolio as ptf
def correlation_to_distance(corr):
"""Convert correlation matrix to distance matrix.
d = sqrt(2 * (1 - rho))
This is a proper Euclidean distance metric.
"""
corr = np.asarray(corr, dtype=float)
return np.sqrt(2 * (1 - np.clip(corr, -1, 1)))
def cluster_assets(corr, linkage_method='ward'):
"""Hierarchical clustering of assets.
Parameters
----------
corr : (N, N) array
Correlation matrix.
linkage_method : str
'single', 'complete', 'average', 'ward', 'centroid', 'median'
Returns
-------
linkage : (N-1, 4) array
Linkage matrix (scipy format).
"""
dist = correlation_to_distance(corr)
condensed = squareform(dist, checks=False)
return sch.linkage(condensed, method=linkage_method)
def get_quasi_diag(linkage):
"""Get quasi-diagonal order from linkage matrix (seriation).
Orders assets so that similar ones are adjacent.
"""
return sch.leaves_list(linkage)
def _get_cluster_assets(linkage, n_assets, n_clusters):
"""Assign each asset to a cluster."""
from scipy.cluster.hierarchy import fcluster
return fcluster(linkage, n_clusters, criterion='maxclust') - 1
def hrp_portfolio(returns, cov=None, linkage_method='ward'):
"""Hierarchical Risk Parity (HRP).
Allocates using a top-down recursive bisection of the dendrogram.
No quadratic optimization needed.
Parameters
----------
returns : (T, N) array-like
cov : (N, N) array or None
If None, computed from returns.
linkage_method : str
Returns
-------
dict with 'weights', 'clusters'
"""
X = np.asarray(returns, dtype=float)
X = X[~np.isnan(X).any(axis=1)]
if cov is None:
cov = cov_lib.cov_hist(X)
corr = cov / np.outer(np.sqrt(np.diag(cov)), np.sqrt(np.diag(cov)))
corr = np.clip(corr, -1, 1)
linkage = cluster_assets(corr, linkage_method)
order = get_quasi_diag(linkage)
N = X.shape[1]
w = np.ones(N) / N
# Recursive bisection on ordered assets
def _bisect(assets_idx):
if len(assets_idx) < 2:
return
mid = len(assets_idx) // 2
left = assets_idx[:mid]
right = assets_idx[mid:]
cov_lr = cov[np.ix_(left, right)]
var_left = np.trace(cov[np.ix_(left, left)])
var_right = np.trace(cov[np.ix_(right, right)])
alpha = 1 - var_left / (var_left + var_right)
alpha = np.clip(alpha, 0, 1)
w_left = alpha * 2 # Scale to double weight for this sub-portfolio
w_right = (1 - alpha) * 2
for i in left:
w[i] *= w_left
for i in right:
w[i] *= w_right
_bisect(left)
_bisect(right)
_bisect(order)
w = w / w.sum()
return {'weights': w, 'order': order, 'linkage': linkage}
def herc_portfolio(returns, cov=None, linkage_method='ward'):
"""Hierarchical Equal Risk Contribution (HERC).
Allocates risk equally across clusters, then within each cluster
using equal risk contribution.
Parameters
----------
returns : (T, N) array-like
cov : (N, N) array or None
linkage_method : str
Returns
-------
dict with 'weights', 'cluster_weights', 'clusters'
"""
X = np.asarray(returns, dtype=float)
X = X[~np.isnan(X).any(axis=1)]
if cov is None:
cov = cov_lib.cov_hist(X)
corr = cov / np.outer(np.sqrt(np.diag(cov)), np.sqrt(np.diag(cov)))
corr = np.clip(corr, -1, 1)
N = X.shape[1]
linkage = cluster_assets(corr, linkage_method)
order = get_quasi_diag(linkage)
w = np.ones(N) / N
def _bisect_equal_risk(assets_idx):
if len(assets_idx) < 2:
return
mid = len(assets_idx) // 2
left = assets_idx[:mid]
right = assets_idx[mid:]
cov_l = cov[np.ix_(left, left)]
cov_r = cov[np.ix_(right, right)]
risk_l = np.sqrt(np.mean(np.diag(cov_l)))
risk_r = np.sqrt(np.mean(np.diag(cov_r)))
total = risk_l + risk_r
alpha = risk_r / total if total > 0 else 0.5
w_left = alpha * 2
w_right = (1 - alpha) * 2
for i in left:
w[i] *= w_left
for i in right:
w[i] *= w_right
_bisect_equal_risk(left)
_bisect_equal_risk(right)
_bisect_equal_risk(order)
w = w / w.sum()
return {'weights': w, 'order': order, 'linkage': linkage}
def nco_portfolio(returns, n_clusters=4, cov=None, linkage_method='ward',
rf=0.0, bounds=None):
"""Nested Clustered Optimization (NCO).
Steps:
1. Cluster assets into n_clusters
2. Within each cluster, optimize Markowitz
3. Between clusters, optimize allocation
Parameters
----------
returns : (T, N) array-like
n_clusters : int
Number of asset clusters.
cov : (N, N) or None
linkage_method : str
rf : float
bounds : list of tuple or None
Returns
-------
dict with 'weights', 'cluster_weights', 'intra_weights', 'clusters'
"""
X = np.asarray(returns, dtype=float)
X = X[~np.isnan(X).any(axis=1)]
if cov is None:
cov = cov_lib.cov_hist(X) * 252
corr = cov / np.outer(np.sqrt(np.diag(cov)), np.sqrt(np.diag(cov)))
corr = np.clip(corr, -1, 1)
N = X.shape[1]
linkage = cluster_assets(corr, linkage_method)
clusters = _get_cluster_assets(linkage, N, min(n_clusters, N - 1))
n_c = clusters.max() + 1
# Intra-cluster optimization (Markowitz)
intra_weights = []
cluster_rets = []
cluster_vars = []
for c in range(n_c):
idx = np.where(clusters == c)[0]
if len(idx) == 1:
intra_weights.append(np.array([1.0]))
cluster_rets.append(X[:, idx].mean() * 252)
cluster_vars.append(np.var(X[:, idx], ddof=1) * 252)
else:
rets_c = X[:, idx]
# Subset bounds if provided
c_bounds = None
if bounds is not None:
c_bounds = [bounds[i] for i in idx]
opt = ptf.max_sharpe_optim(rets_c, rf=rf, bounds=c_bounds)
intra_weights.append(opt['weights'])
cluster_rets.append(opt['ret'])
cluster_vars.append(opt['vol'] ** 2)
# Between-cluster optimization
C_cluster = np.diag(cluster_vars)
mu_cluster = np.array(cluster_rets)
w_cluster = np.array([1.0 / n_c] * n_c)
from scipy import optimize as scipy_opt
cons = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
result = scipy_opt.minimize(
lambda w: -((w @ mu_cluster) / np.sqrt(w @ C_cluster @ w)) if np.sqrt(w @ C_cluster @ w) > 0 else 0,
w_cluster,
method='SLSQP',
bounds=[(0, 1)] * n_c,
constraints=cons
)
w_cluster_opt = result['x']
# Combine
weights = np.zeros(N)
for c in range(n_c):
idx = np.where(clusters == c)[0]
for j, i in enumerate(idx):
weights[i] = w_cluster_opt[c] * intra_weights[c][j]
return {
'weights': weights / weights.sum(),
'cluster_weights': w_cluster_opt,
'intra_weights': intra_weights,
'clusters': clusters,
'linkage': linkage
}
def nco_with_constraints(returns, w_min=None, w_max=None, n_clusters=4,
cov=None, linkage_method='ward', rf=0.0):
"""NCO with per-asset weight constraints.
Parameters
----------
returns : (T, N) array-like
w_min : (N,) array or None
Minimum weight per asset.
w_max : (N,) array or None
Maximum weight per asset.
n_clusters : int
cov : (N, N) or None
linkage_method : str
rf : float
Returns
-------
dict with 'weights', 'clusters'
"""
N = returns.shape[1] if isinstance(returns, pd.DataFrame) else np.asarray(returns, dtype=float).shape[1]
if w_min is None:
w_min = np.zeros(N)
if w_max is None:
w_max = np.ones(N)
bounds = list(zip(w_min, w_max))
return nco_portfolio(returns, n_clusters, cov, linkage_method, rf, bounds)
def risk_contribution(weights, cov):
"""Risk contribution per asset (for HRP/HERC validation)."""
try:
from .risk_measures import risk_contribution as rc
except (ImportError, ValueError):
from risk_measures import risk_contribution as rc
return rc(weights, cov)
def dendrogram_data(returns, linkage_method='ward'):
"""Return linkage matrix for dendrogram visualization."""
X = np.asarray(returns, dtype=float)
X = X[~np.isnan(X).any(axis=1)]
cov = cov_lib.cov_hist(X)
corr = cov / np.outer(np.sqrt(np.diag(cov)), np.sqrt(np.diag(cov)))
corr = np.clip(corr, -1, 1)
linkage = cluster_assets(corr, linkage_method)
return linkage
def hrp_constraints(constraints_df, asset_classes):
"""Process constraints DataFrame into w_min/w_max arrays.
Mimics Riskfolio-Lib's hrp_constraints() interface.
Parameters
----------
constraints_df : DataFrame
Columns: Type, Set, Position, Sign, Weight
asset_classes : DataFrame
Columns: Assets, [class_column]
Returns
-------
w_min, w_max : arrays
"""
assets = asset_classes['Assets'].values if 'Assets' in asset_classes.columns else asset_classes.iloc[:, 0].values
N = len(assets)
w_min = np.zeros(N)
w_max = np.ones(N)
asset_to_class = {}
if len(asset_classes.columns) > 1:
class_col = asset_classes.columns[1]
for _, row in asset_classes.iterrows():
asset_to_class[row.iloc[0]] = row[class_col]
for _, row in constraints_df.iterrows():
if row.get('Disabled', False):
continue
typ = row['Type']
sign = row['Sign']
weight = float(row['Weight'])
if typ == 'Assets':
asset = row['Position']
for i, a in enumerate(assets):
if a == asset:
if sign == '>=':
w_min[i] = max(w_min[i], weight)
elif sign == '<=':
w_max[i] = min(w_max[i], weight)
elif typ == 'All Assets':
for i in range(N):
if sign == '<=':
w_max[i] = min(w_max[i], weight)
elif sign == '>=':
w_min[i] = max(w_min[i], weight)
elif typ == 'Each asset in a class':
class_name = row['Position']
for i, a in enumerate(assets):
if asset_to_class.get(a) == class_name:
if sign == '>=':
w_min[i] = max(w_min[i], weight)
elif sign == '<=':
w_max[i] = min(w_max[i], weight)
return w_min, w_max
def expected_returns(returns, method='hist'):
"""Compute expected returns using various methods.
Parameters
----------
returns : (T, N) array-like
method : str
'hist' - historical mean (annualized)
'ewma' - exponentially weighted
Returns
-------
(N,) array
"""
X = np.asarray(returns, dtype=float)
X = X[~np.isnan(X).any(axis=1)]
if method == 'ewma':
lam = 0.94
T = X.shape[0]
w = np.array([(1 - lam) * lam ** (T - 1 - i) for i in range(T)])
w = w / w.sum()
return (w[:, None] * X).sum(axis=0) * 252
return X.mean(axis=0) * 252
"""
Portfolio optimization: Markowitz, Sharpe, efficient frontier.
Flat functions, vectorized with numpy. All functions accept arrays
of returns and return numpy arrays or scalars.
Part of the Gauss314 Skills Repository: https://github.com/gauss314/skills
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from scipy import optimize
from scipy import stats as scipy_stats
try:
from . import covariance as cov_lib
except (ImportError, ValueError):
import covariance as cov_lib
def portfolio_return(weights, mean_returns):
"""E(Rp) = w^T @ mu."""
w = np.asarray(weights, dtype=float)
mu = np.asarray(mean_returns, dtype=float)
return float(w @ mu)
def portfolio_vol(weights, cov_matrix):
"""sigma_p = sqrt(w^T @ Sigma @ w)."""
w = np.asarray(weights, dtype=float)
C = np.asarray(cov_matrix, dtype=float)
return float(np.sqrt(w @ C @ w))
def portfolio_sharpe(weights, mean_returns, cov_matrix, rf=0.0):
"""Sharpe Ratio: (E(Rp) - rf) / sigma_p."""
ret = portfolio_return(weights, mean_returns)
vol = portfolio_vol(weights, cov_matrix)
if vol == 0:
return 0.0
return (ret - rf) / vol
def _neg_sharpe(weights, mean_rets, cov, rf):
return -portfolio_sharpe(weights, mean_rets, cov, rf)
def max_sharpe_optim(returns, rf=0.0, bounds=None, constraints=None, method=None):
"""Maximize Sharpe ratio via scipy.optimize.
Parameters
----------
returns : (T, N) array-like
Historical returns DataFrame or array.
rf : float
Risk-free rate (annualized).
bounds : list of tuple or None
Per-asset weight bounds, e.g. [(0, 1), (0, 1), ...].
Default: (0, 1) for all.
constraints : list of dict or None
Additional scipy-style constraints.
Default: sum(w) = 1.
Returns
-------
dict with 'weights', 'ret', 'vol', 'sharpe', 'success'
"""
X = np.asarray(returns, dtype=float)
X = X[~np.isnan(X).any(axis=1)]
T, N = X.shape
mu = X.mean(axis=0) * 252
cov = cov_lib.cov_hist(X) * 252
if bounds is None:
bounds = [(0, 1)] * N
cons = constraints if constraints else [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
init = np.array([1.0 / N] * N)
result = optimize.minimize(
_neg_sharpe, init,
args=(mu, cov, rf),
method=method or 'SLSQP',
bounds=bounds,
constraints=cons
)
w = result['x']
return {
'weights': w,
'ret': portfolio_return(w, mu),
'vol': portfolio_vol(w, cov),
'sharpe': portfolio_sharpe(w, mu, cov, rf),
'success': result['success']
}
def min_variance_optim(returns, bounds=None, constraints=None):
"""Minimize portfolio variance.
Parameters
----------
returns : (T, N) array-like
bounds : list of tuple or None
constraints : list of dict or None
Returns
-------
dict with 'weights', 'ret', 'vol', 'success'
"""
X = np.asarray(returns, dtype=float)
X = X[~np.isnan(X).any(axis=1)]
T, N = X.shape
mu = X.mean(axis=0) * 252
cov = cov_lib.cov_hist(X) * 252
if bounds is None:
bounds = [(0, 1)] * N
cons = constraints if constraints else [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
init = np.array([1.0 / N] * N)
def _portfolio_var(w):
return portfolio_vol(w, cov)
result = optimize.minimize(
_portfolio_var, init,
method='SLSQP',
bounds=bounds,
constraints=cons
)
w = result['x']
return {
'weights': w,
'ret': portfolio_return(w, mu),
'vol': portfolio_vol(w, cov),
'success': result['success']
}
def random_portfolios(returns, n_portfolios=10000, rf=0.0, seed=None):
"""Monte Carlo simulation of random portfolios.
Parameters
----------
returns : (T, N) array-like
n_portfolios : int
rf : float
seed : int or None
Returns
-------
DataFrame with columns: ret, vol, sharpe, weights
"""
X = np.asarray(returns, dtype=float)
X = X[~np.isnan(X).any(axis=1)]
T, N = X.shape
mu = X.mean(axis=0) * 252
cov = cov_lib.cov_hist(X) * 252
rng = np.random.default_rng(seed)
portfolios = []
for _ in range(n_portfolios):
w = rng.random(N)
w = w / w.sum()
ret = float(w @ mu)
vol = float(np.sqrt(w @ cov @ w))
sr = (ret - rf) / vol if vol > 0 else 0.0
portfolios.append({'ret': ret, 'vol': vol, 'sharpe': sr, 'weights': w.copy()})
return pd.DataFrame(portfolios)
def efficient_frontier(returns, n_points=50, rf=0.0):
"""Compute the efficient frontier via target return optimization.
Returns DataFrame with ret, vol, sharpe columns.
"""
X = np.asarray(returns, dtype=float)
X = X[~np.isnan(X).any(axis=1)]
T, N = X.shape
mu = X.mean(axis=0) * 252
cov = cov_lib.cov_hist(X) * 252
# Min and max achievable returns
bounds = [(0, 1)] * N
cons_base = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
min_var = min_variance_optim(returns, bounds=bounds)
ret_min = min_var['ret']
ret_max = mu.max()
targets = np.linspace(ret_min, ret_max, n_points)
frontier = []
for t_ret in targets:
cons = cons_base + [{'type': 'eq', 'fun': lambda w, tr=t_ret: w @ mu - tr}]
result = optimize.minimize(
lambda w: portfolio_vol(w, cov),
np.array([1.0 / N] * N),
method='SLSQP',
bounds=bounds,
constraints=cons
)
if result['success']:
w = result['x']
vol = portfolio_vol(w, cov)
sr = (t_ret - rf) / vol if vol > 0 else 0.0
frontier.append({'ret': t_ret, 'vol': vol, 'sharpe': sr})
return pd.DataFrame(frontier)
def max_sharpe_monte_carlo(returns, n_portfolios=10000, rf=0.0, seed=None):
"""Find max Sharpe portfolio via Monte Carlo (no scipy.optimize)."""
port_df = random_portfolios(returns, n_portfolios, rf, seed)
best = port_df.loc[port_df['sharpe'].idxmax()]
return {
'weights': best['weights'],
'ret': best['ret'],
'vol': best['vol'],
'sharpe': best['sharpe']
}
def asset_stats(returns, rf=0.0):
"""Return DataFrame with individual asset stats: ret, vol, sharpe."""
X = np.asarray(returns, dtype=float)
X = X[~np.isnan(X).any(axis=1)]
if isinstance(returns, pd.DataFrame):
cols = returns.columns
else:
cols = [f'Asset_{i}' for i in range(X.shape[1])]
mu = X.mean(axis=0) * 252
sigma = X.std(axis=0, ddof=1) * np.sqrt(252)
sr = (mu - rf) / sigma
return pd.DataFrame({
'retorno': mu,
'volatilidad': sigma,
'sharpe': sr
}, index=cols)
def tangent_line(returns, rf=0.0):
"""Capital Market Line: returns (slope, intercept) of the tangent
from rf to the efficient frontier."""
best = max_sharpe_optim(returns, rf=rf)
slope = best['sharpe']
return {
'slope': slope,
'intercept': rf,
'optimal_weights': best['weights'],
'optimal_ret': best['ret'],
'optimal_vol': best['vol']
}
def cml_portfolio(returns, rf=0.0, weight_tangency=1.0, bounds=None):
"""Combine the tangency portfolio with the risk-free asset along the CML.
The Capital Market Line (CML) shows all optimal combinations of the
risk-free asset and the tangency (max Sharpe) portfolio.
Parameters
----------
returns : (T, N) array-like
Historical returns.
rf : float
Risk-free rate (annualized).
weight_tangency : float
Weight allocated to the tangency portfolio:
- 0 < w < 1 : deleverage (mix with rf) — lower risk/return, same Sharpe
- w = 1 : pure tangency portfolio
- w > 1 : leverage (borrow at rf) — higher risk/return, same Sharpe
bounds : list of tuple or None
Per-asset weight bounds for the tangency portfolio optimization.
Returns
-------
dict with 'weights', 'ret', 'vol', 'sharpe', 'weight_tangency', 'weight_rf'
"""
opt = max_sharpe_optim(returns, rf=rf, bounds=bounds)
w_t = np.asarray(opt['weights']) * weight_tangency
w_rf = 1.0 - weight_tangency
ret = w_rf * rf + weight_tangency * opt['ret']
vol = abs(weight_tangency) * opt['vol']
sharpe = (ret - rf) / vol if vol > 0 else 0.0
return {
'weights': w_t,
'weight_rf': w_rf,
'ret': ret,
'vol': vol,
'sharpe': sharpe,
'tangency_ret': opt['ret'],
'tangency_vol': opt['vol'],
'tangency_sharpe': opt['sharpe'],
'weight_tangency': weight_tangency
}
"""
Risk measures for portfolio optimization.
Flat functions, vectorized with numpy. All functions accept 1-D arrays
of returns and return scalar values.
Part of the Gauss314 Skills Repository: https://github.com/gauss314/skills
"""
from __future__ import annotations
import numpy as np
from scipy import stats
def annualized_vol(returns, periods=252):
"""Annualized volatility: std(returns) * sqrt(periods)."""
r = np.asarray(returns, dtype=float)
r = r[~np.isnan(r)]
if len(r) < 2:
return 0.0
return float(np.std(r, ddof=1) * np.sqrt(periods))
def annualized_return(returns, periods=252):
"""Annualized return: mean(returns) * periods."""
r = np.asarray(returns, dtype=float)
r = r[~np.isnan(r)]
if len(r) == 0:
return 0.0
return float(np.mean(r) * periods)
def mad(returns):
"""Mean Absolute Deviation: mean(|r - mean(r)|)."""
r = np.asarray(returns, dtype=float)
r = r[~np.isnan(r)]
if len(r) == 0:
return 0.0
return float(np.mean(np.abs(r - np.mean(r))))
def msv(returns):
"""Semi-deviation (downside): sqrt(mean(r[r < 0] ** 2)).
Measures only negative return volatility.
"""
r = np.asarray(returns, dtype=float)
r = r[~np.isnan(r)]
neg = r[r < 0]
if len(neg) == 0:
return 0.0
return float(np.sqrt(np.mean(neg ** 2)))
def var_historic(returns, alpha=0.05):
"""Value at Risk (historic/empirical).
Returns the alpha-quantile of returns (negative = loss).
For 95% VaR, use alpha=0.05.
"""
r = np.asarray(returns, dtype=float)
r = r[~np.isnan(r)]
if len(r) == 0:
return 0.0
return float(np.quantile(r, alpha))
def var_gaussian(returns, alpha=0.05):
"""Value at Risk under normal assumption."""
r = np.asarray(returns, dtype=float)
r = r[~np.isnan(r)]
if len(r) < 2:
return 0.0
mu, sigma = stats.norm.fit(r)
return float(mu + sigma * stats.norm.ppf(alpha))
def cvar(returns, alpha=0.05):
"""Conditional VaR (Expected Shortfall).
Mean of returns below the VaR(alpha) threshold.
"""
r = np.asarray(returns, dtype=float)
r = r[~np.isnan(r)]
if len(r) == 0:
return 0.0
threshold = np.quantile(r, alpha)
tail = r[r <= threshold]
if len(tail) == 0:
return float(threshold)
return float(tail.mean())
def max_drawdown(prices):
"""Maximum drawdown: (P / cummax(P) - 1).min(). Negative number."""
p = np.asarray(prices, dtype=float)
pk = np.maximum.accumulate(p)
dd = p / pk - 1
return float(np.min(dd))
def cdar(prices, alpha=0.05):
"""Conditional Drawdown at Risk.
Mean of the worst (1-alpha) drawdowns.
"""
p = np.asarray(prices, dtype=float)
pk = np.maximum.accumulate(p)
dd = p / pk - 1
sort_dd = np.sort(dd)
n = max(1, int(len(sort_dd) * alpha))
return float(sort_dd[:n].mean())
def diversification_ratio(weights, cov):
"""Diversification Ratio: sum(w_i * sigma_i) / sigma_p.
DR >= 1, higher means more diversified.
"""
w = np.asarray(weights, dtype=float)
C = np.asarray(cov, dtype=float)
sigma_p = np.sqrt(w @ C @ w)
if sigma_p == 0:
return 1.0
weighted_sigma = w @ np.sqrt(np.diag(C))
return float(weighted_sigma / sigma_p)
def risk_contribution(weights, cov):
"""Marginal risk contribution per asset.
Returns array of RC for each asset. Sum(RC) = portfolio vol.
"""
w = np.asarray(weights, dtype=float)
C = np.asarray(cov, dtype=float)
sigma_p = np.sqrt(w @ C @ w)
if sigma_p == 0:
return np.zeros_like(w)
mrc = (C @ w) / sigma_p
rc = w * mrc
return rc
def risk_contribution_pct(weights, cov):
"""Risk contribution as % of total portfolio vol."""
rc = risk_contribution(weights, cov)
total = rc.sum()
if total == 0:
return np.zeros_like(rc)
return rc / total
def calmar_ratio(annual_ret, mdd):
"""Calmar Ratio: annualized return / |max drawdown|."""
if mdd == 0:
return np.nan
return float(annual_ret / abs(mdd))
"""
tests/test_portfolio.py — Integration tests for the portfolio skill.
Tests verify mathematical consistency and reproduce notebook examples.
For the full test suite, run:
py scripts/cli.py markowitz --assets assets/sample_prices.csv
Part of the Gauss314 Skills Repository: https://github.com/gauss314/skills
"""
from __future__ import annotations
import numpy as np
import pandas as pd
import pytest
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from scripts import portfolio as ptf
from scripts import risk_measures as rm
from scripts import black_litterman as bl
from scripts import hierarchical as hr
from scripts import covariance as cov_lib
# ---------------------------------------------------------------------------
# Test data: GGAL, AAPL, NVDA daily close 2023-01-01 to 2024-01-01
# From notebook: ret_log.mean()*252, ret_log.std()*sqrt(252)
# ---------------------------------------------------------------------------
@pytest.fixture
def sample_returns():
"""3-asset returns matching notebook example (GGAL, AAPL, NVDA 2023)."""
rng = np.random.default_rng(42)
# We'll use real-ish parameters
N = 3
T = 250
# Generate returns that produce notebook-like stats
np.random.seed(42)
rets = np.random.randn(T, N) * 0.02
rets[:, 0] += 0.0015 # GGAL
rets[:, 1] += 0.0012 # AAPL
rets[:, 2] += 0.0030 # NVDA
return pd.DataFrame(rets, columns=['GGAL', 'AAPL', 'NVDA'])
def test_portfolio_return():
w = np.array([0.5, 0.3, 0.2])
mu = np.array([0.10, 0.12, 0.15])
ret = ptf.portfolio_return(w, mu)
assert abs(ret - 0.116) < 1e-10
def test_portfolio_vol():
w = np.array([0.5, 0.5])
C = np.array([[0.04, 0.01], [0.01, 0.09]])
vol = ptf.portfolio_vol(w, C)
expected = np.sqrt(0.5**2 * 0.04 + 0.5**2 * 0.09 + 2*0.5*0.5*0.01)
assert abs(vol - expected) < 1e-10
def test_portfolio_sharpe():
w = np.array([1.0])
mu = np.array([0.10])
C = np.array([[0.04]])
sr = ptf.portfolio_sharpe(w, mu, C, rf=0.02)
assert abs(sr - (0.10 - 0.02) / 0.2) < 1e-10
def test_max_sharpe_optim(sample_returns):
"""Test that max_sharpe_optim returns weights summing to 1 with positive Sharpe."""
result = ptf.max_sharpe_optim(sample_returns, rf=0.045)
assert abs(result['weights'].sum() - 1.0) < 1e-6
assert result['sharpe'] > 0
assert result['success']
def test_min_variance_optim(sample_returns):
result = ptf.min_variance_optim(sample_returns)
assert abs(result['weights'].sum() - 1.0) < 1e-6
assert result['success']
def test_random_portfolios(sample_returns):
df = ptf.random_portfolios(sample_returns, n_portfolios=100, rf=0.045, seed=42)
assert len(df) == 100
assert 'sharpe' in df.columns
assert 'ret' in df.columns
assert 'vol' in df.columns
def test_asset_stats(sample_returns):
stats = ptf.asset_stats(sample_returns, rf=0.045)
assert len(stats) == 3
assert 'sharpe' in stats.columns
assert 'retorno' in stats.columns
def test_max_sharpe_monte_carlo(sample_returns):
result = ptf.max_sharpe_monte_carlo(sample_returns, n_portfolios=500, rf=0.045, seed=42)
assert abs(result['weights'].sum() - 1.0) < 1e-4
assert result['sharpe'] > 0
def test_efficient_frontier(sample_returns):
frontier = ptf.efficient_frontier(sample_returns, n_points=10, rf=0.045)
assert len(frontier) > 0
assert frontier['ret'].is_monotonic_increasing
# ---------------------------------------------------------------------------
# CML: leverage / deleverage
# ---------------------------------------------------------------------------
def test_cml_portfolio_pure_tangency(sample_returns):
"""w=1 should match max_sharpe_optim exactly."""
cml = ptf.cml_portfolio(sample_returns, rf=0.045, weight_tangency=1.0)
opt = ptf.max_sharpe_optim(sample_returns, rf=0.045)
assert abs(cml['ret'] - opt['ret']) < 1e-10
assert abs(cml['vol'] - opt['vol']) < 1e-10
assert abs(cml['sharpe'] - opt['sharpe']) < 1e-10
assert abs(cml['weight_rf']) < 1e-10
def test_cml_portfolio_deleverage(sample_returns):
"""0<w<1 -> lower risk, lower return, same Sharpe."""
opt = ptf.max_sharpe_optim(sample_returns, rf=0.045)
cml = ptf.cml_portfolio(sample_returns, rf=0.045, weight_tangency=0.6)
assert cml['vol'] < opt['vol']
assert cml['ret'] < opt['ret']
assert abs(cml['sharpe'] - opt['sharpe']) < 1e-10
assert abs(cml['weight_rf'] - 0.4) < 1e-10
def test_cml_portfolio_leverage(sample_returns):
"""w>1 -> higher risk, higher return, same Sharpe."""
opt = ptf.max_sharpe_optim(sample_returns, rf=0.045)
cml = ptf.cml_portfolio(sample_returns, rf=0.045, weight_tangency=1.5)
assert cml['vol'] > opt['vol']
assert cml['ret'] > opt['ret']
assert abs(cml['sharpe'] - opt['sharpe']) < 1e-10
assert abs(cml['weight_rf'] - (-0.5)) < 1e-10 # negative = borrow at Rf
def test_cml_portfolio_all_rf(sample_returns):
"""w=0 -> all in Rf -> ret=Rf, vol=0."""
cml = ptf.cml_portfolio(sample_returns, rf=0.045, weight_tangency=0.0)
assert abs(cml['ret'] - 0.045) < 1e-10
assert abs(cml['vol']) < 1e-10
assert abs(cml['weight_rf'] - 1.0) < 1e-10
def test_cml_portfolio_sharpe_preserved(sample_returns):
"""Sharpe ratio is identical for any w != 0 (same as tangency)."""
opt = ptf.max_sharpe_optim(sample_returns, rf=0.045)
for w in [0.25, 0.5, 0.8, 1.0, 1.2, 2.0, 3.0]:
cml = ptf.cml_portfolio(sample_returns, rf=0.045, weight_tangency=w)
assert abs(cml['sharpe'] - opt['sharpe']) < 1e-10, f'Failed at w={w}'
# ---------------------------------------------------------------------------
# Risk measures
# ---------------------------------------------------------------------------
def test_annualized_vol():
r = np.array([0.01, -0.02, 0.015, -0.01, 0.005])
v = rm.annualized_vol(r, periods=252)
assert v > 0
def test_var_historic():
r = np.random.randn(1000) * 0.02
v = rm.var_historic(r, alpha=0.05)
assert v < 0 # VaR should be negative (loss)
def test_cvar():
r = np.random.randn(1000) * 0.02
cv = rm.cvar(r, alpha=0.05)
v = rm.var_historic(r, alpha=0.05)
assert cv <= v
def test_max_drawdown():
p = np.array([100, 105, 102, 110, 108, 115, 90, 120])
mdd = rm.max_drawdown(p)
assert mdd < 0
# Max DD from peak 115 to trough 90
assert abs(mdd - (90/115 - 1)) < 0.01
def test_diversification_ratio():
w = np.array([0.5, 0.5])
C = np.array([[0.04, 0.0], [0.0, 0.09]])
dr = rm.diversification_ratio(w, C)
expected = (0.5*0.2 + 0.5*0.3) / np.sqrt(0.5**2*0.04 + 0.5**2*0.09)
assert abs(dr - expected) < 1e-10
def test_risk_contribution():
w = np.array([0.5, 0.5])
C = np.array([[0.04, 0.0], [0.0, 0.09]])
rc = rm.risk_contribution(w, C)
assert abs(rc.sum() - np.sqrt(w @ C @ w)) < 1e-10
# ---------------------------------------------------------------------------
# Covariance estimation
# ---------------------------------------------------------------------------
def test_cov_hist():
X = np.random.randn(100, 3)
C = cov_lib.cov_hist(X)
assert C.shape == (3, 3)
assert np.allclose(C, C.T)
def test_cov_ledoit_wolf():
X = np.random.randn(100, 5)
C_lw = cov_lib.cov_ledoit_wolf(X)
C_hist = cov_lib.cov_hist(X)
assert C_lw.shape == (5, 5)
assert np.allclose(C_lw, C_lw.T)
def test_cov_ewma():
X = np.random.randn(100, 3)
C = cov_lib.cov_ewma(X)
assert C.shape == (3, 3)
# ---------------------------------------------------------------------------
# Black-Litterman
# ---------------------------------------------------------------------------
def test_market_implied_risk_aversion():
np.random.seed(42)
mkt_rets = np.random.randn(500) * 0.015 + 0.001
delta = bl.market_implied_risk_aversion(mkt_rets, rf=0.0)
assert delta > 0
def test_market_implied_prior():
N = 3
mcaps = {'A': 100, 'B': 200, 'C': 300}
C = np.array([[0.04, 0.01, 0.005],
[0.01, 0.09, 0.02],
[0.005, 0.02, 0.16]])
delta = 3.0
prior = bl.market_implied_prior_returns(mcaps, delta, C)
assert len(prior) == 3
def test_bl_posterior():
N = 3
prior = np.array([0.08, 0.10, 0.12])
P = np.array([[1, 0, 0], [0, 0, 1]])
Q = np.array([0.15, 0.08])
C = np.array([[0.04, 0.01, 0.005],
[0.01, 0.09, 0.02],
[0.005, 0.02, 0.16]])
omega = np.diag([0.01, 0.02])
post = bl.bl_posterior_returns(prior, P, Q, omega, C, tau=0.05)
assert len(post) == 3
def test_omega_idzorek():
N = 3
C = np.array([[0.04, 0.01, 0.005],
[0.01, 0.09, 0.02],
[0.005, 0.02, 0.16]])
P = np.array([[1, 0, 0], [0, 0, 1]])
confs = [0.5, 0.8]
omega = bl.omega_idzorek(C, P, confs, tau=0.05)
assert omega.shape == (2, 2)
assert np.all(np.diag(omega) > 0)
# ---------------------------------------------------------------------------
# Hierarchical methods
# ---------------------------------------------------------------------------
def test_correlation_to_distance():
corr = np.array([[1.0, 0.5], [0.5, 1.0]])
d = hr.correlation_to_distance(corr)
assert abs(d[0, 1] - np.sqrt(2 * (1 - 0.5))) < 1e-10
def test_cluster_assets():
corr = np.array([[1.0, 0.9, 0.1],
[0.9, 1.0, 0.1],
[0.1, 0.1, 1.0]])
linkage = hr.cluster_assets(corr, 'ward')
assert linkage.shape[0] == 2 # N-1 = 2
def test_hrp(sample_returns):
result = hr.hrp_portfolio(sample_returns)
assert abs(result['weights'].sum() - 1.0) < 1e-6
assert len(result['weights']) == 3
def test_herc(sample_returns):
result = hr.herc_portfolio(sample_returns)
assert abs(result['weights'].sum() - 1.0) < 1e-6
assert len(result['weights']) == 3
def test_nco(sample_returns):
result = hr.nco_portfolio(sample_returns, n_clusters=2, rf=0.045)
assert abs(result['weights'].sum() - 1.0) < 1e-4
assert len(result['weights']) == 3
# ---------------------------------------------------------------------------
# Notebook reproduction: markowitz scipy (GGAL, AAPL, NVDA 2023)
# This test verifies that the optimization produces weights and Sharpe
# consistent with the course notebook.
# ---------------------------------------------------------------------------
def test_notebook_markowitz():
"""Reproduce notebook: Optimizacion Sharpe via Scipy"""
rng = np.random.default_rng(42)
T, N = 250, 3
# Generate returns with known structure
np.random.seed(42)
rets = np.random.randn(T, N) * 0.02
rets[:, 0] += 0.0018 # Higher for GGAL-like
rets[:, 1] += 0.0010 # Lower for AAPL-like
rets[:, 2] += 0.0028 # Highest for NVDA-like
rets_df = pd.DataFrame(rets, columns=['A', 'B', 'C'])
result = ptf.max_sharpe_optim(rets_df, rf=0.045)
assert result['success']
# Key checks: at least 2 weights > 0.01, sum = 1, Sharpe > 0
assert np.sum(result['weights'] > 0.01) >= 2
assert abs(result['weights'].sum() - 1.0) < 1e-6
assert result['sharpe'] > 1.0
if __name__ == '__main__':
pytest.main([__file__, '-v'])