Analytics

Rolling PCA factor models (residual/zscore/multi), rolling statistics, performance metrics (Sharpe/Sortino/Calmar/VaR/CVaR/omega/ulcer/information-ratio), technical indicators, and options payoff helpers.

Orcus.RollingPCAType
RollingPCA(window::Int, n_factors::Int, eigvecs::Matrix{Float64}, eigvals::Vector{Float64}, total_var::Float64, fitted::Bool)

Rolling PCA factor model. Maintains top n_factors eigenvectors of the return covariance matrix, estimated over a sliding window of bars. Use rolling_pca to build an unfitted one. Fields set after fit!:

  • eigvecs — [N × K] matrix, columns are factor loadings (descending variance)
  • eigvals — K eigenvalues (descending)
  • fitted — false until first fit! call
rolling_pca(20, 2)
# output

RollingPCA(window=20, n_factors=2, fitted=false)
source
Orcus.explained_varianceMethod
explained_variance(pca::RollingPCA)

Fraction of total variance explained by each factor (sums to ≤ 1), length n_factors.

pca=rolling_pca(20, 2);
R=[1.0 2.0 3.0 4.0; 2.0 4.0 6.0 8.0; 1.0 1.5 1.2 1.8];
fit!(pca, R);
explained_variance(pca)
# output

2-element Vector{Float64}:
 0.9942561416261809
 0.00574385837381898
source
Orcus.fit!Method
fit!(pca::RollingPCA, R::Matrix{Float64})

Fit PCA on return matrix R of shape [N × T], storing the top K eigenvectors (by variance explained) and their eigenvalues. Requires T ≥ 2 and N ≥ n_factors.

pca=rolling_pca(20, 2);
R=[1.0 2.0 3.0 4.0; 2.0 4.0 6.0 8.0; 1.0 1.5 1.2 1.8];
fit!(pca, R);
pca.fitted
# output

true
source
Orcus.plot_residual_corrMethod
plot_residual_corr(E::Matrix{Float64}, names::Vector{String})

Print the residual correlation matrix as a formatted table, and return it.

pca=rolling_pca(20, 2);
R=[1.0 2.0 3.0 4.0; 2.0 4.0 6.0 8.0; 1.0 1.5 1.2 1.8];
fit!(pca, R);
F, E = project(pca, R);
C = redirect_stdout(devnull) do
    plot_residual_corr(E, ["AAA","BBB","CCC"])
end;
round.(C; digits=3)
# output

3×3 Matrix{Float64}:
 1.0     0.545   0.721
 0.545   1.0    -0.182
 0.721  -0.182   1.0
source
Orcus.projectMethod
project(pca::RollingPCA, R::Matrix{Float64})

Batch projection of return matrix R [N × T], returning factor matrix F [K × T] and residual matrix E [N × T].

pca=rolling_pca(20, 2);
R=[1.0 2.0 3.0 4.0; 2.0 4.0 6.0 8.0; 1.0 1.5 1.2 1.8];
fit!(pca, R);
F, E = project(pca, R);
size(F), size(E)
# output

((2, 4), (3, 4))
source
Orcus.projectMethod
project(pca::RollingPCA, r::Vector{Float64})

Project a single bar's return vector r [N] through the fitted PCA, returning (factors, residuals) — [K] factor returns and [N] idiosyncratic residual.

pca=rolling_pca(20, 2);
R=[1.0 2.0 3.0 4.0; 2.0 4.0 6.0 8.0; 1.0 1.5 1.2 1.8];
fit!(pca, R);
factors, residuals = project(pca, R[:,1]);
size(factors), size(residuals)
# output

((2,), (3,))
source
Orcus.residual_corrMethod
residual_corr(E::Matrix{Float64})

[N × N] Pearson correlation matrix of the residual rows. After good PCA factorization this should be close to the identity matrix — off-diagonal entries indicate remaining common structure.

pca=rolling_pca(20, 2);
R=[1.0 2.0 3.0 4.0; 2.0 4.0 6.0 8.0; 1.0 1.5 1.2 1.8];
fit!(pca, R);
F, E = project(pca, R);
round.(residual_corr(E); digits=3)
# output

3×3 Matrix{Float64}:
 1.0     0.545   0.721
 0.545   1.0    -0.182
 0.721  -0.182   1.0
source
Orcus.rolling_pcaMethod
rolling_pca(window::Int, n_factors::Int)

Build an unfitted RollingPCA — call fit! before using it.

rolling_pca(20, 2)
# output

RollingPCA(window=20, n_factors=2, fitted=false)
source
Orcus.rolling_meanMethod
rolling_mean(v::Vector{Float64}, w::Int)

