diff --git a/Banner.png b/Banner.png index 28d46824..a71f1a21 100644 Binary files a/Banner.png and b/Banner.png differ diff --git a/docs/src/FL.md b/docs/src/FL.md index 9998df61..90eacfd0 100644 --- a/docs/src/FL.md +++ b/docs/src/FL.md @@ -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) @@ -647,13 +649,33 @@ 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]; @@ -661,7 +683,15 @@ julia> querry = [get_prices(ticker, startdt="2024-01-01", enddt="2024-03-01")["a 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]); diff --git a/docs/src/index.md b/docs/src/index.md index 4b584002..71f8c517 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -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. diff --git a/docs/src/refs.bib b/docs/src/refs.bib index ceae645f..21a35b5b 100644 --- a/docs/src/refs.bib +++ b/docs/src/refs.bib @@ -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}, diff --git a/src/Algos/SPOLC.jl b/src/Algos/SPOLC.jl new file mode 100644 index 00000000..36bac60a --- /dev/null +++ b/src/Algos/SPOLC.jl @@ -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 t1e6 + @. 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 diff --git a/src/OnlinePortfolioSelection.jl b/src/OnlinePortfolioSelection.jl index d782a32c..dadba439 100644 --- a/src/OnlinePortfolioSelection.jl +++ b/src/OnlinePortfolioSelection.jl @@ -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 @@ -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") @@ -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 @@ -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 diff --git a/test/SPOLC.jl b/test/SPOLC.jl new file mode 100644 index 00000000..e0392cdf --- /dev/null +++ b/test/SPOLC.jl @@ -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 diff --git a/test/runtests.jl b/test/runtests.jl index f05c24a8..9cc28901 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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