Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SPOLC 🆕 #86

Merged
merged 17 commits into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified Banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 33 additions & 3 deletions docs/src/FL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ The "Follow the Loser" (FL) strategy, introduced by [borodin2003can](@citet), in
6. [Gaussian Weighting Reversion (GWR)](@ref)
7. [Distributed Mean Reversion (DMR)](@ref)
8. [Robust Median Reversion (RMR)](@ref)
9. [Transaction Cost Optimization (TCO)](@ref)
9. [Short-term portfolio optimization with loss control (SPOLC)](@ref)
10. [Transaction Cost Optimization (TCO)](@ref)


## Reweighted Price Relative Tracking System for Automatic Portfolio Optimization (RPRT)

Expand Down Expand Up @@ -647,21 +649,49 @@ julia> model.b

You can analyse the algorithm's performance using several metrics that have been provided in this package. Check out the [Performance evaluation](@ref) section for more details.

## Transaction Cost Optimization (TCO)

Proportional transaction costs have also been investigated in the field of OPS algorithms. Transaction Cost Optimization (TCO) [1357831](@cite) is an algorithm that probes the aformentioned issue. The TCO framework integrates the L1 norm of successive allocations' differences with the goal of maximizing anticipated log return. This formulation is addressed through convex optimization, yielding two explicit portfolio update formulas, namely, TCO1 and TCO2. Both variants is implemented in this package and can be used for research purposes. See [`tco`](@ref).
## Short-term portfolio optimization with loss control (SPOLC)

Estimating covariance matrix in rapidly-changing financial markets is barely investigated in the loiterature of the OPS algorithms. [10.5555/3455716.3455813](@citet) proposed a novel online portfolio selection strategy called Short-term portfolio optimization with loss control (SPOLC) which addresses the issue and is very strong in controlling extreme losses. They proposed an innovative rank-one covariance estimate model which effectively catches the instantaneous risk structure of the current financial circumstance, and incorporate it in a short-term portfolio optimization (SPO) that minimizes the downside risk of the portfolio. See [`spolc`](@ref).

Let's run the algorithm on the real market data.

```julia
julia> using OnlinePortfolioSelection, YFinance

julia> tickers = ["AAPL", "AMZN", "GOOG", "MSFT"];

julia> querry = [get_prices(ticker, startdt="2019-01-01", enddt="2019-01-25")["adjclose"] for ticker in tickers];

julia> prices = stack(querry, dims=1);

julia> rel_pr = prices[:, 2:end] ./ prices[:, 1:end-1];

julia> model = spolc(rel_pr, 0.025, 5);

julia> model.b
4×15 Matrix{Float64}:
0.25 0.197923 0.244427 0.239965 … 0.999975 8.49064e-6 2.41014e-6
0.25 0.272289 0.251802 0.276544 1.57258e-5 0.999983 0.999992
0.25 0.269046 0.255524 0.240024 6.50008e-6 5.94028e-6 3.69574e-6
0.25 0.260742 0.248247 0.243466 2.99939e-6 3.04485e-6 1.56805e-6

julia> tickers = ["MSFT", "TSLA", "GOOGL", "NVDA"];

julia> querry = [get_prices(ticker, startdt="2024-01-01", enddt="2024-03-01")["adjclose"] for ticker in tickers];

julia> pr = stack(querry, dims=1);

julia> r = pr[:, 2:end]./pr[:, 1:end-1];
```

You can analyse the algorithm's performance using several metrics that have been provided in this package. Check out the [Performance evaluation](@ref) section for more details.

## Transaction Cost Optimization (TCO)

Proportional transaction costs have also been investigated in the field of OPS algorithms. Transaction Cost Optimization (TCO) [1357831](@cite) is an algorithm that probes the aformentioned issue. The TCO framework integrates the L1 norm of successive allocations' differences with the goal of maximizing anticipated log return. This formulation is addressed through convex optimization, yielding two explicit portfolio update formulas, namely, TCO1 and TCO2. Both variants is implemented in this package and can be used for research purposes. See [`tco`](@ref).

