Distributionally Robust Portfolio Optimization

distributionally Robust Return as Objective Term

using HiGHS
using Clarabel
using Distributions
using PortfolioOpt
using PortfolioOpt.TestUtils

Read Prices

prices = get_test_data();
numD, numA = size(prices) # A: Assets    D: Days
(316, 6)

Calculating returns

returns_series = percentchange(prices);
315×6 TimeSeries.TimeArray{Float64, 2, Dates.Date, Matrix{Float64}} 2009-09-02 to 2010-12-01
┌────────────┬──────────────┬──────────────┬─────────────┬─────────────┬────────
│            │ AAPL         │ BA           │ DELL        │ CAT         │ EBAY  ⋯
├────────────┼──────────────┼──────────────┼─────────────┼─────────────┼────────
│ 2009-09-02 │ -0.000725953 │  -0.00758663 │  0.00920447 │ -0.00752737 │  -0.0 ⋯
│ 2009-09-03 │   0.00829398 │   0.00123967 │ -0.00651466 │   0.0351643 │  0.00 ⋯
│ 2009-09-04 │    0.0225758 │    0.0142385 │   0.0288525 │   0.0237567 │   0.0 ⋯
│ 2009-09-08 │    0.0153837 │   0.00712106 │   0.0172084 │   0.0186511 │  -0.0 ⋯
│ 2009-09-09 │    -0.010351 │    0.0208081 │ -0.00250627 │   0.0306579 │   0.0 ⋯
│ 2009-09-10 │    0.0082973 │ -0.000791609 │    0.040201 │  0.00578393 │   0.0 ⋯
│ 2009-09-11 │  -0.00231803 │    0.0170331 │  0.00241546 │  -0.0032861 │  0.00 ⋯
│ 2009-09-14 │   0.00906134 │  -0.00740019 │  -0.0126506 │  0.00494539 │   0.0 ⋯
│     ⋮      │      ⋮       │      ⋮       │      ⋮      │      ⋮      │       ⋱
│ 2010-11-22 │    0.0216151 │   0.00691933 │  0.00431655 │  0.00035727 │   0.0 ⋯
│ 2010-11-23 │   -0.0147753 │   -0.0067156 │  -0.0100287 │  -0.0163095 │  -0.0 ⋯
│ 2010-11-24 │    0.0196612 │    0.0284591 │  0.00434153 │   0.0249304 │   0.0 ⋯
│ 2010-11-26 │  0.000635324 │  -0.00932579 │  -0.0165706 │ -0.00661235 │ -0.00 ⋯
│ 2010-11-29 │   0.00593651 │  -0.00679012 │ -0.00586081 │ -0.00546773 │  -0.0 ⋯
│ 2010-11-30 │   -0.0180516 │  -0.00916718 │  -0.0257922 │   0.0111151 │  -0.0 ⋯
│ 2010-12-01 │    0.0168729 │    0.0305786 │   0.0143722 │   0.0336879 │  0.00 ⋯
└────────────┴──────────────┴──────────────┴─────────────┴─────────────┴────────
                                                  2 columns and 300 rows omitted

Backtest parameters

DEFAULT_SOLVER = optimizer_with_attributes(
    HiGHS.Optimizer, "presolve" => "on", "time_limit" => 60.0, "log_to_console" => false
)

Clarabel_SOLVER = optimizer_with_attributes(
    Clarabel.Optimizer, "verbose" => false, "max_iter" => 900000
)

date_range = timestamp(returns_series)[100:end];
216-element Vector{Dates.Date}:
 2010-01-26
 2010-01-27
 2010-01-28
 2010-01-29
 2010-02-01
 2010-02-02
 2010-02-03
 2010-02-04
 2010-02-05
 2010-02-08
 ⋮
 2010-11-18
 2010-11-19
 2010-11-22
 2010-11-23
 2010-11-24
 2010-11-26
 2010-11-29
 2010-11-30
 2010-12-01

Backtest Delage's robust return

backtest_results = Dict()

