Coverage for book/marimo/notebooks/Experiment3.py: 100%
52 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-09 08:57 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-09 08:57 +0000
1# /// script
2# requires-python = ">=3.12"
3# dependencies = [
4# "marimo==0.24.0",
5# "numpy==2.4.6",
6# "plotly==6.9.0",
7# "polars==1.44.1",
8# "jquantstats==0.11.0",
9# "tinycta==0.14.0"
10# ]
11#
12# [tool.ty.environment]
13# # ``from preamble import ...`` resolves at runtime via the sys.path.insert in the
14# # setup cell below. ty analyses a PEP 723 script in isolation from the project, so
15# # pyproject.toml's [tool.ty.environment] never reaches this file and the path has
16# # to be declared here. Preserve this table if marimo rewrites the header.
17# extra-paths = ["."]
18# ///
20"""Experiment 3: Advanced CTA strategy with price filtering and oscillators.
22This module implements a more sophisticated trend-following strategy that
23incorporates price filtering to handle outliers and oscillators with proper
24scaling for more consistent signal generation across different assets.
25"""
27import marimo
29__generated_with = "0.23.1"
30app = marimo.App()
32with app.setup:
33 import sys
34 from pathlib import Path
36 import marimo as mo
37 import polars as pl
38 from jquantstats import Portfolio
39 from tinycta.osc import osc
40 from tinycta.util import vol_adj
42 sys.path.insert(0, str(Path(__file__).parent))
44 from preamble import date_col, load_prices
46 prices = load_prices(__file__)
47 prices_only = prices.drop(date_col)
50@app.cell(hide_code=True)
51def _():
52 mo.md(r"""# CTA 3.0""")
53 return
56@app.cell(hide_code=True)
57def _():
58 mo.md(
59 r"""
60 We use the system:
61 $$\mathrm{CashPosition}=\frac{f(\mathrm{Price})}{\mathrm{Volatility(Returns)}}$$
63 This is very problematic:
64 * Prices may live on very different scales, hence trying to find a
65 more universal function $f$ is almost impossible. The sign-function was
66 a good choice as the results don't depend on the scale of the argument.
67 * Price may come with all sorts of spikes/outliers/problems.
68 """
69 )
70 return
73@app.cell(hide_code=True)
74def _():
75 mo.md(
76 r"""
77 We need a simple price filter process
78 * We compute volatility-adjusted returns, filter them and compute prices from those returns.
79 * Don't call it Winsorizing in Switzerland. We apply Huber functions.
80 """
81 )
82 return
85@app.cell(hide_code=True)
86def _():
87 mo.md(
88 r"""
89 ### Oscillators
90 * All prices are now following a standard arithmetic Brownian
91 motion with std $1$.
92 * What we want is the difference of two moving means (exponentially weighted)
93 to have a constant std regardless of the two lengths.
94 * An oscillator is the **scaled difference of two moving averages**.
95 """
96 )
97 return
100@app.function
101def f(price: "pl.Expr", slow: int = 96, fast: int = 32, vola: int = 96, clip: float = 3) -> "pl.Expr":
102 """Return the tanh oscillator of vol-adjusted cumulative price, divided by volatility."""
103 price_adj = vol_adj(price, vola=vola, clip=clip, min_samples=300).cum_sum()
104 mu = osc(price_adj, fast=fast, slow=slow).tanh()
105 vol = price.pct_change().ewm_std(com=slow, min_samples=300)
106 return mu / vol
109@app.cell
110def _():
111 fast = mo.ui.slider(4, 192, step=4, value=32, label="Fast Moving Average")
112 slow = mo.ui.slider(4, 192, step=4, value=96, label="Slow Moving Average")
113 vola = mo.ui.slider(4, 192, step=4, value=32, label="Volatility")
114 winsor = mo.ui.slider(1.0, 6.0, step=0.1, value=4.2, label="Winsorizing")
116 mo.vstack([fast, slow, vola, winsor])
118 return fast, slow, vola, winsor
121@app.cell
122def _(fast, slow, vola, winsor):
123 signals = prices_only.select(
124 (f(pl.all(), fast=fast.value, slow=slow.value, vola=vola.value, clip=winsor.value) * 1e5)
125 .fill_nan(0.0)
126 .fill_null(0.0)
127 )
128 portfolio = Portfolio.from_cash_position(prices=prices, cash_position=signals, aum=1e8)
129 return (portfolio,)
132@app.cell
133def _(portfolio):
134 print(portfolio.stats.sharpe())
137@app.cell
138def _(portfolio):
139 fig = portfolio.plots.snapshot()
140 fig
141 return
144if __name__ == "__main__":
145 app.run()