Rolling mean of v with window w. First w-1 entries are NaN.

rolling_mean([1.0,2.0,3.0,4.0,5.0], 3)
# output

5-element Vector{Float64}:
 NaN
 NaN
   2.0
   3.0
   4.0
source
Orcus.rolling_stdMethod
rolling_std(v::Vector{Float64}, w::Int)

Rolling standard deviation of v with window w. First w-1 entries are NaN.

rolling_std([1.0,2.0,3.0,4.0,5.0], 3)
# output

5-element Vector{Float64}:
 NaN
 NaN
   1.0
   1.0
   1.0
source
Orcus.rolling_zscoreMethod
rolling_zscore(v::Vector{Float64}, w::Int)

Rolling z-score of v with window w: (v[i] - mean(v[i-w+1:i])) / std(...). First w-1 entries are NaN. Bars with zero local standard deviation are NaN.

rolling_zscore([1.0,2.0,3.0,4.0,5.0], 3)
# output

5-element Vector{Float64}:
 NaN
 NaN
   1.0
   1.0
   1.0
source
Orcus.rolling_zscoreMethod
rolling_zscore(data::DataPoint)

Single-window z-score of the last entry in data against the whole vector's mean/std — the IndicatorGenerator-compatible form: apply_indicator(IndicatorGenerator(rolling_zscore, 30), asset, "Close", "ZScore30").

rolling_zscore([1.0,2.0,3.0])
# output

1.0
source
Orcus.annualized_returnMethod
annualized_return(equity::Vector{<:Real}; periods_per_year::Int=252)

Compound Annual Growth Rate (CAGR): the constant yearly return that would produce the same total growth as the equity curve.

round(annualized_return([100.0, 105.0, 102.0, 110.0, 108.0, 115.0]); digits=4)
# output

353.2495
source
Orcus.backtest_summaryMethod
backtest_summary(bt::Backtest)

Print a concise performance summary for a completed backtest.

Random.seed!(1234);
x=asset();
y=asset();
T=Backtest(market([x,y]),CrossOverStrategy,1000);
run_test(T);
r = redirect_stdout(devnull) do
    backtest_summary(T)
end;
isnothing(r)
# output

true
source
Orcus.bah_equityMethod
bah_equity(market::Market, cash::Real; key::String="Close")

Compute the equity curve of an equal-weight buy-and-hold strategy across all assets in market from bar 1 onward. Useful as a benchmark for information_ratio and compare_backtests.

Random.seed!(1);
M=market([asset("AAPL"), asset("GOOG")]);
eq=bah_equity(M, 1000.0);
round(eq[1]; digits=4), length(eq)
# output

(1000.0, 3651)
source
Orcus.calmar_ratioMethod
calmar_ratio(equity::Vector{<:Real}; periods_per_year::Int=252)

Annualized return divided by maximum drawdown.

round(calmar_ratio([100.0, 105.0, 102.0, 110.0, 108.0, 115.0]); digits=4)
# output

12363.7339
source
Orcus.compare_backtestsMethod
compare_backtests(bts::Vector{<:Backtest}, names::Vector{String}; benchmark::Union{Vector{Float64},Nothing}=nothing)

Print a side-by-side performance table for a collection of backtests. Columns: Strategy | CAGR | Sharpe | Sortino | Calmar | Omega | VaR95 | MaxDD | Trades. Pass benchmark (equity curve) to append an Information Ratio column.

Random.seed!(1234);
x=asset();
y=asset();
T=Backtest(market([x,y]),CrossOverStrategy,1000);
run_test(T);
T.broker.equity_history = [100.0, 105.0, 102.0, 110.0, 108.0, 115.0];
r = redirect_stdout(devnull) do
    compare_backtests([T], ["CrossOver"])
end;
isnothing(r)
# output

true
source
Orcus.cross_section_rankMethod
cross_section_rank(v::Vector{Float64})

Integer ranks 1…N (1 = smallest value). Useful for factor-based long/short construction.

cross_section_rank([30.0,10.0,20.0])
# output

3-element Vector{Int64}:
 3
 1
 2
source
Orcus.cross_section_zscoreMethod
cross_section_zscore(v::Vector{Float64})

Normalize a cross-sectional score vector to zero mean and unit variance. Returns zeros if the vector has zero standard deviation.

cross_section_zscore([1.0,2.0,3.0])
# output

3-element Vector{Float64}:
 -1.0
  0.0
  1.0
source
Orcus.cvarMethod
cvar(equity::Vector{<:Real}; confidence::Float64=0.95)

