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,354 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "R-on-Rails: ship the ledger on Rails, keep the science in R"
|
|
3
|
+
subtitle: "A portfolio stress tester that puts SQLite, Hotwire, and GNU R on one desk"
|
|
4
|
+
author: "Rodrigo Botafogo"
|
|
5
|
+
tags: [Galaaz, "R-on-Rails", Rails, Ruby, R, Ledger, Arrow, Plotly, Hotwire]
|
|
6
|
+
date: "2026"
|
|
7
|
+
output:
|
|
8
|
+
html_document:
|
|
9
|
+
self_contained: true
|
|
10
|
+
keep_md: true
|
|
11
|
+
toc: true
|
|
12
|
+
toc_float: true
|
|
13
|
+
toc_depth: 2
|
|
14
|
+
number_sections: true
|
|
15
|
+
includes:
|
|
16
|
+
before_body: _logo_before_body.html
|
|
17
|
+
pdf_document:
|
|
18
|
+
includes:
|
|
19
|
+
in_header:
|
|
20
|
+
- "../../sty/galaaz.sty"
|
|
21
|
+
- "../../sty/galaaz-headers-from-p3.tex"
|
|
22
|
+
keep_tex: yes
|
|
23
|
+
number_sections: yes
|
|
24
|
+
toc: true
|
|
25
|
+
toc_depth: 2
|
|
26
|
+
fontsize: 11pt
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
```{r setup, echo=FALSE}
|
|
30
|
+
options(width = 70)
|
|
31
|
+
knitr::opts_chunk$set(fig.path = "r_on_rails_ledger_files/figure-")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```{ruby setup_ruby, echo=FALSE}
|
|
35
|
+
ENV['GALAAZ_BRIDGE_TIMEOUT_SEC'] ||= '300'
|
|
36
|
+
ENV['GALAAZ_JOBS_DIR'] ||=
|
|
37
|
+
File.join(Dir.tmpdir, 'galaaz_jobs_ledger_blog')
|
|
38
|
+
R.options(crayon__enabled: false, width: 70)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```{r brand_logo, echo=FALSE, fig.align='center', out.width='45%', eval=knitr::is_latex_output()}
|
|
42
|
+
# PDF only — HTML uses _logo_before_body.html (above the TOC).
|
|
43
|
+
knitr::include_graphics("images/galaaz-lockup-stacked.png")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
# Two audiences, one gap
|
|
47
|
+
|
|
48
|
+
**If you live in R**, you already have the hard part: VaR, density
|
|
49
|
+
estimates, Monte Carlo, Bioconductor, the whole CRAN catalog. What is
|
|
50
|
+
still painful is turning a notebook into a **product**—logins, a
|
|
51
|
+
database, background jobs, a page that updates when the job finishes,
|
|
52
|
+
and a deploy story that is not a pile of glue scripts.
|
|
53
|
+
|
|
54
|
+
**If you live in Ruby on Rails**, you already have the product shell.
|
|
55
|
+
What is still painful is serious statistics without standing up a
|
|
56
|
+
Python microservice fleet, learning another ORM, and ferrying CSV
|
|
57
|
+
files between processes.
|
|
58
|
+
|
|
59
|
+
**R-on-Rails** is the name we give to a simple division of labor:
|
|
60
|
+
|
|
61
|
+
* **Rails** owns HTTP, Active Record, jobs, Hotwire, and the UX.
|
|
62
|
+
* **GNU R** owns the math and the statistical vocabulary.
|
|
63
|
+
* **[Galaaz](https://github.com/rbotafogo/galaaz)** is the bridge that
|
|
64
|
+
makes R feel like a Ruby DSL—and moves bulky tables with Apache
|
|
65
|
+
Arrow when you need more than a few numbers.
|
|
66
|
+
|
|
67
|
+
This post walks through a real demo app—the
|
|
68
|
+
**[R-on-Rails Ledger](https://github.com/rbotafogo/r_on_rails_ledger)**—
|
|
69
|
+
so you can see the database, the calculators, and the charts on one
|
|
70
|
+
page.
|
|
71
|
+
|
|
72
|
+
# What the Ledger is
|
|
73
|
+
|
|
74
|
+
The Ledger is a **family-office style portfolio stress tester** on
|
|
75
|
+
**Rails 8**, **CRuby**, **SQLite**, **Solid Queue / Solid Cable**, and
|
|
76
|
+
**Hotwire**. It is a sibling app to the Galaaz gem (not packaged inside
|
|
77
|
+
the gem). One click runs two engines on the same portfolio return
|
|
78
|
+
panel:
|
|
79
|
+
|
|
80
|
+
* **Engine A — historical:** empirical VaR / expected shortfall /
|
|
81
|
+
return density from history (`R.quantile`, `R.mean`, `R.density`).
|
|
82
|
+
* **Engine B — Monte Carlo:** forward GBM paths calibrated to that
|
|
83
|
+
history, then forward VaR-style KPIs and a 30-day “cone.”
|
|
84
|
+
|
|
85
|
+
Rails stores the book and the job results; R does the science; the
|
|
86
|
+
browser gets Plotly charts from JSON that R (and a little Ruby)
|
|
87
|
+
produced.
|
|
88
|
+
|
|
89
|
+
# What’s in Rails (and the database)
|
|
90
|
+
|
|
91
|
+
The “ledger” here is a **portfolio price book**, not double-entry
|
|
92
|
+
accounting:
|
|
93
|
+
|
|
94
|
+
| Table | Role |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `portfolios` | Named book (`name`, `total_value`) |
|
|
97
|
+
| `assets` | Tickers + weights |
|
|
98
|
+
| `historical_prices` | Bars: `adjusted_close`, `daily_return`, ... |
|
|
99
|
+
| `stress_tests` | Job status, scalar KPIs, `chart_payload` JSON |
|
|
100
|
+
| `stress_test_runs` | Per-engine (`historical` / `monte_carlo`) payloads |
|
|
101
|
+
|
|
102
|
+
Seed data is **synthetic GBM** (honest demo data—not a live feed).
|
|
103
|
+
`SEED_PROFILE=fast` is laptop-friendly (~10 assets x 2k bars);
|
|
104
|
+
`SEED_PROFILE=wow` is the “about a million bars” line.
|
|
105
|
+
|
|
106
|
+
The click path is ordinary Rails:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
POST /portfolios/:id/stress_tests
|
|
110
|
+
-> StressTestsController#create
|
|
111
|
+
-> StressTestJob.perform_later(...)
|
|
112
|
+
-> Risk::DualOrchestrator
|
|
113
|
+
|- Risk::HistoricalEngine
|
|
114
|
+
|- Risk::MonteCarloEngine
|
|
115
|
+
-> Turbo::StreamsChannel broadcast
|
|
116
|
+
-> results partial + Plotly redraw
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
No React SPA. No Redis required for the demo. Development often runs
|
|
120
|
+
the job **`:async`** in-process so one `bin/dev` is enough; Solid Queue
|
|
121
|
+
stays in the Gemfile for production-shaped runs.
|
|
122
|
+
|
|
123
|
+
# R calculators that read like Ruby
|
|
124
|
+
|
|
125
|
+
After returns land in R (via Arrow—see below), the historical engine
|
|
126
|
+
exposes small methods that call R the way Galaaz intends: **named
|
|
127
|
+
functions and vectors**, not a wall of quoted R source for every KPI.
|
|
128
|
+
|
|
129
|
+
From the Ledger’s `Risk::HistoricalEngine` (abbreviated):
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
def historical_var(returns_vec, probs:)
|
|
133
|
+
RValues.scalar_f(
|
|
134
|
+
R.as__numeric(
|
|
135
|
+
R.quantile(returns_vec,
|
|
136
|
+
probs: probs,
|
|
137
|
+
names: false,
|
|
138
|
+
type: 7)
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def expected_shortfall(returns_vec, var_level)
|
|
144
|
+
thr = var_level.is_a?(Numeric) ?
|
|
145
|
+
var_level : RValues.scalar_f(var_level)
|
|
146
|
+
RValues.scalar_f(
|
|
147
|
+
R.as__numeric(
|
|
148
|
+
R.mean(returns_vec[returns_vec <= thr])
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def return_density(returns_vec, n: 128)
|
|
154
|
+
ArrowHandoff.density_xy_for_plotly(returns_vec, n: n)
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
That is the pitch for Ruby developers: `R.quantile` is a method call.
|
|
159
|
+
For R developers: the quantile is still **R’s** quantile—same
|
|
160
|
+
`type = 7` story you know—just invoked from the app that owns the
|
|
161
|
+
database.
|
|
162
|
+
|
|
163
|
+
A tiny live taste of the same idea (outside Rails):
|
|
164
|
+
|
|
165
|
+
```{ruby quantile_hello, message=FALSE, warning=FALSE}
|
|
166
|
+
x = R.c(-0.02, -0.01, 0.0, 0.01, 0.015, -0.005)
|
|
167
|
+
q05 = R.as__numeric(
|
|
168
|
+
R.quantile(x, probs: 0.05, names: false, type: 7))
|
|
169
|
+
puts "5% quantile: #{q05 >> 0}"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Engine B calibrates mu/sigma in R, simulates GBM paths, and returns
|
|
173
|
+
structured fields (`sim_var_30d`, sample paths, terminal density)
|
|
174
|
+
that Rails stores on each run’s `payload` JSON.
|
|
175
|
+
|
|
176
|
+
# How data moves (Arrow, honestly)
|
|
177
|
+
|
|
178
|
+
Crossing a process boundary means you must be honest about **copies**.
|
|
179
|
+
Galaaz can move columnar data with Apache Arrow in three ways:
|
|
180
|
+
|
|
181
|
+
1. **Copy into R** — Ruby builds batches; R materialises an Arrow
|
|
182
|
+
table / proxy. Simple and available today.
|
|
183
|
+
2. **IPC file handoff** — Ruby writes an Arrow IPC file (often under
|
|
184
|
+
`/dev/shm`); R opens it by **path**. Only the path crosses
|
|
185
|
+
NewBridge—not megabytes of MsgPack. This is what the Ledger
|
|
186
|
+
prefers for the return panel.
|
|
187
|
+
3. **Shared-memory zero-copy** — both processes attach to one live
|
|
188
|
+
segment. **Not implemented yet**; we do not claim it.
|
|
189
|
+
|
|
190
|
+
## Example: copy into R
|
|
191
|
+
|
|
192
|
+
Good when the table is modest or you do not have a Ruby Arrow writer
|
|
193
|
+
installed. One call builds the R-side table; then you Remote-Control
|
|
194
|
+
it.
|
|
195
|
+
|
|
196
|
+
```{ruby arrow_copy, message=FALSE, warning=FALSE}
|
|
197
|
+
arrow_ok = R::Support.eval(
|
|
198
|
+
"requireNamespace('arrow', quietly=TRUE)") == true
|
|
199
|
+
unless arrow_ok
|
|
200
|
+
puts '(Skip: need R package arrow.)'
|
|
201
|
+
else
|
|
202
|
+
batches = [
|
|
203
|
+
[{ daily_return: -0.01 }, { daily_return: 0.02 }],
|
|
204
|
+
[{ daily_return: 0.005 }, { daily_return: -0.003 }]
|
|
205
|
+
]
|
|
206
|
+
tbl = R::Arrow.from_ruby_batches(batches)
|
|
207
|
+
puts "R class: #{tbl.rclass}"
|
|
208
|
+
vec = R.as__numeric(
|
|
209
|
+
R.dplyr___collect(tbl)[['daily_return']])
|
|
210
|
+
q05 = R.as__numeric(
|
|
211
|
+
R.quantile(vec, probs: 0.05, names: false, type: 7))
|
|
212
|
+
puts "5% quantile: #{q05 >> 0}"
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The Ledger’s fallback path is similar: a small `data.frame` plus
|
|
217
|
+
`R::Arrow.table_from(df)` when IPC is unavailable.
|
|
218
|
+
|
|
219
|
+
## Example: IPC file handoff
|
|
220
|
+
|
|
221
|
+
Preferred for larger same-machine panels. Ruby writes the file; R
|
|
222
|
+
opens by path; Ruby can unlink after R has the table. The reverse
|
|
223
|
+
direction (`R::Arrow.write_ipc` then `Galaaz::ArrowIpc.read`) is how
|
|
224
|
+
the Ledger can pull density coordinates back for Plotly.
|
|
225
|
+
|
|
226
|
+
```{ruby arrow_ipc, message=FALSE, warning=FALSE}
|
|
227
|
+
ipc_ok = Galaaz::ArrowIpc.available? &&
|
|
228
|
+
(R::Support.eval(
|
|
229
|
+
"requireNamespace('arrow', quietly=TRUE)") == true)
|
|
230
|
+
unless ipc_ok
|
|
231
|
+
puts '(Skip: need Arrow IPC backend + R arrow.)'
|
|
232
|
+
else
|
|
233
|
+
returns = [-0.01, 0.02, 0.005, -0.003, -0.008]
|
|
234
|
+
path = Galaaz::ArrowIpc.write(
|
|
235
|
+
'daily_return' => returns.map(&:to_f))
|
|
236
|
+
begin
|
|
237
|
+
tbl = R::Arrow.open_ipc(path)
|
|
238
|
+
puts "R class: #{tbl.rclass}"
|
|
239
|
+
puts "IPC: #{File.basename(path)}"
|
|
240
|
+
vec = R.as__numeric(
|
|
241
|
+
R.dplyr___collect(tbl)[['daily_return']])
|
|
242
|
+
q05 = R.as__numeric(
|
|
243
|
+
R.quantile(vec, probs: 0.05, names: false, type: 7))
|
|
244
|
+
puts "5% quantile: #{q05 >> 0}"
|
|
245
|
+
ensure
|
|
246
|
+
Galaaz::ArrowIpc.release(path)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Then the product rule is **Remote Control**: keep the heavy panel in
|
|
252
|
+
R; unbox **KPIs and chart coordinates** back to Ruby for SQLite and
|
|
253
|
+
the browser.
|
|
254
|
+
|
|
255
|
+
```text
|
|
256
|
+
SQLite -> portfolio return series (Ruby)
|
|
257
|
+
-> Arrow IPC file (path on the bridge)
|
|
258
|
+
-> GNU R (quantile / density / GBM)
|
|
259
|
+
-> JSON-ish payloads on SQLite
|
|
260
|
+
-> Turbo Stream -> Stimulus -> Plotly.js
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
# The page with the charts
|
|
264
|
+
|
|
265
|
+
After a Local R stress test, the portfolio show page replaces the
|
|
266
|
+
results panel in place. Captures below are from a live run of the
|
|
267
|
+
demo (synthetic seed, Local engines, Plotly in the browser—**not**
|
|
268
|
+
ggplot2 SVG).
|
|
269
|
+
|
|
270
|
+
## Side-by-side KPIs
|
|
271
|
+
|
|
272
|
+
Engine A is **1-day historical** VaR. Engine B is **30-day forward**
|
|
273
|
+
Monte Carlo. The UI says so on purpose: compare directionally, not as
|
|
274
|
+
identical metrics.
|
|
275
|
+
|
|
276
|
+
```{r fig_panel, echo=FALSE, fig.cap="Ledger results panel: Local R runtime banner, KPI table, and three Plotly charts.", out.width="100%"}
|
|
277
|
+
knitr::include_graphics("images/01_results_panel.png")
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Density and tail risk
|
|
281
|
+
|
|
282
|
+
Historical kernel density from Engine A, Monte Carlo terminal-return
|
|
283
|
+
density from Engine B, with VaR markers as vertical lines.
|
|
284
|
+
|
|
285
|
+
```{r fig_density, echo=FALSE, fig.cap="Density and tail risk (Plotly): historical vs Monte Carlo terminal returns.", out.width="100%"}
|
|
286
|
+
knitr::include_graphics("images/02_density_tail_risk.png")
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Thirty-day Monte Carlo cone
|
|
290
|
+
|
|
291
|
+
Sample paths, a 5–95% band, median, and baseline—forward uncertainty
|
|
292
|
+
as a picture, not only a scalar.
|
|
293
|
+
|
|
294
|
+
```{r fig_cone, echo=FALSE, fig.cap="30-day Monte Carlo cone (Plotly): paths, median, and 5–95% band.", out.width="100%"}
|
|
295
|
+
knitr::include_graphics("images/03_mc_cone.png")
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Rolling historical VaR and breaches
|
|
299
|
+
|
|
300
|
+
Trailing returns against a rolling 95% VaR line, with breach markers
|
|
301
|
+
when returns punch through the threshold.
|
|
302
|
+
|
|
303
|
+
```{r fig_rolling, echo=FALSE, fig.cap="Rolling historical VaR and breaches (Plotly).", out.width="100%"}
|
|
304
|
+
knitr::include_graphics("images/04_rolling_var.png")
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
# Why this is a production-shaped story
|
|
308
|
+
|
|
309
|
+
* **One desk.** Rails conventions for the app; CRAN for the science.
|
|
310
|
+
* **Jobs, not request-thread math.** Stress work runs in
|
|
311
|
+
`StressTestJob`; the UI waits on Turbo/Cable.
|
|
312
|
+
* **Process isolation.** GNU R is a separate process (NewBridge). A
|
|
313
|
+
bad R call need not take down Puma.
|
|
314
|
+
* **Optional dual-version R.** The same job can target Docker images
|
|
315
|
+
(e.g. R 3.6.3 || 4.3.3) when you want version isolation; Local alone
|
|
316
|
+
still tells the Arrow + DSL story.
|
|
317
|
+
* **OnRails family.** Ruby on Rails -> Omarchy / LinuxOnRails ->
|
|
318
|
+
**R-on-Rails**—same “one person can ship the whole product” energy,
|
|
319
|
+
applied to people whose science already lives in R.
|
|
320
|
+
|
|
321
|
+
# Try it
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
git clone https://github.com/rbotafogo/r_on_rails_ledger.git
|
|
325
|
+
cd r_on_rails_ledger
|
|
326
|
+
bundle install
|
|
327
|
+
bin/rails db:prepare
|
|
328
|
+
SEED_PROFILE=fast bin/rails db:seed
|
|
329
|
+
bin/dev
|
|
330
|
+
# open http://localhost:3000 -> portfolio -> Run stress test
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
With a Galaaz checkout nearby, `galaaz add ledger` (Omarchy / CLI
|
|
334
|
+
helper) can clone and boot the same demo. In-app docs live at `/docs`
|
|
335
|
+
(architecture, Ruby DSL excerpts, runbook).
|
|
336
|
+
|
|
337
|
+
For the bridge itself—JRuby/CRuby, Arrow handoffs, `R::Job`—see the
|
|
338
|
+
companion post **Galaaz 2.0** in this blog series.
|
|
339
|
+
|
|
340
|
+
# Honest limits
|
|
341
|
+
|
|
342
|
+
* Seed prices are **synthetic**, not broker data.
|
|
343
|
+
* VaR / GBM here are a **demo risk stack**, not a bank production
|
|
344
|
+
engine (no full PerformanceAnalytics suite in the live path).
|
|
345
|
+
* Charts are **Plotly.js** fed by JSON coordinates—not ggplot embedded
|
|
346
|
+
as SVG in this build.
|
|
347
|
+
* The Ledger’s Arrow path is an **IPC file / mmap handoff**, not
|
|
348
|
+
shared Ruby/R heap memory (zero-copy is still future work).
|
|
349
|
+
* Historical and Monte Carlo **horizons differ**; the KPI footnotes
|
|
350
|
+
exist so the demo does not oversell a single number.
|
|
351
|
+
|
|
352
|
+
The point of honesty is the same as the point of the architecture:
|
|
353
|
+
Rails can be the product, R can stay R, and you can show the charts
|
|
354
|
+
without pretending the hard parts vanished.
|