Coverage for book/marimo/notebooks/Experiment2.py: 100%

42 statements  

« 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# ] 

10# 

11# [tool.ty.environment] 

12# # ``from preamble import ...`` resolves at runtime via the sys.path.insert in the 

13# # setup cell below. ty analyses a PEP 723 script in isolation from the project, so 

14# # pyproject.toml's [tool.ty.environment] never reaches this file and the path has 

15# # to be declared here. Preserve this table if marimo rewrites the header. 

16# extra-paths = ["."] 

17# /// 

18 

19"""Experiment 2: Improved CTA strategy with volatility scaling. 

20 

21This module enhances the basic trend-following strategy by incorporating 

22volatility scaling to adjust position sizes based on market conditions. 

23""" 

24 

25import marimo 

26 

27__generated_with = "0.23.1" 

28app = marimo.App() 

29 

30with app.setup: 

31 import sys 

32 from pathlib import Path 

33 

34 import marimo as mo 

35 import polars as pl 

36 from jquantstats import Portfolio 

37 

38 sys.path.insert(0, str(Path(__file__).parent)) 

39 

40 from preamble import date_col, load_prices 

41 

42 prices = load_prices(__file__) 

43 prices_only = prices.drop(date_col) 

44 

45 

46@app.cell(hide_code=True) 

47def _(): 

48 mo.md(r"""# CTA 2.0""") 

49 return 

50 

51 

52@app.function 

53def f(price: "pl.Expr", fast: int = 32, slow: int = 96, volatility: int = 32) -> "pl.Expr": 

54 """Return the volatility-scaled EWM crossover signal.""" 

55 return ( 

56 price.ewm_mean(com=fast, min_samples=300) - price.ewm_mean(com=slow, min_samples=300) 

57 ).sign() / price.pct_change().ewm_std(com=volatility, min_samples=300) 

58 

59 

60@app.cell 

61def _(): 

62 fast = mo.ui.slider(4, 192, step=4, value=32, label="Fast Moving Average") 

63 slow = mo.ui.slider(4, 192, step=4, value=96, label="Slow Moving Average") 

64 vola = mo.ui.slider(4, 192, step=4, value=32, label="Volatility") 

65 

66 mo.vstack([fast, slow, vola]) 

67 

68 return fast, slow, vola 

69 

70 

71@app.cell 

72def _(fast, slow, vola): 

73 signals = prices_only.select( 

74 f(pl.all(), fast=fast.value, slow=slow.value, volatility=vola.value).fill_null(0.0) * 1e5 

75 ) 

76 portfolio = Portfolio.from_cash_position(prices=prices, cash_position=signals, aum=1e8) 

77 return (portfolio,) 

78 

79 

80@app.cell 

81def _(portfolio): 

82 print(portfolio.stats.sharpe()) 

83 

84 

85@app.cell(hide_code=True) 

86def _(): 

87 mo.md( 

88 r""" 

89 * This is a **univariate** trading system, we map the (real) price of an asset to its (cash)position 

90 * Only 3 **free parameters** used here. 

91 * Scaling the bet-size by volatility has improved the situation. 

92 """ 

93 ) 

94 return 

95 

96 

97@app.cell(hide_code=True) 

98def _(): 

99 mo.md( 

100 r""" 

101 Results do not look terrible but... 

102 * No concept of risk integrated 

103 

104 Often hedge funds outsource the risk management to some board or committee 

105 and develop machinery for more systematic **parameter-hacking**. 

106 """ 

107 ) 

108 return 

109 

110 

111@app.cell 

112def _(portfolio): 

113 portfolio.plots.snapshot() 

114 return 

115 

116 

117if __name__ == "__main__": 

118 app.run()