```julia
# TCO1
julia> model = tco(r, 5, 5, 0.04, 10, TCO1, [0.05, 0.05, 0.7, 0.2]);

Expand Down
2 changes: 1 addition & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ end
# Introduction

Online Portfolio Selection (OPS) strategies represent trading algorithms that sequentially allocate capital among a pool of assets with the aim of maximizing investment returns. This forms a fundamental issue in computational finance, extensively explored across various research domains, including finance, statistics, artificial intelligence, machine learning, and data mining. Framed within an online machine learning context, OPS is defined as a sequential decision problem, providing a range of advanced approaches to tackle this challenge. These approaches categorize into benchmarks, “Follow-the-Winner” and “Follow-the-Loser” strategies, “Pattern-Matching” based methodologies, and "Meta-Learning" Algorithms [li2013online](@cite).
This package offers an efficient implementation of OPS algorithms in Julia, ensuring complete type stability. All algorithms yield an [`OPSAlgorithm`](@ref) object, permitting inquiries into portfolio weights, asset count, and algorithm names. Presently, 30 algorithms are incorporated, with ongoing plans for further additions. The existing algorithms are as follows:
This package offers an efficient implementation of OPS algorithms in Julia, ensuring complete type stability. All algorithms yield an [`OPSAlgorithm`](@ref) object, permitting inquiries into portfolio weights, asset count, and algorithm names. Presently, 32 algorithms are incorporated, with ongoing plans for further additions. The existing algorithms are as follows:

!!! note
In the following table, the abbreviations **PM**, **ML**, **FL**, and **FW** stand for **Pattern-Matching**, **Meta-Learning**, **Follow the Loser**, and **Follow the Winner**, respectively.
Expand Down
18 changes: 18 additions & 0 deletions docs/src/refs.bib
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,24 @@ @ARTICLE{Zhang2022-ht
doi = {10.1007/s10878-021-00800-7},
}

@article{10.5555/3455716.3455813,
author = {Lai, Zhao-Rong and Tan, Liming and Wu, Xiaotian and Fang, Liangda},
title = {Loss control with rank-one covariance estimate for short-term portfolio optimization},
year = {2020},
issue_date = {January 2020},
publisher = {JMLR.org},
volume = {21},
number = {1},
issn = {1532-4435},
abstract = {In short-term portfolio optimization (SPO), some financial characteristics like the expected return and the true covariance might be dynamic. Then there are only a small window size w of observations that are sufficiently close to the current moment and reliable to make estimations. w is usually much smaller than the number of assets d, which leads to a typical undersampled problem. Worse still, the asset price relatives are not likely subject to any proper distributions. These facts violate the statistical assumptions of the traditional covariance estimates and invalidate their statistical efficiency and consistency in risk measurement. In this paper, we propose to reconsider the function of covariance estimates in the perspective of operators, and establish a rank-one covariance estimate in the principal rank-one tangent space at the observation matrix. Moreover, we propose a loss control scheme with this estimate, which effectively catches the instantaneous risk structure and avoids extreme losses. We conduct extensive experiments on 7 real-world benchmark daily or monthly data sets with stocks, funds and portfolios from diverse regional markets to show that the proposed method achieves state-of-the-art performance in comprehensive downside risk metrics and gains good investing incomes as well. It offers a novel perspective of rank-related approaches for undersampled estimations in SPO.},
journal = {J. Mach. Learn. Res.},
month = {jan},
articleno = {97},
numpages = {37},
keywords = {rank-one covariance estimate, short-term portfolio optimization, undersampled condition, loss control, downside risk},
url = {https://dl.acm.org/doi/abs/10.5555/3455716.3455813}
}

@article{1357831,
author = {Bin, Li and Jialei, Wang and Dingjiang, Huang and Steven C. H. Hoi},
title = {Transaction cost optimization for online portfolio selection},
Expand Down
113 changes: 113 additions & 0 deletions src/Algos/SPOLC.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
spolc(x::AbstractMatrix, 𝛾::AbstractFloat, w::Integer)

Run loss control strategy with a rank-one covariance estimate for short-term portfolio \
optimization (SPOLC).

# Arguments
- `x::AbstractMatrix`: Matrix of relative prices.
- `𝛾::AbstractFloat`: Mixing parameter that trades off between the increasing factor \
and the risk.
- `w::Integer`: Window size.

!!! warning "Beware!"
`x` should be a matrix of size `n_assets` × `n_periods`.

# Returns
- `::OPSAlgorithm`: An object of [`OPSAlgorithm`](@ref).

# Example
```julia
julia> using OnlinePortfolioSelection, YFinance