backtest_results["Delage"], _ = sequential_backtest_market(
    VolumeMarketHistory(returns_series), date_range,
) do market, past_returns, ext
    numD, numA = size(past_returns)
    returns = values(past_returns)
    k_back = 80
    Σ, r̄ = mean_variance(returns[(end - k_back):end, :])

    d = MvNormal(r̄, Σ)

    formulation = PortfolioFormulation(MAX_SENSE,
        ObjectiveTerm(ExpectedUtility(MomentUncertainty(d, 0.05, 1.3),
            PieceWiseUtility(
                [1.0], [0.0]
            )
        ))
    )

    pointers = change_bids!(market, formulation, Clarabel_SOLVER)
    return pointers
end
(Dict{Symbol, PortfolioOpt.StateRecorder}(:decisions => PortfolioOpt.DecisionRecorder{Float64}(2-dimensional DenseAxisArray{Float64,2,...} with index sets:
    Dimension 1, [Dates.Date("2010-01-26"), Dates.Date("2010-01-27"), Dates.Date("2010-01-28"), Dates.Date("2010-01-29"), Dates.Date("2010-02-01"), Dates.Date("2010-02-02"), Dates.Date("2010-02-03"), Dates.Date("2010-02-04"), Dates.Date("2010-02-05"), Dates.Date("2010-02-08")  …  Dates.Date("2010-11-17"), Dates.Date("2010-11-18"), Dates.Date("2010-11-19"), Dates.Date("2010-11-22"), Dates.Date("2010-11-23"), Dates.Date("2010-11-24"), Dates.Date("2010-11-26"), Dates.Date("2010-11-29"), Dates.Date("2010-11-30"), Dates.Date("2010-12-01")]
    Dimension 2, 1:6
And data, a 216×6 Matrix{Float64}:
 1.0446035529238486e-7  1.6138677962036675e-7  …  1.7909538434629792e-6
 1.9595030396539483e-7  1.0849042020182421e-7     4.553832788423345e-6
 1.5314183936883542e-6  5.230417106435837e-6      0.9999838333401446
 5.275251547616286e-7   0.038670498561696756      0.9492064286030465
 3.093419826294679e-7   0.09791699329878623       0.841284550049047
 4.515949711380667e-7   0.11768697985263832    …  0.8450649247756528
 2.2041080388927448e-7  0.09634511197079534       0.8873842220017293
 5.65196672431233e-7    0.015713349064952815      0.9867462221399228
 6.783041401583405e-10  6.532787036839921e-9      1.0858027210250551e-7
 6.811610418678869e-8   1.7588744966396973e-7     6.994091342307946e-6
 ⋮                                             ⋱  ⋮
 9.421719663156232e-8   5.576307214207446e-8      0.23347753837488613
 2.2137055615545522e-7  3.766518005886245e-8      0.10273824736251057
 2.999573127939947e-7   5.437911865715581e-8      0.1729582294252451
 4.3298551940071007e-7  1.2201567527958584e-7  …  0.09177592786040506
 3.849616907111671e-6   2.396557156276092e-7      0.0901936638793415
 8.531980867345937e-6   2.1156144822289596e-7     0.05532759982692901
 2.6818543349039953e-6  3.39625222672151e-7       0.0797551574356618
 0.04611145811819807    1.6111896213876369e-7     0.1258982854328859
 0.09891373230200771    2.687728296015252e-7   …  0.20157349671590005), :wealth => PortfolioOpt.WealthRecorder{Float64}(1-dimensional DenseAxisArray{Float64,1,...} with index sets:
    Dimension 1, [Dates.Date("2010-01-26"), Dates.Date("2010-01-27"), Dates.Date("2010-01-28"), Dates.Date("2010-01-29"), Dates.Date("2010-02-01"), Dates.Date("2010-02-02"), Dates.Date("2010-02-03"), Dates.Date("2010-02-04"), Dates.Date("2010-02-05"), Dates.Date("2010-02-08")  …  Dates.Date("2010-11-18"), Dates.Date("2010-11-19"), Dates.Date("2010-11-22"), Dates.Date("2010-11-23"), Dates.Date("2010-11-24"), Dates.Date("2010-11-26"), Dates.Date("2010-11-29"), Dates.Date("2010-11-30"), Dates.Date("2010-12-01"), Dates.Date("2010-12-02")]
And data, a 217-element Vector{Float64}:
 1.0
 1.0000000281760482
 1.00000017590688
 0.9878791022917486
 0.9392488057466241
 0.9627567875079588
 0.983733234284786
 1.0024638850788277
 0.9527489242504287
 0.9527489227319182
 ⋮
 1.1501669601334692
 1.1419356138167378
 1.1519587081757525
 1.1332095409511684
 1.1702422522604468
 1.1686191982301928
 1.1362621811214757
 1.0997212280317192
 1.1112574412062064), :returns => PortfolioOpt.ReturnsRecorder{Float64}(1-dimensional DenseAxisArray{Float64,1,...} with index sets:
    Dimension 1, [Dates.Date("2010-01-26"), Dates.Date("2010-01-27"), Dates.Date("2010-01-28"), Dates.Date("2010-01-29"), Dates.Date("2010-02-01"), Dates.Date("2010-02-02"), Dates.Date("2010-02-03"), Dates.Date("2010-02-04"), Dates.Date("2010-02-05"), Dates.Date("2010-02-08")  …  Dates.Date("2010-11-17"), Dates.Date("2010-11-18"), Dates.Date("2010-11-19"), Dates.Date("2010-11-22"), Dates.Date("2010-11-23"), Dates.Date("2010-11-24"), Dates.Date("2010-11-26"), Dates.Date("2010-11-29"), Dates.Date("2010-11-30"), Dates.Date("2010-12-01")]
And data, a 216-element Vector{Float64}:
  2.8176048171663604e-8
  1.4773082759463338e-7
 -0.012121071482951495
 -0.04922697163277237
  0.02502849257566831
  0.0217878980953472
  0.019040376131705644
 -0.049592769942519876
 -1.5938201946571453e-9
  3.5923014607218135e-8
  ⋮
  0.004511705792416523
 -0.0071566534268869345
  0.008777285021800937
 -0.016275902158225167
  0.032679491277663064
 -0.0013869384968103323
 -0.027688247084867328
 -0.03215890988617699
  0.010490125024807046)), nothing)

