1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
//! Financial Data Geodesic Driver
//!
//! Maps live or historical price data to geodesic parameters.
//!
//! * **Price volatility** (realized standard deviation of log-returns over the
//! rolling window) drives `speed_multiplier`.
//! * **Trend direction** (normalised slope of the close price over the window)
//! affects the initial velocity vector components (`theta_velocity`,
//! `phi_velocity`).
//! * **Volume** (normalized against the window maximum) affects `trail_width`
//! and feeds into `color_hue`.
//!
//! # Example
//!
//! ```
//! use geodesic_wallpaper::finance_driver::{FinanceDriver, MarketBar};
//!
//! let mut driver = FinanceDriver::new(20);
//! driver.push_bar(MarketBar {
//! open: 100.0, high: 102.0, low: 99.0, close: 101.5,
//! volume: 1_000_000.0, timestamp: 0,
//! });
//! let params = driver.compute_params();
//! assert!(params.speed_multiplier >= 0.0);
//! ```
/// A single OHLCV candlestick bar.
#[derive(Debug, Clone)]
pub struct MarketBar {
/// Opening price.
pub open: f64,
/// Highest price in the bar.
pub high: f64,
/// Lowest price in the bar.
pub low: f64,
/// Closing price.
pub close: f64,
/// Traded volume.
pub volume: f64,
/// UNIX timestamp in seconds.
pub timestamp: u64,
}
/// Geodesic rendering parameters derived from market data.
#[derive(Debug, Clone)]
pub struct GeodesicParams {
/// Multiplier applied to the RK4 integration speed.
/// Driven by realized volatility; clamped to `[0.1, 5.0]`.
pub speed_multiplier: f32,
/// Initial polar angle θ ∈ \[0, 2π\), derived from latest close price
/// modulo the surface domain.
pub initial_theta: f32,
/// Initial azimuthal angle φ ∈ \[0, 2π\), derived from latest volume.
pub initial_phi: f32,
/// Tangent velocity along the θ direction.
/// Positive for an upward trend, negative for downward.
pub theta_velocity: f32,
/// Tangent velocity along the φ direction.
/// Magnitude proportional to normalized volume.
pub phi_velocity: f32,
/// Trail line width in logical pixels. Range `[1.0, 6.0]`.
pub trail_width: f32,
/// HSL hue in `[0.0, 1.0]` mapped from price trend.
/// 0.0 (red) = maximum downtrend; 0.33 (green) = maximum uptrend;
/// 0.66 (blue) = neutral.
pub color_hue: f32,
}
impl Default for GeodesicParams {
fn default() -> Self {
GeodesicParams {
speed_multiplier: 1.0,
initial_theta: 0.0,
initial_phi: 0.0,
theta_velocity: 0.1,
phi_velocity: 0.1,
trail_width: 2.0,
color_hue: 0.66,
}
}
}
/// Maps OHLCV bars to `GeodesicParams`.
pub struct FinanceDriver {
bars: Vec<MarketBar>,
/// Number of bars used for the rolling statistics window.
window: usize,
/// Scaling constant that translates normalized volatility to speed units.
/// Defaults to `3.0`; tune to taste.
pub c_financial: f64,
}
impl FinanceDriver {
/// Create a new driver with the given rolling-window size.
///
/// `window` must be at least 2 (a single bar produces no log-return).
/// Values less than 2 are silently promoted to 2.
pub fn new(window: usize) -> Self {
FinanceDriver {
bars: Vec::new(),
window: window.max(2),
c_financial: 3.0,
}
}
/// Push a new bar. Only the most recent `window` bars are retained.
pub fn push_bar(&mut self, bar: MarketBar) {
self.bars.push(bar);
if self.bars.len() > self.window {
let excess = self.bars.len() - self.window;
self.bars.drain(0..excess);
}
}
/// Map the current rolling window to geodesic parameters.
///
/// Returns sensible defaults when fewer than 2 bars are available.
pub fn compute_params(&self) -> GeodesicParams {
if self.bars.len() < 2 {
return GeodesicParams::default();
}
let vol = self.volatility();
let trend = self.trend_direction(); // -1.0 .. +1.0
let norm_vol = self.normalized_volume();
// Speed: volatility scaled by c_financial, clamped to [0.1, 5.0].
let raw_speed = (vol * self.c_financial) as f32;
let speed_multiplier = raw_speed.clamp(0.1, 5.0).max(0.1);
// Initial position derived from the latest close and volume.
let latest = self.bars.last().unwrap();
let initial_theta = ((latest.close % (2.0 * std::f64::consts::PI)) as f32).abs();
let initial_phi = ((latest.volume * 1e-6 % (2.0 * std::f64::consts::PI)) as f32).abs();
// Velocity direction driven by trend.
let theta_velocity = trend as f32 * 0.3;
let phi_velocity = norm_vol * 0.2;
// Trail width: wider on high volume.
let trail_width = 1.0 + norm_vol * 5.0;
// Hue: red (0.0) on max downtrend → green (0.33) on max uptrend.
// Remap trend from [-1, 1] to [0, 0.33].
let color_hue = ((trend + 1.0) * 0.165) as f32; // 0.0 .. 0.33
GeodesicParams {
speed_multiplier,
initial_theta,
initial_phi,
theta_velocity,
phi_velocity,
trail_width,
color_hue,
}
}
/// Parse simple CSV rows: `timestamp,open,high,low,close,volume`.
///
/// Lines starting with `#` or that cannot be parsed are silently skipped.
/// Returns the number of bars successfully loaded.
///
/// # Example
///
/// ```
/// use geodesic_wallpaper::finance_driver::FinanceDriver;
///
/// let mut d = FinanceDriver::new(10);
/// let csv = "1700000000,100.0,102.0,99.0,101.5,500000\n\
/// 1700000060,101.5,103.0,101.0,102.0,600000\n";
/// let loaded = d.load_csv(csv);
/// assert_eq!(loaded, 2);
/// ```
pub fn load_csv(&mut self, csv: &str) -> usize {
let mut count = 0;
for line in csv.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let parts: Vec<&str> = trimmed.split(',').collect();
if parts.len() < 6 {
continue;
}
let parsed: Option<MarketBar> = (|| {
Some(MarketBar {
timestamp: parts[0].trim().parse().ok()?,
open: parts[1].trim().parse().ok()?,
high: parts[2].trim().parse().ok()?,
low: parts[3].trim().parse().ok()?,
close: parts[4].trim().parse().ok()?,
volume: parts[5].trim().parse().ok()?,
})
})();
if let Some(bar) = parsed {
self.push_bar(bar);
count += 1;
}
}
count
}
// ── Private helpers ────────────────────────────────────────────────────
/// Realized volatility: population standard deviation of log-returns.
fn volatility(&self) -> f64 {
let returns = self.log_returns();
if returns.is_empty() {
return 0.0;
}
let mean = returns.iter().sum::<f64>() / returns.len() as f64;
let variance =
returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / returns.len() as f64;
variance.sqrt()
}
/// Trend direction in `[-1.0, 1.0]`.
///
/// Computed as the sign-and-magnitude of the OLS slope of close prices
/// normalised by the mean price, clamped to `[-1, 1]`.
fn trend_direction(&self) -> f64 {
let closes: Vec<f64> = self.bars.iter().map(|b| b.close).collect();
let n = closes.len() as f64;
if n < 2.0 {
return 0.0;
}
let mean_x = (n - 1.0) / 2.0;
let mean_y = closes.iter().sum::<f64>() / n;
let num: f64 = closes
.iter()
.enumerate()
.map(|(i, &y)| (i as f64 - mean_x) * (y - mean_y))
.sum();
let den: f64 = (0..closes.len() as usize)
.map(|i| (i as f64 - mean_x).powi(2))
.sum();
if den == 0.0 || mean_y == 0.0 {
return 0.0;
}
let slope_normalized = num / den / mean_y;
slope_normalized.clamp(-1.0, 1.0)
}
/// Volume normalized to `[0.0, 1.0]` relative to the window maximum.
fn normalized_volume(&self) -> f32 {
let max_vol = self
.bars
.iter()
.map(|b| b.volume)
.fold(f64::NEG_INFINITY, f64::max);
if max_vol <= 0.0 {
return 0.0;
}
let latest_vol = self.bars.last().map(|b| b.volume).unwrap_or(0.0);
(latest_vol / max_vol) as f32
}
/// Compute log-returns for the current window.
fn log_returns(&self) -> Vec<f64> {
self.bars
.windows(2)
.filter_map(|w| {
let prev = w[0].close;
let curr = w[1].close;
if prev > 0.0 && curr > 0.0 {
Some((curr / prev).ln())
} else {
None
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_bar(close: f64, volume: f64) -> MarketBar {
MarketBar {
open: close * 0.99,
high: close * 1.01,
low: close * 0.98,
close,
volume,
timestamp: 0,
}
}
#[test]
fn default_params_on_empty_driver() {
let d = FinanceDriver::new(10);
let p = d.compute_params();
assert!((p.speed_multiplier - 1.0).abs() < 1e-6);
assert!((p.color_hue - 0.66).abs() < 1e-4);
}
#[test]
fn push_bar_trims_to_window() {
let mut d = FinanceDriver::new(3);
for i in 0..10 {
d.push_bar(make_bar(100.0 + i as f64, 1000.0));
}
assert_eq!(d.bars.len(), 3);
}
#[test]
fn load_csv_parses_valid_lines() {
let mut d = FinanceDriver::new(20);
let csv = "1700000000,100.0,102.0,99.0,101.5,500000\n\
# comment line\n\
\n\
1700000060,101.5,103.0,101.0,102.0,600000\n";
let n = d.load_csv(csv);
assert_eq!(n, 2);
}
#[test]
fn load_csv_skips_bad_rows() {
let mut d = FinanceDriver::new(20);
let csv = "not,enough\n\
1700000000,100.0,102.0,99.0,101.5,500000\n";
let n = d.load_csv(csv);
assert_eq!(n, 1);
}
#[test]
fn speed_multiplier_is_clamped() {
let mut d = FinanceDriver::new(5);
// Extreme volatility scenario.
d.push_bar(make_bar(100.0, 1_000_000.0));
d.push_bar(make_bar(1.0, 1_000_000.0)); // log-return ≈ -4.6
d.push_bar(make_bar(1000.0, 1_000_000.0)); // log-return ≈ +6.9
let p = d.compute_params();
assert!(p.speed_multiplier <= 5.0);
assert!(p.speed_multiplier >= 0.1);
}
#[test]
fn trend_direction_uptrend() {
let mut d = FinanceDriver::new(5);
for i in 0..5 {
d.push_bar(make_bar(100.0 + i as f64 * 2.0, 1_000_000.0));
}
let p = d.compute_params();
// Consistent uptrend → positive theta_velocity.
assert!(p.theta_velocity > 0.0);
// Hue should be closer to green (0.33) than red (0.0).
assert!(p.color_hue > 0.1);
}
#[test]
fn normalized_volume_range() {
let mut d = FinanceDriver::new(5);
d.push_bar(make_bar(100.0, 500_000.0));
d.push_bar(make_bar(101.0, 1_000_000.0));
let p = d.compute_params();
assert!((0.0..=6.1).contains(&p.trail_width));
}
}