julia> tickers = ["AAPL", "AMZN", "GOOG", "MSFT"];

julia> querry = [get_prices(ticker, startdt="2019-01-01", enddt="2019-01-25")["adjclose"] for ticker in tickers];

julia> prices = stack(querry, dims=1);

julia> rel_pr = prices[:, 2:end] ./ prices[:, 1:end-1];

julia> model = spolc(rel_pr, 0.025, 5);

julia> model.b
4×15 Matrix{Float64}:
0.25 0.197923 0.244427 0.239965 … 0.999975 8.49064e-6 2.41014e-6
0.25 0.272289 0.251802 0.276544 1.57258e-5 0.999983 0.999992
0.25 0.269046 0.255524 0.240024 6.50008e-6 5.94028e-6 3.69574e-6
0.25 0.260742 0.248247 0.243466 2.99939e-6 3.04485e-6 1.56805e-6

julia> sum(model.b, dims=1) .|> isapprox(1.) |> all
true
```

# Reference
> [Loss Control with Rank-one Covariance Estimate for Short-term Portfolio Optimization](https://dl.acm.org/doi/abs/10.5555/3455716.3455813)
"""
function spolc(x::AbstractMatrix, 𝛾::AbstractFloat, w::Integer)
𝛾>0 || ArgumentError("`𝛾` should be greater than 0. $𝛾 is passed.") |> throw
w>1 || ArgumentError("`w` should be more than 1. $w is passed.") |> throw
n_assets, T = size(x)
b = similar(x)
b[:, 1] .= 1/n_assets
q = zeros(length(x))
for t ∈ 1:T-1
if t==1
q = x[:, 1]
b̂ = simplexproj(q, 1)
elseif t<w+1
q = x[:, t]
b̂ = simplexproj(q, 1)
else
xbefore = x[:, t-w+1:t]
b̂ = main(xbefore, 𝛾)
end
b[:, t+1] = b̂
end
return OPSAlgorithm(n_assets, b, "SPOLC")
end

function simplexproj(v::AbstractVector, b::Integer)
while maximum(abs.(v))>1e6
@. v = v/2
end
u = sort(v, rev=true)
sv = cumsum(u)
ρ = findlast(u.>(sv.-b)./(1:length(u)))
θ = (sv[ρ] - b)/ρ
w = max.(v .- θ, 0)
return w
end

function main(x::AbstractMatrix, 𝛾::AbstractFloat)
n_assets, n_days = size(x)
H = zeros(n_assets+1, n_assets+1)
U_tmp,Sig_tmp,V_tmp = svd(x)
S = diagm(Sig_tmp)
tol = maximum((n_days, n_days))*S[1]*eps(eltype(x))
r = sum(S .> tol)
U = U_tmp[:, 1:r]
V = V_tmp[:, 1:r]
S = S[1:r]
Sig = diagm(S)
Sig1 = Sig.^(2)
Sig2 = Sig1.-Sig*V'*ones(n_days, n_days)*V*Sig/n_days
ζ = Sig1[1, 1]/sqrt(tr(Sig2))/n_assets/(n_days-1)
Htmp2 = U[:, 1]*ζ*U[:, 1]'
H[1:n_assets, 1:n_assets] = Htmp2
f = vcat(zeros(n_assets), 1)
A = vcat(-x, ones(1, n_days))
return ẑfunc(𝛾, H, f, A)
end

function ẑfunc(𝛾::AbstractFloat, H::AbstractMatrix, f::AbstractVector, A::AbstractMatrix)
n_assets = size(A, 1)
model = Model(optimizer_with_attributes(Optimizer, "print_level" => 0))
@variable(model, z[1:n_assets])
@constraint(model, z'*A .≤ 0)
@constraint(model, 0. .≤ z[1:n_assets] .≤ 1.)
@constraint(model, sum(z)==1)
@objective(model, Min, 𝛾*z'*H*z+f'*z)
optimize!(model)
return value.(z)[1:end-1]
end
6 changes: 4 additions & 2 deletions src/OnlinePortfolioSelection.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module OnlinePortfolioSelection

using Statistics: cor, var, mean, median, std
using LinearAlgebra: I, norm, Symmetric, diagm, tr, diagind
using LinearAlgebra: I, norm, Symmetric, diagm, tr, diagind, svd
using JuMP: Model, @variable, @constraint, @NLobjective, @expression, @objective
using JuMP: value, @NLconstraint, set_silent, optimize!, optimizer_with_attributes, objective_value
using Ipopt: Optimizer
Expand Down Expand Up @@ -47,6 +47,7 @@ include("Algos/RMR.jl")
include("Algos/SSPO.jl")
include("Algos/WAEG.jl")
include("Algos/MAEG.jl")
include("Algos/SPOLC.jl")
include("Algos/TCO.jl")
include("Tools/metrics.jl")
include("Tools/show.jl")
Expand All @@ -55,7 +56,7 @@ include("Tools/cornfam.jl")

export up, eg, cornu, cornk, dricornk, bcrp, bs, rprt, anticor, olmar, bk, load, mrvol, cwogd
export uniform, cluslog, pamr, ppt, cwmr, caeg, oldem, aictr, egm, tppt, gwr, ons, dmr, rmr, sspo
export waeg, maeg, tco
export waeg, maeg, spolc, tco
export opsmetrics, sn, mer, apy, ann_std, ann_sharpe, mdd, calmar, ir, at
export OPSAlgorithm, OPSMetrics, KMNLOG, KMDLOG, PAMR, PAMR1, PAMR2
export CWMRD, CWMRS, Var, Stdev
Expand Down Expand Up @@ -132,6 +133,7 @@ function opsmethods()
println(" SSPO: Short-term Sparse Portfolio Optimization - Call `sspo`")
println(" WAEG: Weak Aggregating Exponential Gradient - Call `waeg`")
println(" MAEG: Moving-window-based Adaptive Exponential Gradient - Call `maeg`")
println(" SPOLC: loss control strategy for short-term portfolio optimization (SPOLC) - Call `spolc`")
println(" TCO: Transaction Cost Optimization - Call `tco`")
end
# COV_EXCL_STOP
Expand Down
27 changes: 27 additions & 0 deletions test/SPOLC.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
rel_pr = [
0.900393 1.04269 0.997774 1.01906 1.01698 1.0032 0.990182 0.984963 1.02047 1.01222 1.00594 1.00616 0.977554 1.00404 0.992074
0.974759 1.05006 1.03435 1.01661 1.00171 0.998072 0.990545 0.985767 1.03546 1.00551 1.00561 1.00176 0.962251 1.00481 1.00909
0.971516 1.05379 0.997833 1.00738 0.998495 0.995971 0.987723 0.988176 1.03107 1.00355 1.00826 1.00767 0.974742 1.00472 0.998447
0.963212 1.04651 1.00128 1.00725 1.0143 0.993575 0.992278 0.992704 1.02901 1.00352 1.00702 1.01498 0.981153 1.00975 0.995221
]

𝛾 = 0.025
w = 5

@testset "SPOLC.jl" begin
@testset "With valid arguments" begin
model = spolc(rel_pr, 𝛾, w)
@test size(model.b) == size(rel_pr)
@test (model.b[:, 1] .== 1/size(rel_pr, 1)) |> all
@test sum(model.b, dims=1) .|> isapprox(1.) |> all
end

@testset "With invalid arguments" begin
@test_throws ArgumentError spolc(rel_pr, 0., w)
@test_throws ArgumentError spolc(rel_pr, 𝛾, 1)
end

@testset "Individual funcs" begin
@test OnlinePortfolioSelection.simplexproj([1e6, 2e6, 3e6], 3)≈[0., 0., 3.]
end
end
2 changes: 2 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ using Statistics
include("WAEG.jl")
@info "Run unit tests in MAEG.jl"
include("MAEG.jl")
@info "Run unit tests in SPOLC.jl"
include("SPOLC.jl")
@info "Run unit tests in TCO.jl"
include("TCO.jl")
end
Expand Down
Loading