Backtest NingNing Du's Wasserstein robust return (under light tail assumption)

backtest_results["Wasserstein_light_tail"], _ = sequential_backtest_market(
    VolumeMarketHistory(returns_series), date_range,
) do market, past_returns, ext
    numD, numA = size(past_returns)
    returns = values(past_returns)

    j_robust = 30
    δ = 0.1
    ϵ = log(1/δ)/j_robust

    d = DeterministicSamples(returns'[:, (end - j_robust):end])
    s = DuWassersteinBall(d; ϵ=ϵ)

    formulation = PortfolioFormulation(MAX_SENSE,
        ObjectiveTerm(ExpectedReturn(s))
    )

    pointers = change_bids!(market, formulation, DEFAULT_SOLVER)
    return pointers
end
(Dict{Symbol, PortfolioOpt.StateRecorder}(:decisions => PortfolioOpt.DecisionRecorder{Float64}(2-dimensional DenseAxisArray{Float64,2,...} with index sets:
    Dimension 1, [Dates.Date("2010-01-26"), Dates.Date("2010-01-27"), Dates.Date("2010-01-28"), Dates.Date("2010-01-29"), Dates.Date("2010-02-01"), Dates.Date("2010-02-02"), Dates.Date("2010-02-03"), Dates.Date("2010-02-04"), Dates.Date("2010-02-05"), Dates.Date("2010-02-08")  …  Dates.Date("2010-11-17"), Dates.Date("2010-11-18"), Dates.Date("2010-11-19"), Dates.Date("2010-11-22"), Dates.Date("2010-11-23"), Dates.Date("2010-11-24"), Dates.Date("2010-11-26"), Dates.Date("2010-11-29"), Dates.Date("2010-11-30"), Dates.Date("2010-12-01")]
    Dimension 2, 1:6
And data, a 216×6 Matrix{Float64}:
 0.0  0.0  0.0  0.0  0.0                 1.0
 0.0  0.0  0.0  0.0  0.0                 1.014505893019039
 0.0  0.0  0.0  0.0  0.0                 1.0471441523118772
 0.0  0.0  0.0  0.0  0.0                 1.034451495920218
 0.0  0.0  0.0  0.0  0.0                 0.9827742520398913
 0.0  0.0  0.0  0.0  0.0                 1.0081595648232098
 0.0  0.0  0.0  0.0  0.0                 1.0326382592928383
 0.0  0.0  0.0  0.0  0.0                 1.0553037171350865
 0.0  0.0  0.0  0.0  0.0                 1.0027198549410703
 0.0  0.0  0.0  0.0  0.0                 0.9891205802357208
 ⋮                                       ⋮
 0.0  0.0  0.0  0.0  0.0                 1.1199295540171785
 0.0  0.0  0.0  0.0  1.0823300006448997  0.0
 0.0  0.0  0.0  0.0  0.0                 1.0727706581465637
 0.0  0.0  0.0  0.0  1.0674990578608308  0.0
 0.0  0.0  0.0  0.0  1.0514726359738862  0.0
 0.0  0.0  0.0  0.0  1.08735788498161    0.0
 0.0  0.0  0.0  0.0  1.0852674821267911  0.0
 0.0  0.0  0.0  0.0  1.0532146383529022  0.0
 0.0  0.0  0.0  0.0  0.0                 1.0152389864903593), :wealth => PortfolioOpt.WealthRecorder{Float64}(1-dimensional DenseAxisArray{Float64,1,...} with index sets:
    Dimension 1, [Dates.Date("2010-01-26"), Dates.Date("2010-01-27"), Dates.Date("2010-01-28"), Dates.Date("2010-01-29"), Dates.Date("2010-02-01"), Dates.Date("2010-02-02"), Dates.Date("2010-02-03"), Dates.Date("2010-02-04"), Dates.Date("2010-02-05"), Dates.Date("2010-02-08")  …  Dates.Date("2010-11-18"), Dates.Date("2010-11-19"), Dates.Date("2010-11-22"), Dates.Date("2010-11-23"), Dates.Date("2010-11-24"), Dates.Date("2010-11-26"), Dates.Date("2010-11-29"), Dates.Date("2010-11-30"), Dates.Date("2010-12-01"), Dates.Date("2010-12-02")]
And data, a 217-element Vector{Float64}:
 1.0
 1.014505893019039
 1.0471441523118772
 1.034451495920218
 0.9827742520398913
 1.0081595648232098
 1.0326382592928383
 1.0553037171350865
 1.0027198549410703
 0.9891205802357208
 ⋮
 1.0823300006448997
 1.0727706581465637
 1.0674990578608308
 1.0514726359738862
 1.08735788498161
 1.0852674821267911
 1.0532146383529022
 1.0152389864903593
 1.048358451545252), :returns => PortfolioOpt.ReturnsRecorder{Float64}(1-dimensional DenseAxisArray{Float64,1,...} with index sets:
    Dimension 1, [Dates.Date("2010-01-26"), Dates.Date("2010-01-27"), Dates.Date("2010-01-28"), Dates.Date("2010-01-29"), Dates.Date("2010-02-01"), Dates.Date("2010-02-02"), Dates.Date("2010-02-03"), Dates.Date("2010-02-04"), Dates.Date("2010-02-05"), Dates.Date("2010-02-08")  …  Dates.Date("2010-11-17"), Dates.Date("2010-11-18"), Dates.Date("2010-11-19"), Dates.Date("2010-11-22"), Dates.Date("2010-11-23"), Dates.Date("2010-11-24"), Dates.Date("2010-11-26"), Dates.Date("2010-11-29"), Dates.Date("2010-11-30"), Dates.Date("2010-12-01")]
And data, a 216-element Vector{Float64}:
  0.014505893019038872
  0.03217158176943748
 -0.012121212121212248
 -0.049956178790534884
  0.02583025830258316
  0.024280575539568545
  0.021949078138718017
 -0.049828178694157885
 -0.013562386980108948
  0.005499541704858329
  ⋮
 -0.03357314148681014
 -0.00883218842001981
 -0.004914004914005337
 -0.015013054830287144
  0.034128561961563865
 -0.0019224607497595274
 -0.029534510433386705
 -0.036056897122064425
  0.032622333751568616)), nothing)

Plot

using Plots
using Plots.PlotMeasures

plt = plot(;title="Culmulative Wealth",
    xlabel="Time",
    ylabel="Wealth",
    legend=:outertopright,
    left_margin=10mm
);
for (strategy_name, recorders) in backtest_results
    plot!(plt,
        axes(get_records(recorders[:wealth]), 1), get_records(recorders[:wealth]).data;
        label=strategy_name,
    )
end
plt

This page was generated using Literate.jl.