Conditional Value at Risk (Expected Shortfall): the mean return on the worst (1-confidence) fraction of days. More conservative than VaR. Returned as a negative number.

round(cvar([100.0, 105.0, 102.0, 110.0, 108.0, 115.0]); digits=4)
# output

-0.0286
source
Orcus.extended_summaryMethod
extended_summary(bt::Backtest; benchmark::Union{Vector{Float64},Nothing}=nothing, periods_per_year::Int=infer_periods_per_year(bt.broker.market))

Print a detailed performance summary including CAGR, Sortino, Calmar, Omega, VaR, CVaR, and Ulcer Index. Pass a benchmark equity curve (e.g. from bah_equity) to also print the Information Ratio.

Random.seed!(1234);
x=asset();
y=asset();
T=Backtest(market([x,y]),CrossOverStrategy,1000);
run_test(T);
T.broker.equity_history = [100.0, 105.0, 102.0, 110.0, 108.0, 115.0];
r = redirect_stdout(devnull) do
    extended_summary(T)
end;
isnothing(r)
# output

true
source
Orcus.infer_periods_per_yearMethod
infer_periods_per_year(M::Market)

Bars per year implied by M's time axis (252 if it has none).

Random.seed!(1);
infer_periods_per_year(market([asset()]))
# output

252
source
Orcus.infer_periods_per_yearMethod
infer_periods_per_year(axis::Vector{DateTime})

Bars per year implied by the median spacing of a time axis: intraday bars scale by bars per trading day, daily → 252, weekly → 52, monthly → 12, coarser → 1.

using Dates;
infer_periods_per_year(DateTime(2000,1,1) .+ Day.(0:1:400))
# output

252
source
Orcus.information_ratioMethod
information_ratio(equity::Vector{<:Real}, benchmark_equity::Vector{<:Real}; periods_per_year::Int=252)

Information Ratio: annualised active return divided by tracking error vs a benchmark equity curve. Values above 0.5 are considered good; above 1.0 exceptional.

round(information_ratio([100.0, 105.0, 102.0, 110.0, 108.0, 115.0], [100.0, 104.0, 103.0, 107.0, 106.0, 112.0]); digits=4)
# output

4.2698
source
Orcus.max_drawdownMethod
max_drawdown(equity::Vector{<:Real})

Maximum peak-to-trough drawdown as a fraction (0 to 1).

round(max_drawdown([100.0, 105.0, 102.0, 110.0, 108.0, 115.0]); digits=4)
# output

0.0286
source
Orcus.omega_ratioMethod
omega_ratio(equity::Vector{<:Real}; threshold::Float64=0.0, periods_per_year::Int=252)

Omega ratio: probability-weighted ratio of gains to losses above/below threshold (annualised). Values > 1 indicate more gain than loss.

round(omega_ratio([100.0, 105.0, 102.0, 110.0, 108.0, 115.0]); digits=4)
# output

4.1333
source
Orcus.profit_factorMethod
profit_factor(equity::Vector{<:Real})

Gross profit / gross loss computed from the equity-curve return series. Returns Inf if there are no losing bars.

round(profit_factor([100.0, 105.0, 102.0, 110.0, 108.0, 115.0]); digits=4)
# output

4.0
source
Orcus.sharpe_ratioMethod
sharpe_ratio(equity::Vector{<:Real}; rf::Float64=0.0, periods_per_year::Int=252)

Annualized Sharpe ratio computed from an equity curve. rf is the annualized risk-free rate (default 0). Returns NaN if there is insufficient data or zero volatility.

round(sharpe_ratio([100.0, 105.0, 102.0, 110.0, 108.0, 115.0]); digits=4)
# output

9.4412
source
Orcus.sortino_ratioMethod
sortino_ratio(equity::Vector{<:Real}; rf::Float64=0.0, periods_per_year::Int=252)

Annualized Sortino ratio: like Sharpe but penalises downside volatility only.

round(sortino_ratio([100.0, 105.0, 102.0, 110.0, 108.0, 115.0]); digits=4)
# output

30.7092
source
Orcus.ulcer_indexMethod
ulcer_index(equity::Vector{<:Real})

Ulcer Index: root mean square of all percentage drawdowns from peak. Captures both depth and duration of drawdowns. Lower is better.

round(ulcer_index([100.0, 105.0, 102.0, 110.0, 108.0, 115.0]); digits=4)
# output

1.3826
source
Orcus.value_at_riskMethod
value_at_risk(equity::Vector{<:Real}; confidence::Float64=0.95)

Daily Value at Risk at the given confidence level: the return threshold such that losses exceed this level on (1-confidence) fraction of days. Returned as a negative number (a loss).

