galaaz 2.1.8 → 2.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/Rakefile +20 -2
- data/bin/check_gemfile_lock_version +46 -0
- data/bin/release_bump +26 -0
- data/blogs/README.md +4 -0
- data/blogs/galaaz_2_0/galaaz_2_0.Rmd +385 -0
- data/blogs/galaaz_2_0/galaaz_2_0.md +409 -0
- data/blogs/galaaz_2_0/galaaz_2_0.tex +756 -0
- data/blogs/galaaz_2_0/images/galaaz-header.png +0 -0
- data/blogs/galaaz_2_0/images/galaaz-lockup-stacked.png +0 -0
- data/blogs/galaaz_ggplot/galaaz_ggplot.Rmd +14 -1
- data/blogs/galaaz_ggplot/galaaz_ggplot.md +123 -103
- data/blogs/galaaz_ggplot/galaaz_ggplot.tex +60 -23
- data/blogs/galaaz_ggplot/images/galaaz-lockup-stacked.png +0 -0
- data/blogs/gknit/gknit.Rmd +16 -1
- data/blogs/gknit/gknit.md +13 -1
- data/blogs/gknit/gknit.tex +64 -23
- data/blogs/gknit/gknit_files/figure-html/bubble-1.png +0 -0
- data/blogs/gknit/gknit_files/figure-html/diverging_bar.png +0 -0
- data/blogs/gknit/gknit_files/figure-latex/bubble-1.png +0 -0
- data/blogs/gknit/images/galaaz-lockup-stacked.png +0 -0
- data/blogs/manual/images/galaaz-lockup-stacked.png +0 -0
- data/blogs/manual/manual.Rmd +32 -11
- data/blogs/manual/manual.md +30 -23
- data/blogs/manual/manual.tex +88 -66
- data/blogs/manual/manual_files/figure-html/bubble-1.png +0 -0
- data/blogs/manual/manual_files/figure-latex/bubble-1.png +0 -0
- data/blogs/nse_dplyr/images/galaaz-lockup-stacked.png +0 -0
- data/blogs/nse_dplyr/nse_dplyr.Rmd +14 -1
- data/blogs/nse_dplyr/nse_dplyr.md +697 -649
- data/blogs/nse_dplyr/nse_dplyr.tex +61 -24
- data/blogs/oh_my/images/galaaz-lockup-stacked.png +0 -0
- data/blogs/oh_my/oh_my.Rmd +14 -1
- data/blogs/oh_my/oh_my.md +36 -26
- data/blogs/oh_my/oh_my.tex +95 -58
- data/blogs/r_on_rails_ledger/images/00_portfolio_page.png +0 -0
- data/blogs/r_on_rails_ledger/images/01_results_panel.png +0 -0
- data/blogs/r_on_rails_ledger/images/02_density_tail_risk.png +0 -0
- data/blogs/r_on_rails_ledger/images/03_mc_cone.png +0 -0
- data/blogs/r_on_rails_ledger/images/04_rolling_var.png +0 -0
- data/blogs/r_on_rails_ledger/images/galaaz-lockup-stacked.png +0 -0
- data/blogs/r_on_rails_ledger/r_on_rails_ledger.Rmd +354 -0
- data/blogs/r_on_rails_ledger/r_on_rails_ledger.md +365 -0
- data/blogs/r_on_rails_ledger/r_on_rails_ledger.tex +670 -0
- data/blogs/ruby_plot/images/galaaz-lockup-stacked.png +0 -0
- data/blogs/ruby_plot/ruby_plot.Rmd +14 -1
- data/blogs/ruby_plot/ruby_plot.md +11 -1
- data/blogs/ruby_plot/ruby_plot.tex +60 -23
- data/blogs/ruby_plot/ruby_plot_files/figure-html/facets_with_jitter.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-html/final_violin_plot.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-html/violin_with_jitter.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/facets_with_jitter.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/final_violin_plot.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/figure-latex/violin_with_jitter.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/ruby_plot_files/figure-latex/facets_with_jitter.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/ruby_plot_files/figure-latex/final_violin_plot.png +0 -0
- data/blogs/ruby_plot/ruby_plot_files/ruby_plot_files/figure-latex/violin_with_jitter.png +0 -0
- data/lib/galaaz/cli.rb +47 -3
- data/logos/icon-font/README.md +27 -0
- data/logos/icon-font/build_font.py +130 -0
- data/logos/icon-font/galaaz-mark.svg +34 -0
- data/script/omarchy/README.md +8 -1
- data/script/omarchy/fonts/galaaz.ttf +0 -0
- data/script/omarchy/install-galaaz.sh +8 -1
- data/script/omarchy/omarchy-menu.jsonc +21 -9
- data/sty/galaaz-header.png +0 -0
- data/sty/galaaz-headers-from-p3.tex +4 -0
- data/sty/galaaz.sty +54 -23
- data/version.rb +1 -1
- metadata +30 -9
- data/blogs/galaaz_ggplot/galaaz_ggplot.log +0 -745
- data/blogs/gknit/gknit_files/gknit_files/figure-latex/bubble-1.png +0 -0
- data/blogs/manual/manual.log +0 -1530
- data/blogs/manual/manual_files/manual_files/figure-latex/bubble-1.png +0 -0
- data/blogs/nse_dplyr/nse_dplyr.log +0 -824
- data/blogs/oh_my/oh_my.log +0 -974
- data/blogs/ruby_plot/ruby_plot.log +0 -887
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
% Options for packages loaded elsewhere
|
|
2
|
+
\PassOptionsToPackage{unicode}{hyperref}
|
|
3
|
+
\PassOptionsToPackage{hyphens}{url}
|
|
4
|
+
\documentclass[
|
|
5
|
+
11pt,
|
|
6
|
+
]{article}
|
|
7
|
+
\usepackage{xcolor}
|
|
8
|
+
\usepackage[margin=1in]{geometry}
|
|
9
|
+
\usepackage{amsmath,amssymb}
|
|
10
|
+
\setcounter{secnumdepth}{5}
|
|
11
|
+
\usepackage{iftex}
|
|
12
|
+
\ifPDFTeX
|
|
13
|
+
\usepackage[T1]{fontenc}
|
|
14
|
+
\usepackage[utf8]{inputenc}
|
|
15
|
+
\usepackage{textcomp} % provide euro and other symbols
|
|
16
|
+
\else % if luatex or xetex
|
|
17
|
+
\usepackage{unicode-math} % this also loads fontspec
|
|
18
|
+
\defaultfontfeatures{Scale=MatchLowercase}
|
|
19
|
+
\defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1}
|
|
20
|
+
\fi
|
|
21
|
+
\usepackage{lmodern}
|
|
22
|
+
\ifPDFTeX\else
|
|
23
|
+
% xetex/luatex font selection
|
|
24
|
+
\fi
|
|
25
|
+
% Use upquote if available, for straight quotes in verbatim environments
|
|
26
|
+
\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
|
|
27
|
+
\IfFileExists{microtype.sty}{% use microtype if available
|
|
28
|
+
\usepackage[]{microtype}
|
|
29
|
+
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
|
|
30
|
+
}{}
|
|
31
|
+
\makeatletter
|
|
32
|
+
\@ifundefined{KOMAClassName}{% if non-KOMA class
|
|
33
|
+
\IfFileExists{parskip.sty}{%
|
|
34
|
+
\usepackage{parskip}
|
|
35
|
+
}{% else
|
|
36
|
+
\setlength{\parindent}{0pt}
|
|
37
|
+
\setlength{\parskip}{6pt plus 2pt minus 1pt}}
|
|
38
|
+
}{% if KOMA class
|
|
39
|
+
\KOMAoptions{parskip=half}}
|
|
40
|
+
\makeatother
|
|
41
|
+
\usepackage{color}
|
|
42
|
+
\usepackage{fancyvrb}
|
|
43
|
+
\newcommand{\VerbBar}{|}
|
|
44
|
+
\newcommand{\VERB}{\Verb[commandchars=\\\{\}]}
|
|
45
|
+
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\{\}}
|
|
46
|
+
% Add ',fontsize=\small' for more characters per line
|
|
47
|
+
\usepackage{framed}
|
|
48
|
+
\definecolor{shadecolor}{RGB}{248,248,248}
|
|
49
|
+
\newenvironment{Shaded}{\begin{snugshade}}{\end{snugshade}}
|
|
50
|
+
\newcommand{\AlertTok}[1]{\textcolor[rgb]{0.94,0.16,0.16}{#1}}
|
|
51
|
+
\newcommand{\AnnotationTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textbf{\textit{#1}}}}
|
|
52
|
+
\newcommand{\AttributeTok}[1]{\textcolor[rgb]{0.13,0.29,0.53}{#1}}
|
|
53
|
+
\newcommand{\BaseNTok}[1]{\textcolor[rgb]{0.00,0.00,0.81}{#1}}
|
|
54
|
+
\newcommand{\BuiltInTok}[1]{#1}
|
|
55
|
+
\newcommand{\CharTok}[1]{\textcolor[rgb]{0.31,0.60,0.02}{#1}}
|
|
56
|
+
\newcommand{\CommentTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textit{#1}}}
|
|
57
|
+
\newcommand{\CommentVarTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textbf{\textit{#1}}}}
|
|
58
|
+
\newcommand{\ConstantTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{#1}}
|
|
59
|
+
\newcommand{\ControlFlowTok}[1]{\textcolor[rgb]{0.13,0.29,0.53}{\textbf{#1}}}
|
|
60
|
+
\newcommand{\DataTypeTok}[1]{\textcolor[rgb]{0.13,0.29,0.53}{#1}}
|
|
61
|
+
\newcommand{\DecValTok}[1]{\textcolor[rgb]{0.00,0.00,0.81}{#1}}
|
|
62
|
+
\newcommand{\DocumentationTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textbf{\textit{#1}}}}
|
|
63
|
+
\newcommand{\ErrorTok}[1]{\textcolor[rgb]{0.64,0.00,0.00}{\textbf{#1}}}
|
|
64
|
+
\newcommand{\ExtensionTok}[1]{#1}
|
|
65
|
+
\newcommand{\FloatTok}[1]{\textcolor[rgb]{0.00,0.00,0.81}{#1}}
|
|
66
|
+
\newcommand{\FunctionTok}[1]{\textcolor[rgb]{0.13,0.29,0.53}{\textbf{#1}}}
|
|
67
|
+
\newcommand{\ImportTok}[1]{#1}
|
|
68
|
+
\newcommand{\InformationTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textbf{\textit{#1}}}}
|
|
69
|
+
\newcommand{\KeywordTok}[1]{\textcolor[rgb]{0.13,0.29,0.53}{\textbf{#1}}}
|
|
70
|
+
\newcommand{\NormalTok}[1]{#1}
|
|
71
|
+
\newcommand{\OperatorTok}[1]{\textcolor[rgb]{0.81,0.36,0.00}{\textbf{#1}}}
|
|
72
|
+
\newcommand{\OtherTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{#1}}
|
|
73
|
+
\newcommand{\PreprocessorTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textit{#1}}}
|
|
74
|
+
\newcommand{\RegionMarkerTok}[1]{#1}
|
|
75
|
+
\newcommand{\SpecialCharTok}[1]{\textcolor[rgb]{0.81,0.36,0.00}{\textbf{#1}}}
|
|
76
|
+
\newcommand{\SpecialStringTok}[1]{\textcolor[rgb]{0.31,0.60,0.02}{#1}}
|
|
77
|
+
\newcommand{\StringTok}[1]{\textcolor[rgb]{0.31,0.60,0.02}{#1}}
|
|
78
|
+
\newcommand{\VariableTok}[1]{\textcolor[rgb]{0.00,0.00,0.00}{#1}}
|
|
79
|
+
\newcommand{\VerbatimStringTok}[1]{\textcolor[rgb]{0.31,0.60,0.02}{#1}}
|
|
80
|
+
\newcommand{\WarningTok}[1]{\textcolor[rgb]{0.56,0.35,0.01}{\textbf{\textit{#1}}}}
|
|
81
|
+
\usepackage{longtable,booktabs,array}
|
|
82
|
+
\newcounter{none} % for unnumbered tables
|
|
83
|
+
\usepackage{calc} % for calculating minipage widths
|
|
84
|
+
% Correct order of tables after \paragraph or \subparagraph
|
|
85
|
+
\usepackage{etoolbox}
|
|
86
|
+
\makeatletter
|
|
87
|
+
\patchcmd\longtable{\par}{\if@noskipsec\mbox{}\fi\par}{}{}
|
|
88
|
+
\makeatother
|
|
89
|
+
% Allow footnotes in longtable head/foot
|
|
90
|
+
\IfFileExists{footnotehyper.sty}{\usepackage{footnotehyper}}{\usepackage{footnote}}
|
|
91
|
+
\makesavenoteenv{longtable}
|
|
92
|
+
\usepackage{graphicx}
|
|
93
|
+
\makeatletter
|
|
94
|
+
\newsavebox\pandoc@box
|
|
95
|
+
\newcommand*\pandocbounded[1]{% scales image to fit in text height/width
|
|
96
|
+
\sbox\pandoc@box{#1}%
|
|
97
|
+
\Gscale@div\@tempa{\textheight}{\dimexpr\ht\pandoc@box+\dp\pandoc@box\relax}%
|
|
98
|
+
\Gscale@div\@tempb{\linewidth}{\wd\pandoc@box}%
|
|
99
|
+
\ifdim\@tempb\p@<\@tempa\p@\let\@tempa\@tempb\fi% select the smaller of both
|
|
100
|
+
\ifdim\@tempa\p@<\p@\scalebox{\@tempa}{\usebox\pandoc@box}%
|
|
101
|
+
\else\usebox{\pandoc@box}%
|
|
102
|
+
\fi%
|
|
103
|
+
}
|
|
104
|
+
% Set default figure placement to htbp
|
|
105
|
+
\def\fps@figure{htbp}
|
|
106
|
+
\makeatother
|
|
107
|
+
\setlength{\emergencystretch}{3em} % prevent overfull lines
|
|
108
|
+
\providecommand{\tightlist}{%
|
|
109
|
+
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
|
|
110
|
+
% usar portugues do Brasil
|
|
111
|
+
% \usepackage[brazilian]{babel}
|
|
112
|
+
\usepackage[utf8]{inputenc}
|
|
113
|
+
|
|
114
|
+
\usepackage{geometry}
|
|
115
|
+
\geometry{a4paper, top=1.05in, headheight=30pt, headsep=14pt}
|
|
116
|
+
|
|
117
|
+
% needed for kableExtra
|
|
118
|
+
\usepackage{longtable}
|
|
119
|
+
\usepackage{multirow}
|
|
120
|
+
\usepackage[table]{xcolor}
|
|
121
|
+
\usepackage{wrapfig}
|
|
122
|
+
\usepackage{float}
|
|
123
|
+
\usepackage{colortbl}
|
|
124
|
+
\usepackage{pdflscape}
|
|
125
|
+
\usepackage{tabu}
|
|
126
|
+
\usepackage{threeparttable}
|
|
127
|
+
\usepackage[normalem]{ulem}
|
|
128
|
+
|
|
129
|
+
\usepackage{bbm}
|
|
130
|
+
\usepackage{booktabs}
|
|
131
|
+
\usepackage{expex}
|
|
132
|
+
|
|
133
|
+
\usepackage{graphicx}
|
|
134
|
+
|
|
135
|
+
\usepackage{fancyhdr}
|
|
136
|
+
% Header: small Galaaz mark (left); page number + section title (right),
|
|
137
|
+
% matching classic LaTeX style: "4 AN EXTENSION TO THE EXAMPLE"
|
|
138
|
+
% (upright page number, uppercase italic section name).
|
|
139
|
+
% Logo paths cover repo blogs (../../sty/), installed ~/sty/, and local images/.
|
|
140
|
+
% Blogs may delay headers with \renewcommand{\galaazheaderfrompage}{3} after this sty
|
|
141
|
+
% (e.g. skip title/TOC and a logo splash page).
|
|
142
|
+
\newcommand{\galaazheaderfrompage}{1}
|
|
143
|
+
\newcommand{\galaazheaderlogo}{%
|
|
144
|
+
\raisebox{-0.12\height}{%
|
|
145
|
+
\IfFileExists{../../sty/galaaz-header.png}{%
|
|
146
|
+
\includegraphics[height=0.32in]{../../sty/galaaz-header.png}%
|
|
147
|
+
}{%
|
|
148
|
+
\IfFileExists{galaaz-header.png}{%
|
|
149
|
+
\includegraphics[height=0.32in]{galaaz-header.png}%
|
|
150
|
+
}{%
|
|
151
|
+
\IfFileExists{images/galaaz-header.png}{%
|
|
152
|
+
\includegraphics[height=0.32in]{images/galaaz-header.png}%
|
|
153
|
+
}{}%
|
|
154
|
+
}%
|
|
155
|
+
}%
|
|
156
|
+
}%
|
|
157
|
+
}
|
|
158
|
+
% Section title only in marks (no \thesection); page number is separate.
|
|
159
|
+
\renewcommand{\sectionmark}[1]{\markboth{#1}{}}
|
|
160
|
+
\renewcommand{\subsectionmark}[1]{}% keep the current section in the header
|
|
161
|
+
\newcommand{\galaazmaybeheaderlogo}{%
|
|
162
|
+
\ifnum\value{page}<\galaazheaderfrompage\relax\else\galaazheaderlogo\fi}
|
|
163
|
+
\newcommand{\galaazmayberighthead}{%
|
|
164
|
+
\ifnum\value{page}<\galaazheaderfrompage\relax
|
|
165
|
+
\else
|
|
166
|
+
\textup{\thepage}\hspace{1em}\textit{\MakeUppercase{\leftmark}}%
|
|
167
|
+
\fi
|
|
168
|
+
}
|
|
169
|
+
\pagestyle{fancy}
|
|
170
|
+
\fancyhf{}
|
|
171
|
+
\fancyhead[L]{\galaazmaybeheaderlogo}
|
|
172
|
+
\fancyhead[R]{\galaazmayberighthead}
|
|
173
|
+
\renewcommand{\headrulewidth}{0.3pt}
|
|
174
|
+
\makeatletter
|
|
175
|
+
\renewcommand{\headrule}{%
|
|
176
|
+
\ifnum\value{page}<\galaazheaderfrompage\relax
|
|
177
|
+
\else
|
|
178
|
+
\hrule\@height\headrulewidth\@width\headwidth
|
|
179
|
+
\vskip-\headrulewidth
|
|
180
|
+
\fi
|
|
181
|
+
}
|
|
182
|
+
\makeatother
|
|
183
|
+
% First page often uses plain; same conditional header logic.
|
|
184
|
+
\fancypagestyle{plain}{%
|
|
185
|
+
\fancyhf{}%
|
|
186
|
+
\fancyhead[L]{\galaazmaybeheaderlogo}%
|
|
187
|
+
\fancyhead[R]{\galaazmayberighthead}%
|
|
188
|
+
\renewcommand{\headrulewidth}{0.3pt}%
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
% make available command lastpage
|
|
192
|
+
\usepackage{lastpage}
|
|
193
|
+
|
|
194
|
+
% default fontsize 11pt better to add
|
|
195
|
+
% fontsize on the yaml header
|
|
196
|
+
% \usepackage[fontsize=11pt]{scrextend}
|
|
197
|
+
|
|
198
|
+
% comandos para formatar uma tabela
|
|
199
|
+
\usepackage{array}
|
|
200
|
+
\newcolumntype{L}[1]{>{\raggedright\let\newline\\\arraybackslash\hspace{0pt}}m{#1}}
|
|
201
|
+
\newcolumntype{C}[1]{>{\centering\let\newline\\\arraybackslash\hspace{0pt}}m{#1}}
|
|
202
|
+
\newcolumntype{R}[1]{>{\raggedleft\let\newline\\\arraybackslash\hspace{0pt}}m{#1}}
|
|
203
|
+
|
|
204
|
+
% necessário if we need to import other latex documents
|
|
205
|
+
\usepackage{import}
|
|
206
|
+
|
|
207
|
+
% Command to import an R variable to latex
|
|
208
|
+
\newcommand{\RtoLatex}[2]{\newcommand{#1}{#2}}
|
|
209
|
+
|
|
210
|
+
% Soft-wrap Pandoc highlighted code/output boxes (Shaded + Highlighting).
|
|
211
|
+
% This runs after Pandoc's default \DefineVerbatimEnvironment{Highlighting}
|
|
212
|
+
% (header includes come later in the generated .tex). Prefer manual line
|
|
213
|
+
% breaks in Rmd sources; breaklines is a safety net for leftovers/output.
|
|
214
|
+
% Requires TinyTeX/TeX Live package: tlmgr install fvextra
|
|
215
|
+
\IfFileExists{fvextra.sty}{%
|
|
216
|
+
\usepackage{fvextra}%
|
|
217
|
+
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{%
|
|
218
|
+
breaklines=true,
|
|
219
|
+
breakanywhere=true,
|
|
220
|
+
breakindent=1.5em,
|
|
221
|
+
fontsize=\small,
|
|
222
|
+
commandchars=\\\{\}%
|
|
223
|
+
}%
|
|
224
|
+
}{%
|
|
225
|
+
% fancyvrb is already loaded by Pandoc; shrink code so more fits per line.
|
|
226
|
+
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{%
|
|
227
|
+
fontsize=\small,
|
|
228
|
+
commandchars=\\\{\}%
|
|
229
|
+
}%
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
%
|
|
233
|
+
%\newcommand{\atraso}[1]{\color{red} \textbf {Tempo desde a Assinatura do Contrato: #1 dias}}
|
|
234
|
+
% No header on title/TOC (p.1) or splash logo page (p.2);
|
|
235
|
+
% logo + page number + section title from page 3 onward.
|
|
236
|
+
% Load after galaaz.sty.
|
|
237
|
+
\renewcommand{\galaazheaderfrompage}{3}
|
|
238
|
+
\usepackage{bookmark}
|
|
239
|
+
\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available
|
|
240
|
+
\urlstyle{same}
|
|
241
|
+
\hypersetup{
|
|
242
|
+
pdftitle={R-on-Rails: ship the ledger on Rails{,} keep the science in R},
|
|
243
|
+
pdfauthor={Rodrigo Botafogo},
|
|
244
|
+
hidelinks,
|
|
245
|
+
pdfcreator={LaTeX via pandoc}}
|
|
246
|
+
|
|
247
|
+
\title{R-on-Rails: ship the ledger on Rails, keep the science in R}
|
|
248
|
+
\usepackage{etoolbox}
|
|
249
|
+
\makeatletter
|
|
250
|
+
\providecommand{\subtitle}[1]{% add subtitle to \maketitle
|
|
251
|
+
\apptocmd{\@title}{\par {\large #1 \par}}{}{}
|
|
252
|
+
}
|
|
253
|
+
\makeatother
|
|
254
|
+
\subtitle{A portfolio stress tester that puts SQLite, Hotwire, and GNU R
|
|
255
|
+
on one desk}
|
|
256
|
+
\author{Rodrigo Botafogo}
|
|
257
|
+
\date{2026}
|
|
258
|
+
|
|
259
|
+
\begin{document}
|
|
260
|
+
\maketitle
|
|
261
|
+
|
|
262
|
+
{
|
|
263
|
+
\setcounter{tocdepth}{2}
|
|
264
|
+
\tableofcontents
|
|
265
|
+
}
|
|
266
|
+
\begin{center}\includegraphics[width=0.45\linewidth]{images/galaaz-lockup-stacked} \end{center}
|
|
267
|
+
|
|
268
|
+
\section{Two audiences, one gap}\label{two-audiences-one-gap}
|
|
269
|
+
|
|
270
|
+
\textbf{If you live in R}, you already have the hard part: VaR, density
|
|
271
|
+
estimates, Monte Carlo, Bioconductor, the whole CRAN catalog. What is
|
|
272
|
+
still painful is turning a notebook into a \textbf{product}---logins, a
|
|
273
|
+
database, background jobs, a page that updates when the job finishes,
|
|
274
|
+
and a deploy story that is not a pile of glue scripts.
|
|
275
|
+
|
|
276
|
+
\textbf{If you live in Ruby on Rails}, you already have the product
|
|
277
|
+
shell. What is still painful is serious statistics without standing up a
|
|
278
|
+
Python microservice fleet, learning another ORM, and ferrying CSV files
|
|
279
|
+
between processes.
|
|
280
|
+
|
|
281
|
+
\textbf{R-on-Rails} is the name we give to a simple division of labor:
|
|
282
|
+
|
|
283
|
+
\begin{itemize}
|
|
284
|
+
\tightlist
|
|
285
|
+
\item
|
|
286
|
+
\textbf{Rails} owns HTTP, Active Record, jobs, Hotwire, and the UX.
|
|
287
|
+
\item
|
|
288
|
+
\textbf{GNU R} owns the math and the statistical vocabulary.
|
|
289
|
+
\item
|
|
290
|
+
\textbf{\href{https://github.com/rbotafogo/galaaz}{Galaaz}} is the
|
|
291
|
+
bridge that makes R feel like a Ruby DSL---and moves bulky tables with
|
|
292
|
+
Apache Arrow when you need more than a few numbers.
|
|
293
|
+
\end{itemize}
|
|
294
|
+
|
|
295
|
+
This post walks through a real demo app---the
|
|
296
|
+
\textbf{\href{https://github.com/rbotafogo/r_on_rails_ledger}{R-on-Rails
|
|
297
|
+
Ledger}}--- so you can see the database, the calculators, and the charts
|
|
298
|
+
on one page.
|
|
299
|
+
|
|
300
|
+
\section{What the Ledger is}\label{what-the-ledger-is}
|
|
301
|
+
|
|
302
|
+
The Ledger is a \textbf{family-office style portfolio stress tester} on
|
|
303
|
+
\textbf{Rails 8}, \textbf{CRuby}, \textbf{SQLite}, \textbf{Solid Queue /
|
|
304
|
+
Solid Cable}, and \textbf{Hotwire}. It is a sibling app to the Galaaz
|
|
305
|
+
gem (not packaged inside the gem). One click runs two engines on the
|
|
306
|
+
same portfolio return panel:
|
|
307
|
+
|
|
308
|
+
\begin{itemize}
|
|
309
|
+
\tightlist
|
|
310
|
+
\item
|
|
311
|
+
\textbf{Engine A --- historical:} empirical VaR / expected shortfall /
|
|
312
|
+
return density from history (\texttt{R.quantile}, \texttt{R.mean},
|
|
313
|
+
\texttt{R.density}).
|
|
314
|
+
\item
|
|
315
|
+
\textbf{Engine B --- Monte Carlo:} forward GBM paths calibrated to
|
|
316
|
+
that history, then forward VaR-style KPIs and a 30-day ``cone.''
|
|
317
|
+
\end{itemize}
|
|
318
|
+
|
|
319
|
+
Rails stores the book and the job results; R does the science; the
|
|
320
|
+
browser gets Plotly charts from JSON that R (and a little Ruby)
|
|
321
|
+
produced.
|
|
322
|
+
|
|
323
|
+
\section{What's in Rails (and the
|
|
324
|
+
database)}\label{whats-in-rails-and-the-database}
|
|
325
|
+
|
|
326
|
+
The ``ledger'' here is a \textbf{portfolio price book}, not double-entry
|
|
327
|
+
accounting:
|
|
328
|
+
|
|
329
|
+
{\def\LTcaptype{none} % do not increment counter
|
|
330
|
+
\begin{longtable}[]{@{}
|
|
331
|
+
>{\raggedright\arraybackslash}p{(\linewidth - 2\tabcolsep) * \real{0.5000}}
|
|
332
|
+
>{\raggedright\arraybackslash}p{(\linewidth - 2\tabcolsep) * \real{0.5000}}@{}}
|
|
333
|
+
\toprule\noalign{}
|
|
334
|
+
\begin{minipage}[b]{\linewidth}\raggedright
|
|
335
|
+
Table
|
|
336
|
+
\end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright
|
|
337
|
+
Role
|
|
338
|
+
\end{minipage} \\
|
|
339
|
+
\midrule\noalign{}
|
|
340
|
+
\endhead
|
|
341
|
+
\bottomrule\noalign{}
|
|
342
|
+
\endlastfoot
|
|
343
|
+
\texttt{portfolios} & Named book (\texttt{name},
|
|
344
|
+
\texttt{total\_value}) \\
|
|
345
|
+
\texttt{assets} & Tickers + weights \\
|
|
346
|
+
\texttt{historical\_prices} & Bars: \texttt{adjusted\_close},
|
|
347
|
+
\texttt{daily\_return}, \ldots{} \\
|
|
348
|
+
\texttt{stress\_tests} & Job status, scalar KPIs,
|
|
349
|
+
\texttt{chart\_payload} JSON \\
|
|
350
|
+
\texttt{stress\_test\_runs} & Per-engine (\texttt{historical} /
|
|
351
|
+
\texttt{monte\_carlo}) payloads \\
|
|
352
|
+
\end{longtable}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
Seed data is \textbf{synthetic GBM} (honest demo data---not a live
|
|
356
|
+
feed). \texttt{SEED\_PROFILE=fast} is laptop-friendly (\textasciitilde10
|
|
357
|
+
assets x 2k bars); \texttt{SEED\_PROFILE=wow} is the ``about a million
|
|
358
|
+
bars'' line.
|
|
359
|
+
|
|
360
|
+
The click path is ordinary Rails:
|
|
361
|
+
|
|
362
|
+
\begin{Shaded}
|
|
363
|
+
\begin{Highlighting}[]
|
|
364
|
+
\NormalTok{POST /portfolios/:id/stress\_tests}
|
|
365
|
+
\NormalTok{ {-}\textgreater{} StressTestsController\#create}
|
|
366
|
+
\NormalTok{ {-}\textgreater{} StressTestJob.perform\_later(...)}
|
|
367
|
+
\NormalTok{ {-}\textgreater{} Risk::DualOrchestrator}
|
|
368
|
+
\NormalTok{ |{-} Risk::HistoricalEngine}
|
|
369
|
+
\NormalTok{ |{-} Risk::MonteCarloEngine}
|
|
370
|
+
\NormalTok{ {-}\textgreater{} Turbo::StreamsChannel broadcast}
|
|
371
|
+
\NormalTok{ {-}\textgreater{} results partial + Plotly redraw}
|
|
372
|
+
\end{Highlighting}
|
|
373
|
+
\end{Shaded}
|
|
374
|
+
|
|
375
|
+
No React SPA. No Redis required for the demo. Development often runs the
|
|
376
|
+
job \textbf{\texttt{:async}} in-process so one \texttt{bin/dev} is
|
|
377
|
+
enough; Solid Queue stays in the Gemfile for production-shaped runs.
|
|
378
|
+
|
|
379
|
+
\section{R calculators that read like
|
|
380
|
+
Ruby}\label{r-calculators-that-read-like-ruby}
|
|
381
|
+
|
|
382
|
+
After returns land in R (via Arrow---see below), the historical engine
|
|
383
|
+
exposes small methods that call R the way Galaaz intends: \textbf{named
|
|
384
|
+
functions and vectors}, not a wall of quoted R source for every KPI.
|
|
385
|
+
|
|
386
|
+
From the Ledger's \texttt{Risk::HistoricalEngine} (abbreviated):
|
|
387
|
+
|
|
388
|
+
\begin{Shaded}
|
|
389
|
+
\begin{Highlighting}[]
|
|
390
|
+
\ControlFlowTok{def}\NormalTok{ historical\_var(returns\_vec, probs}\OperatorTok{:}\NormalTok{)}
|
|
391
|
+
\DataTypeTok{RValues}\AttributeTok{.scalar\_f}\NormalTok{(}
|
|
392
|
+
\ConstantTok{R}\AttributeTok{.as\_\_numeric}\NormalTok{(}
|
|
393
|
+
\ConstantTok{R}\AttributeTok{.quantile}\NormalTok{(returns\_vec,}
|
|
394
|
+
\WarningTok{probs:}\NormalTok{ probs,}
|
|
395
|
+
\WarningTok{names:} \DecValTok{false}\NormalTok{,}
|
|
396
|
+
\WarningTok{type:} \DecValTok{7}\NormalTok{)}
|
|
397
|
+
\NormalTok{ )}
|
|
398
|
+
\NormalTok{ )}
|
|
399
|
+
\ControlFlowTok{end}
|
|
400
|
+
|
|
401
|
+
\ControlFlowTok{def}\NormalTok{ expected\_shortfall(returns\_vec, var\_level)}
|
|
402
|
+
\NormalTok{ thr }\OperatorTok{=}\NormalTok{ var\_level}\AttributeTok{.is\_a?}\NormalTok{(}\DataTypeTok{Numeric}\NormalTok{) }\OperatorTok{?}
|
|
403
|
+
\NormalTok{ var\_level }\OperatorTok{:} \DataTypeTok{RValues}\AttributeTok{.scalar\_f}\NormalTok{(var\_level)}
|
|
404
|
+
\DataTypeTok{RValues}\AttributeTok{.scalar\_f}\NormalTok{(}
|
|
405
|
+
\ConstantTok{R}\AttributeTok{.as\_\_numeric}\NormalTok{(}
|
|
406
|
+
\ConstantTok{R}\AttributeTok{.mean}\NormalTok{(returns\_vec}\KeywordTok{[}\NormalTok{returns\_vec }\OperatorTok{\textless{}=}\NormalTok{ thr}\KeywordTok{]}\NormalTok{)}
|
|
407
|
+
\NormalTok{ )}
|
|
408
|
+
\NormalTok{ )}
|
|
409
|
+
\ControlFlowTok{end}
|
|
410
|
+
|
|
411
|
+
\ControlFlowTok{def}\NormalTok{ return\_density(returns\_vec, n}\OperatorTok{:} \DecValTok{128}\NormalTok{)}
|
|
412
|
+
\DataTypeTok{ArrowHandoff}\AttributeTok{.density\_xy\_for\_plotly}\NormalTok{(returns\_vec, }\WarningTok{n:}\NormalTok{ n)}
|
|
413
|
+
\ControlFlowTok{end}
|
|
414
|
+
\end{Highlighting}
|
|
415
|
+
\end{Shaded}
|
|
416
|
+
|
|
417
|
+
That is the pitch for Ruby developers: \texttt{R.quantile} is a method
|
|
418
|
+
call. For R developers: the quantile is still \textbf{R's}
|
|
419
|
+
quantile---same \texttt{type\ =\ 7} story you know---just invoked from
|
|
420
|
+
the app that owns the database.
|
|
421
|
+
|
|
422
|
+
A tiny live taste of the same idea (outside Rails):
|
|
423
|
+
|
|
424
|
+
\begin{Shaded}
|
|
425
|
+
\begin{Highlighting}[]
|
|
426
|
+
\NormalTok{x }\OperatorTok{=} \ConstantTok{R}\AttributeTok{.c}\NormalTok{(}\OperatorTok{{-}}\FloatTok{0.02}\NormalTok{, }\OperatorTok{{-}}\FloatTok{0.01}\NormalTok{, }\FloatTok{0.0}\NormalTok{, }\FloatTok{0.01}\NormalTok{, }\FloatTok{0.015}\NormalTok{, }\OperatorTok{{-}}\FloatTok{0.005}\NormalTok{)}
|
|
427
|
+
\NormalTok{q05 }\OperatorTok{=} \ConstantTok{R}\AttributeTok{.as\_\_numeric}\NormalTok{(}
|
|
428
|
+
\ConstantTok{R}\AttributeTok{.quantile}\NormalTok{(x, }\WarningTok{probs:} \FloatTok{0.05}\NormalTok{, }\WarningTok{names:} \DecValTok{false}\NormalTok{, }\WarningTok{type:} \DecValTok{7}\NormalTok{))}
|
|
429
|
+
\FunctionTok{puts} \StringTok{"5\% quantile: }\SpecialCharTok{\#\{}\NormalTok{q05 }\OperatorTok{\textgreater{}\textgreater{}} \DecValTok{0}\SpecialCharTok{\}}\StringTok{"}
|
|
430
|
+
\end{Highlighting}
|
|
431
|
+
\end{Shaded}
|
|
432
|
+
|
|
433
|
+
\begin{verbatim}
|
|
434
|
+
## 5% quantile: -0.017499999999999998
|
|
435
|
+
\end{verbatim}
|
|
436
|
+
|
|
437
|
+
Engine B calibrates mu/sigma in R, simulates GBM paths, and returns
|
|
438
|
+
structured fields (\texttt{sim\_var\_30d}, sample paths, terminal
|
|
439
|
+
density) that Rails stores on each run's \texttt{payload} JSON.
|
|
440
|
+
|
|
441
|
+
\section{How data moves (Arrow,
|
|
442
|
+
honestly)}\label{how-data-moves-arrow-honestly}
|
|
443
|
+
|
|
444
|
+
Crossing a process boundary means you must be honest about
|
|
445
|
+
\textbf{copies}. Galaaz can move columnar data with Apache Arrow in
|
|
446
|
+
three ways:
|
|
447
|
+
|
|
448
|
+
\begin{enumerate}
|
|
449
|
+
\def\labelenumi{\arabic{enumi}.}
|
|
450
|
+
\tightlist
|
|
451
|
+
\item
|
|
452
|
+
\textbf{Copy into R} --- Ruby builds batches; R materialises an Arrow
|
|
453
|
+
table / proxy. Simple and available today.
|
|
454
|
+
\item
|
|
455
|
+
\textbf{IPC file handoff} --- Ruby writes an Arrow IPC file (often
|
|
456
|
+
under \texttt{/dev/shm}); R opens it by \textbf{path}. Only the path
|
|
457
|
+
crosses NewBridge---not megabytes of MsgPack. This is what the Ledger
|
|
458
|
+
prefers for the return panel.
|
|
459
|
+
\item
|
|
460
|
+
\textbf{Shared-memory zero-copy} --- both processes attach to one live
|
|
461
|
+
segment. \textbf{Not implemented yet}; we do not claim it.
|
|
462
|
+
\end{enumerate}
|
|
463
|
+
|
|
464
|
+
\subsection{Example: copy into R}\label{example-copy-into-r}
|
|
465
|
+
|
|
466
|
+
Good when the table is modest or you do not have a Ruby Arrow writer
|
|
467
|
+
installed. One call builds the R-side table; then you Remote-Control it.
|
|
468
|
+
|
|
469
|
+
\begin{Shaded}
|
|
470
|
+
\begin{Highlighting}[]
|
|
471
|
+
\NormalTok{arrow\_ok }\OperatorTok{=} \ConstantTok{R}\OperatorTok{::}\DataTypeTok{Support}\AttributeTok{.eval}\NormalTok{(}
|
|
472
|
+
\StringTok{"requireNamespace(\textquotesingle{}arrow\textquotesingle{}, quietly=TRUE)"}\NormalTok{) }\OperatorTok{==} \DecValTok{true}
|
|
473
|
+
\ControlFlowTok{unless}\NormalTok{ arrow\_ok}
|
|
474
|
+
\FunctionTok{puts} \VerbatimStringTok{\textquotesingle{}(Skip: need R package arrow.)\textquotesingle{}}
|
|
475
|
+
\ControlFlowTok{else}
|
|
476
|
+
\NormalTok{ batches }\OperatorTok{=} \KeywordTok{[}
|
|
477
|
+
\KeywordTok{[}\OperatorTok{\{} \WarningTok{daily\_return:} \OperatorTok{{-}}\FloatTok{0.01} \OperatorTok{\}}\NormalTok{, }\OperatorTok{\{} \WarningTok{daily\_return:} \FloatTok{0.02} \OperatorTok{\}}\KeywordTok{]}\NormalTok{,}
|
|
478
|
+
\KeywordTok{[}\OperatorTok{\{} \WarningTok{daily\_return:} \FloatTok{0.005} \OperatorTok{\}}\NormalTok{, }\OperatorTok{\{} \WarningTok{daily\_return:} \OperatorTok{{-}}\FloatTok{0.003} \OperatorTok{\}}\KeywordTok{]}
|
|
479
|
+
\KeywordTok{]}
|
|
480
|
+
\NormalTok{ tbl }\OperatorTok{=} \ConstantTok{R}\OperatorTok{::}\DataTypeTok{Arrow}\AttributeTok{.from\_ruby\_batches}\NormalTok{(batches)}
|
|
481
|
+
\FunctionTok{puts} \StringTok{"R class: }\SpecialCharTok{\#\{}\NormalTok{tbl}\AttributeTok{.rclass}\SpecialCharTok{\}}\StringTok{"}
|
|
482
|
+
\NormalTok{ vec }\OperatorTok{=} \ConstantTok{R}\AttributeTok{.as\_\_numeric}\NormalTok{(}
|
|
483
|
+
\ConstantTok{R}\AttributeTok{.dplyr\_\_\_collect}\NormalTok{(tbl)}\KeywordTok{[[}\VerbatimStringTok{\textquotesingle{}daily\_return\textquotesingle{}}\KeywordTok{]]}\NormalTok{)}
|
|
484
|
+
\NormalTok{ q05 }\OperatorTok{=} \ConstantTok{R}\AttributeTok{.as\_\_numeric}\NormalTok{(}
|
|
485
|
+
\ConstantTok{R}\AttributeTok{.quantile}\NormalTok{(vec, }\WarningTok{probs:} \FloatTok{0.05}\NormalTok{, }\WarningTok{names:} \DecValTok{false}\NormalTok{, }\WarningTok{type:} \DecValTok{7}\NormalTok{))}
|
|
486
|
+
\FunctionTok{puts} \StringTok{"5\% quantile: }\SpecialCharTok{\#\{}\NormalTok{q05 }\OperatorTok{\textgreater{}\textgreater{}} \DecValTok{0}\SpecialCharTok{\}}\StringTok{"}
|
|
487
|
+
\ControlFlowTok{end}
|
|
488
|
+
\end{Highlighting}
|
|
489
|
+
\end{Shaded}
|
|
490
|
+
|
|
491
|
+
\begin{verbatim}
|
|
492
|
+
## R class: Table
|
|
493
|
+
## 5% quantile: -0.00895
|
|
494
|
+
\end{verbatim}
|
|
495
|
+
|
|
496
|
+
The Ledger's fallback path is similar: a small \texttt{data.frame} plus
|
|
497
|
+
\texttt{R::Arrow.table\_from(df)} when IPC is unavailable.
|
|
498
|
+
|
|
499
|
+
\subsection{Example: IPC file handoff}\label{example-ipc-file-handoff}
|
|
500
|
+
|
|
501
|
+
Preferred for larger same-machine panels. Ruby writes the file; R opens
|
|
502
|
+
by path; Ruby can unlink after R has the table. The reverse direction
|
|
503
|
+
(\texttt{R::Arrow.write\_ipc} then \texttt{Galaaz::ArrowIpc.read}) is
|
|
504
|
+
how the Ledger can pull density coordinates back for Plotly.
|
|
505
|
+
|
|
506
|
+
\begin{Shaded}
|
|
507
|
+
\begin{Highlighting}[]
|
|
508
|
+
\NormalTok{ipc\_ok }\OperatorTok{=} \DataTypeTok{Galaaz}\OperatorTok{::}\DataTypeTok{ArrowIpc}\AttributeTok{.available?} \OperatorTok{\&\&}
|
|
509
|
+
\NormalTok{ (}\ConstantTok{R}\OperatorTok{::}\DataTypeTok{Support}\AttributeTok{.eval}\NormalTok{(}
|
|
510
|
+
\StringTok{"requireNamespace(\textquotesingle{}arrow\textquotesingle{}, quietly=TRUE)"}\NormalTok{) }\OperatorTok{==} \DecValTok{true}\NormalTok{)}
|
|
511
|
+
\ControlFlowTok{unless}\NormalTok{ ipc\_ok}
|
|
512
|
+
\FunctionTok{puts} \VerbatimStringTok{\textquotesingle{}(Skip: need Arrow IPC backend + R arrow.)\textquotesingle{}}
|
|
513
|
+
\ControlFlowTok{else}
|
|
514
|
+
\NormalTok{ returns }\OperatorTok{=} \KeywordTok{[}\OperatorTok{{-}}\FloatTok{0.01}\NormalTok{, }\FloatTok{0.02}\NormalTok{, }\FloatTok{0.005}\NormalTok{, }\OperatorTok{{-}}\FloatTok{0.003}\NormalTok{, }\OperatorTok{{-}}\FloatTok{0.008}\KeywordTok{]}
|
|
515
|
+
\NormalTok{ path }\OperatorTok{=} \DataTypeTok{Galaaz}\OperatorTok{::}\DataTypeTok{ArrowIpc}\AttributeTok{.write}\NormalTok{(}
|
|
516
|
+
\VerbatimStringTok{\textquotesingle{}daily\_return\textquotesingle{}} \OperatorTok{=\textgreater{}}\NormalTok{ returns}\AttributeTok{.map}\NormalTok{(}\OperatorTok{\&}\WarningTok{:to\_f}\NormalTok{))}
|
|
517
|
+
\ControlFlowTok{begin}
|
|
518
|
+
\NormalTok{ tbl }\OperatorTok{=} \ConstantTok{R}\OperatorTok{::}\DataTypeTok{Arrow}\AttributeTok{.open\_ipc}\NormalTok{(path)}
|
|
519
|
+
\FunctionTok{puts} \StringTok{"R class: }\SpecialCharTok{\#\{}\NormalTok{tbl}\AttributeTok{.rclass}\SpecialCharTok{\}}\StringTok{"}
|
|
520
|
+
\FunctionTok{puts} \StringTok{"IPC: }\SpecialCharTok{\#\{}\DataTypeTok{File}\AttributeTok{.basename}\NormalTok{(path)}\SpecialCharTok{\}}\StringTok{"}
|
|
521
|
+
\NormalTok{ vec }\OperatorTok{=} \ConstantTok{R}\AttributeTok{.as\_\_numeric}\NormalTok{(}
|
|
522
|
+
\ConstantTok{R}\AttributeTok{.dplyr\_\_\_collect}\NormalTok{(tbl)}\KeywordTok{[[}\VerbatimStringTok{\textquotesingle{}daily\_return\textquotesingle{}}\KeywordTok{]]}\NormalTok{)}
|
|
523
|
+
\NormalTok{ q05 }\OperatorTok{=} \ConstantTok{R}\AttributeTok{.as\_\_numeric}\NormalTok{(}
|
|
524
|
+
\ConstantTok{R}\AttributeTok{.quantile}\NormalTok{(vec, }\WarningTok{probs:} \FloatTok{0.05}\NormalTok{, }\WarningTok{names:} \DecValTok{false}\NormalTok{, }\WarningTok{type:} \DecValTok{7}\NormalTok{))}
|
|
525
|
+
\FunctionTok{puts} \StringTok{"5\% quantile: }\SpecialCharTok{\#\{}\NormalTok{q05 }\OperatorTok{\textgreater{}\textgreater{}} \DecValTok{0}\SpecialCharTok{\}}\StringTok{"}
|
|
526
|
+
\ControlFlowTok{ensure}
|
|
527
|
+
\DataTypeTok{Galaaz}\OperatorTok{::}\DataTypeTok{ArrowIpc}\AttributeTok{.release}\NormalTok{(path)}
|
|
528
|
+
\ControlFlowTok{end}
|
|
529
|
+
\ControlFlowTok{end}
|
|
530
|
+
\end{Highlighting}
|
|
531
|
+
\end{Shaded}
|
|
532
|
+
|
|
533
|
+
\begin{verbatim}
|
|
534
|
+
## R class: Table
|
|
535
|
+
## IPC: galaaz_ipc_34399_93fe645f9fa9172f.arrow
|
|
536
|
+
## 5% quantile: -0.0096
|
|
537
|
+
\end{verbatim}
|
|
538
|
+
|
|
539
|
+
Then the product rule is \textbf{Remote Control}: keep the heavy panel
|
|
540
|
+
in R; unbox \textbf{KPIs and chart coordinates} back to Ruby for SQLite
|
|
541
|
+
and the browser.
|
|
542
|
+
|
|
543
|
+
\begin{Shaded}
|
|
544
|
+
\begin{Highlighting}[]
|
|
545
|
+
\NormalTok{SQLite {-}\textgreater{} portfolio return series (Ruby)}
|
|
546
|
+
\NormalTok{ {-}\textgreater{} Arrow IPC file (path on the bridge)}
|
|
547
|
+
\NormalTok{ {-}\textgreater{} GNU R (quantile / density / GBM)}
|
|
548
|
+
\NormalTok{ {-}\textgreater{} JSON{-}ish payloads on SQLite}
|
|
549
|
+
\NormalTok{ {-}\textgreater{} Turbo Stream {-}\textgreater{} Stimulus {-}\textgreater{} Plotly.js}
|
|
550
|
+
\end{Highlighting}
|
|
551
|
+
\end{Shaded}
|
|
552
|
+
|
|
553
|
+
\section{The page with the charts}\label{the-page-with-the-charts}
|
|
554
|
+
|
|
555
|
+
After a Local R stress test, the portfolio show page replaces the
|
|
556
|
+
results panel in place. Captures below are from a live run of the demo
|
|
557
|
+
(synthetic seed, Local engines, Plotly in the browser---\textbf{not}
|
|
558
|
+
ggplot2 SVG).
|
|
559
|
+
|
|
560
|
+
\subsection{Side-by-side KPIs}\label{side-by-side-kpis}
|
|
561
|
+
|
|
562
|
+
Engine A is \textbf{1-day historical} VaR. Engine B is \textbf{30-day
|
|
563
|
+
forward} Monte Carlo. The UI says so on purpose: compare directionally,
|
|
564
|
+
not as identical metrics.
|
|
565
|
+
|
|
566
|
+
\begin{figure}
|
|
567
|
+
\includegraphics[width=1\linewidth]{images/01_results_panel} \caption{Ledger results panel: Local R runtime banner, KPI table, and three Plotly charts.}\label{fig:fig_panel}
|
|
568
|
+
\end{figure}
|
|
569
|
+
|
|
570
|
+
\subsection{Density and tail risk}\label{density-and-tail-risk}
|
|
571
|
+
|
|
572
|
+
Historical kernel density from Engine A, Monte Carlo terminal-return
|
|
573
|
+
density from Engine B, with VaR markers as vertical lines.
|
|
574
|
+
|
|
575
|
+
\begin{figure}
|
|
576
|
+
\includegraphics[width=1\linewidth]{images/02_density_tail_risk} \caption{Density and tail risk (Plotly): historical vs Monte Carlo terminal returns.}\label{fig:fig_density}
|
|
577
|
+
\end{figure}
|
|
578
|
+
|
|
579
|
+
\subsection{Thirty-day Monte Carlo
|
|
580
|
+
cone}\label{thirty-day-monte-carlo-cone}
|
|
581
|
+
|
|
582
|
+
Sample paths, a 5--95\% band, median, and baseline---forward uncertainty
|
|
583
|
+
as a picture, not only a scalar.
|
|
584
|
+
|
|
585
|
+
\begin{figure}
|
|
586
|
+
\includegraphics[width=1\linewidth]{images/03_mc_cone} \caption{30-day Monte Carlo cone (Plotly): paths, median, and 5–95\% band.}\label{fig:fig_cone}
|
|
587
|
+
\end{figure}
|
|
588
|
+
|
|
589
|
+
\subsection{Rolling historical VaR and
|
|
590
|
+
breaches}\label{rolling-historical-var-and-breaches}
|
|
591
|
+
|
|
592
|
+
Trailing returns against a rolling 95\% VaR line, with breach markers
|
|
593
|
+
when returns punch through the threshold.
|
|
594
|
+
|
|
595
|
+
\begin{figure}
|
|
596
|
+
\includegraphics[width=1\linewidth]{images/04_rolling_var} \caption{Rolling historical VaR and breaches (Plotly).}\label{fig:fig_rolling}
|
|
597
|
+
\end{figure}
|
|
598
|
+
|
|
599
|
+
\section{Why this is a production-shaped
|
|
600
|
+
story}\label{why-this-is-a-production-shaped-story}
|
|
601
|
+
|
|
602
|
+
\begin{itemize}
|
|
603
|
+
\tightlist
|
|
604
|
+
\item
|
|
605
|
+
\textbf{One desk.} Rails conventions for the app; CRAN for the
|
|
606
|
+
science.
|
|
607
|
+
\item
|
|
608
|
+
\textbf{Jobs, not request-thread math.} Stress work runs in
|
|
609
|
+
\texttt{StressTestJob}; the UI waits on Turbo/Cable.
|
|
610
|
+
\item
|
|
611
|
+
\textbf{Process isolation.} GNU R is a separate process (NewBridge). A
|
|
612
|
+
bad R call need not take down Puma.
|
|
613
|
+
\item
|
|
614
|
+
\textbf{Optional dual-version R.} The same job can target Docker
|
|
615
|
+
images (e.g.~R 3.6.3 \textbar\textbar{} 4.3.3) when you want version
|
|
616
|
+
isolation; Local alone still tells the Arrow + DSL story.
|
|
617
|
+
\item
|
|
618
|
+
\textbf{OnRails family.} Ruby on Rails -\textgreater{} Omarchy /
|
|
619
|
+
LinuxOnRails -\textgreater{} \textbf{R-on-Rails}---same ``one person
|
|
620
|
+
can ship the whole product'' energy, applied to people whose science
|
|
621
|
+
already lives in R.
|
|
622
|
+
\end{itemize}
|
|
623
|
+
|
|
624
|
+
\section{Try it}\label{try-it}
|
|
625
|
+
|
|
626
|
+
\begin{Shaded}
|
|
627
|
+
\begin{Highlighting}[]
|
|
628
|
+
\FunctionTok{git}\NormalTok{ clone https://github.com/rbotafogo/r\_on\_rails\_ledger.git}
|
|
629
|
+
\BuiltInTok{cd}\NormalTok{ r\_on\_rails\_ledger}
|
|
630
|
+
\ExtensionTok{bundle}\NormalTok{ install}
|
|
631
|
+
\ExtensionTok{bin/rails}\NormalTok{ db:prepare}
|
|
632
|
+
\VariableTok{SEED\_PROFILE}\OperatorTok{=}\NormalTok{fast }\ExtensionTok{bin/rails}\NormalTok{ db:seed}
|
|
633
|
+
\ExtensionTok{bin/dev}
|
|
634
|
+
\CommentTok{\# open http://localhost:3000 {-}\textgreater{} portfolio {-}\textgreater{} Run stress test}
|
|
635
|
+
\end{Highlighting}
|
|
636
|
+
\end{Shaded}
|
|
637
|
+
|
|
638
|
+
With a Galaaz checkout nearby, \texttt{galaaz\ add\ ledger} (Omarchy /
|
|
639
|
+
CLI helper) can clone and boot the same demo. In-app docs live at
|
|
640
|
+
\texttt{/docs} (architecture, Ruby DSL excerpts, runbook).
|
|
641
|
+
|
|
642
|
+
For the bridge itself---JRuby/CRuby, Arrow handoffs,
|
|
643
|
+
\texttt{R::Job}---see the companion post \textbf{Galaaz 2.0} in this
|
|
644
|
+
blog series.
|
|
645
|
+
|
|
646
|
+
\section{Honest limits}\label{honest-limits}
|
|
647
|
+
|
|
648
|
+
\begin{itemize}
|
|
649
|
+
\tightlist
|
|
650
|
+
\item
|
|
651
|
+
Seed prices are \textbf{synthetic}, not broker data.
|
|
652
|
+
\item
|
|
653
|
+
VaR / GBM here are a \textbf{demo risk stack}, not a bank production
|
|
654
|
+
engine (no full PerformanceAnalytics suite in the live path).
|
|
655
|
+
\item
|
|
656
|
+
Charts are \textbf{Plotly.js} fed by JSON coordinates---not ggplot
|
|
657
|
+
embedded as SVG in this build.
|
|
658
|
+
\item
|
|
659
|
+
The Ledger's Arrow path is an \textbf{IPC file / mmap handoff}, not
|
|
660
|
+
shared Ruby/R heap memory (zero-copy is still future work).
|
|
661
|
+
\item
|
|
662
|
+
Historical and Monte Carlo \textbf{horizons differ}; the KPI footnotes
|
|
663
|
+
exist so the demo does not oversell a single number.
|
|
664
|
+
\end{itemize}
|
|
665
|
+
|
|
666
|
+
The point of honesty is the same as the point of the architecture: Rails
|
|
667
|
+
can be the product, R can stay R, and you can show the charts without
|
|
668
|
+
pretending the hard parts vanished.
|
|
669
|
+
|
|
670
|
+
\end{document}
|
|
Binary file
|