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.0446035529237261e-7  1.613867796203562e-7   …  1.7909538434631164e-6
 1.9595030396546636e-7  1.084904202018651e-7      4.553832788424591e-6
 1.5314184003658203e-6  5.230417130358743e-6      0.9999838333400731
 5.275247805998895e-7   0.03867049846463573       0.9492064287014171
 3.0934198261513346e-7  0.09791699329861743       0.8412845500473712
 4.5159497107757124e-7  0.11768697985244408    …  0.8450649247739467
 2.2041079319406903e-7  0.09634511195408313       0.8873842220166757
 5.651966765221162e-7   0.0157133490619404        0.9867462221414217
 6.783041401591429e-10  6.532787036841918e-9      1.085802721025627e-7
 6.811610418677307e-8   1.7588744966393617e-7     6.994091342309392e-6
 ⋮                                             ⋱  ⋮
 9.421715346705977e-8   5.5763072890628804e-8     0.2334775383319056
 2.2137057175139616e-7  3.766518431422223e-8      0.10273824736795502
 2.999573125835883e-7   5.437911863358717e-8      0.17295822942981595
 4.3298577562013234e-7  1.2201566296382917e-7  …  0.09177592788894023
 3.849616906702529e-6   2.396557155165589e-7      0.09019366388190699
 8.53198466773854e-6    2.1156152551956715e-7     0.0553275999721009
 2.681854461626518e-6   3.3962524131622585e-7     0.0797551574781441
 0.04611145811990149    1.6111896235064493e-7     0.12589828543647005
 0.09891373233087623    2.6877283565835096e-7  …  0.2015734967274358), :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.9878791022917492
 0.9392488057447699
 0.9627567875060582
 0.9837332342828432
 1.0024638850773422
 0.952748924248973
 0.9527489227304625
 ⋮
 1.1501669601643518
 1.1419356138474503
 1.151958708206736
 1.133209540981234
 1.1702422522914917
 1.1686191982628253
 1.1362621811543034
 1.0997212280634965
 1.1112574412388214), :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.8176048171665645e-8
  1.4773082759467454e-7
 -0.01212107148295086
 -0.04922697163464988
  0.02502849257566837
  0.021787898095346394
  0.019040376132208117
 -0.049592769942563675
 -1.5938201946604013e-9
  3.5923014607282966e-8
  ⋮
  0.00451170579445364
 -0.0071566534268427745
  0.008777285021802078
 -0.01627590215858428
  0.03267949127766033
 -0.0013869384954168556
 -0.027688247083927184
 -0.03215890988617233
  0.010490125025265858)), 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.