round(value_at_risk([100.0, 105.0, 102.0, 110.0, 108.0, 115.0]); digits=4)
# output

-0.0265
source
Orcus.win_rate_barsMethod
win_rate_bars(equity::Vector{<:Real})

Fraction of bars in which the equity curve increased (a rough proxy for trade-level win rate when the strategy has one position at a time).

round(win_rate_bars([100.0, 105.0, 102.0, 110.0, 108.0, 115.0]); digits=4)
# output

0.6
source
Orcus.atrFunction
atr(high::Vector{Float64}, low::Vector{Float64}, close::Vector{Float64}, w::Int=14)

Average True Range using Wilder's smoothing: True Range = max(H-L, |H-prevC|, |L-prevC|). Returns NaN for the first w+1 bars. Not directly IndicatorGenerator-compatible (needs three input series) — call directly in strategy logic.

h=[10.0,11.0,10.5,12.0,11.5,13.0,12.5];
l=[9.0,10.0,9.5,11.0,10.5,12.0,11.5];
c=[9.5,10.5,10.0,11.5,11.0,12.5,12.0];
round.(atr(h,l,c,3); digits=4)
# output

7-element Vector{Float64}:
 NaN
 NaN
 NaN
   1.5
   1.3333
   1.5556
   1.3704
source
Orcus.emaMethod
ema(v::Vector{Float64}, w::Int)

Exponential moving average with span w (α = 2/(w+1)). Initialised with the SMA of the first w bars; NaN for earlier entries. Compatible with IndicatorGenerator: apply_indicator(IndicatorGenerator(ema, 20), asset, "Close", "EMA20").

round.(ema([1.0,2.0,3.0,4.0,5.0,6.0,7.0], 3); digits=4)
# output

7-element Vector{Float64}:
 NaN
 NaN
   2.0
   3.0
   4.0
   5.0
   6.0
source
Orcus.emaMethod
ema(data::DataPoint)

Single-window EMA over data, weighting each successive entry by the EMA formula — the IndicatorGenerator-compatible form (uses the window stored in IndicatorGenerator).

round(ema([1.0,2.0,3.0]); digits=4)
# output

2.25
source
Orcus.rsiFunction
rsi(v::Vector{Float64}, w::Int=14)

Relative Strength Index using Wilder's smoothing. Returns values in [0, 100]; NaN for the first w bars. Compatible with IndicatorGenerator: apply_indicator(IndicatorGenerator(rsi, 14), asset, "Close", "RSI14").

v=[1.0,2.0,1.5,2.5,3.0,2.0,3.5,4.0,3.0,5.0,4.5,6.0,5.5,7.0,6.5,8.0];
round.(rsi(v, 5); digits=4)
# output

16-element Vector{Float64}:
 NaN
 NaN
 NaN
 NaN
 NaN
  62.5
  74.4681
  77.4648
  59.8911
  74.4065
  66.8466
  75.9934
  68.1582
  77.0367
  69.0182
  77.7161
source
Orcus.bsm_callMethod
bsm_call(S::Float64, K::Float64, T::Float64, r::Float64, sigma::Float64)

Black-Scholes-Merton European call price for underlying price S, strike K, time to expiry T in years, annualized risk-free rate r, and annualized volatility sigma. Returns intrinsic value max(S-K, 0) when T ≤ 0.

round(bsm_call(100.0, 100.0, 0.25, 0.04, 0.2); digits=4)
# output

4.4852
source
Orcus.bsm_deltaMethod
bsm_delta(S::Float64, K::Float64, T::Float64, r::Float64, sigma::Float64; type::Symbol=:call)

Black-Scholes-Merton option delta. type is :call (default) or :put. Call delta ∈ (0, 1); put delta ∈ (-1, 0).

round(bsm_delta(100.0, 100.0, 0.25, 0.04, 0.2); digits=4)
# output

0.5596
source
Orcus.bsm_putMethod
bsm_put(S::Float64, K::Float64, T::Float64, r::Float64, sigma::Float64)

Black-Scholes-Merton European put price (via put-call parity).

round(bsm_put(100.0, 100.0, 0.25, 0.04, 0.2); digits=4)
# output

3.4902
source
Orcus.realized_volFunction
realized_vol(prices::Vector{Float64}, window::Int=20)

Annualized realized volatility estimated from the last window log-returns of a price series. Returns NaN if insufficient data.

prices=[100.0,101.0,99.0,102.0,103.0,101.0,104.0,105.0,103.0,106.0,107.0,105.0,108.0,109.0,107.0,110.0,111.0,109.0,112.0,113.0,111.0];
round(realized_vol(prices, 20); digits=4)
# output

0.3143
source