Skip to content

Instantly share code, notes, and snippets.

@rust-play
Created August 13, 2026 10:53
Show Gist options
  • Select an option

  • Save rust-play/72b3dee8fc6e88b048a1945cc43aa721 to your computer and use it in GitHub Desktop.

Select an option

Save rust-play/72b3dee8fc6e88b048a1945cc43aa721 to your computer and use it in GitHub Desktop.
Code shared from the Rust Playground
use chrono::NaiveDate;
use serde::Deserialize;
use std::f64::consts::PI;
use std::io::Cursor;
const CSV_URL: &str = "https://raw.githubusercontent.com/coinmetrics/data/master/csv/btc.csv";
const GENESIS_DATE: &str = "2009-01-03";
/// Raw CSV Row matching Coin Metrics structure
#[derive(Debug, Deserialize)]
struct RawMetricsRow {
time: String,
#[serde(rename = "PriceUSD")]
price_usd: Option<f64>,
#[serde(rename = "CapMrktCurUSD")]
cap_mrkt_cur_usd: Option<f64>,
#[serde(rename = "CapMVRVCur")]
cap_mvrv_cur: Option<f64>,
#[serde(rename = "SplyCur")]
sply_cur: Option<f64>,
}
/// Cleaned & Processed Data Point
#[derive(Debug, Clone)]
struct BtcDataPoint {
time: NaiveDate,
day: f64,
price_usd: f64,
cap_mvrv_cur: f64,
rp: f64, // Realised price = aggregate cost basis
centre: f64,
}
fn main() {
println!("Loading Coin Metrics BTC data...");
let df = match load_data() {
Ok(data) if !data.is_empty() => data,
Err(e) => {
eprintln!("Network or parsing notice: {}", e);
eprintln!("Falling back to embedded baseline dataset for demonstration...");
load_demo_data()
}
_ => load_demo_data(),
};
if df.is_empty() {
eprintln!("No valid data found.");
return;
}
// Prepare arrays for Layer 1 fit
let x_vals: Vec<f64> = df.iter().map(|d| d.day.log10()).collect();
let y_vals: Vec<f64> = df.iter().map(|d| d.price_usd.log10()).collect();
// =========================================================================
// LAYER 1: Power-Law Trend (log10 P = a + b * log10(day))
// =========================================================================
let (b, a) = polyfit_deg1(&x_vals, &y_vals);
let mut residuals = Vec::with_capacity(df.len());
let mut df_fitted = df.clone();
let y_mean = y_vals.iter().sum::<f64>() / y_vals.len() as f64;
let mut ss_res = 0.0;
let mut ss_tot = 0.0;
for (i, row) in df_fitted.iter_mut().enumerate() {
let fit_y = a + b * x_vals[i];
let resid = y_vals[i] - fit_y;
residuals.push(resid);
row.centre = 10.0_f64.powf(fit_y);
ss_res += resid * resid;
ss_tot += (y_vals[i] - y_mean) * (y_vals[i] - y_mean);
}
let r2 = 1.0 - (ss_res / ss_tot);
let resid_std = (ss_res / residuals.len() as f64).sqrt();
println!("============================================================");
println!("LAYER 1 power-law trend");
println!(" log10 P = {:.4} + {:.4} * log10(day since genesis)", a, b);
println!(
" R^2 = {:.4} residual sd = {:.3} (1 sigma = x{:.2})",
r2,
resid_std,
10.0_f64.powf(resid_std)
);
// =========================================================================
// LAYER 2: Damped Halving Cycle (Fitted on Residuals >= 2012-01-01)
// =========================================================================
let cutoff_date = NaiveDate::from_ymd_opt(2012, 1, 1).unwrap();
let sub_data: Vec<&BtcDataPoint> = df_fitted.iter().filter(|d| d.time >= cutoff_date).collect();
let t_sub: Vec<f64> = sub_data.iter().map(|d| d.day).collect();
let r_sub: Vec<f64> = sub_data
.iter()
.map(|d| d.price_usd.log10() - d.centre.log10())
.collect();
// Fit non-linear curve: f(t) = c + A0 * exp(-lam * t / 365.25) * sin(2*pi*(t - phi)/T)
let p_opt = fit_damped_cycle(&t_sub, &r_sub);
let (a0, lam, t_period, phi, _c_offset) = (p_opt[0], p_opt[1], p_opt[2], p_opt[3], p_opt[4]);
println!("\nLAYER 2 damped halving cycle (fitted on residuals)");
println!(
" amplitude A0 = {:.3} decay = {:.3}/yr period = {:.0}d ({:.2}yr) phase = {:.0}d",
a0,
lam,
t_period,
t_period / 365.25,
phi
);
// =========================================================================
// LAYER 3: Cost-Basis Rails (MVRV at Cycle Tops and Bottoms)
// =========================================================================
let tops = [
("2013", "2013-11-30"),
("2017", "2017-12-17"),
("2021", "2021-11-10"),
("2025", "2025-10-06"),
];
let bots = [
("2015", "2015-01-14"),
("2018", "2018-12-15"),
("2022", "2022-11-21"),
];
println!("\nLAYER 3 cost-basis rails");
println!(" cycle tops (MVRV = price / aggregate cost basis):");
let mut td_tops = Vec::new();
let mut tm_tops = Vec::new();
for (year, date_str) in tops.iter() {
if let Some(row) = find_nearest_date(&df_fitted, date_str) {
let mult = row.price_usd / row.centre;
println!(
" {}: MVRV {:.2} price/trend {:.2}",
year, row.cap_mvrv_cur, mult
);
td_tops.push(row.day);
tm_tops.push(mult);
}
}
println!(" cycle bottoms:");
for (year, date_str) in bots.iter() {
if let Some(row) = find_nearest_date(&df_fitted, date_str) {
println!(
" {}: MVRV {:.2} price/trend {:.2}",
year,
row.cap_mvrv_cur,
row.price_usd / row.centre
);
}
}
// Declining ceiling envelope fitted to price/trend at tops: mult = 1 + exp(p + q*day)
let y_ceiling: Vec<f64> = tm_tops.iter().map(|&tm| (tm - 1.0).max(1e-4).ln()).collect();
let (q, p) = polyfit_deg1(&td_tops, &y_ceiling);
println!(
"\n ceiling multiple = 1 + exp({:.4} {:+.6}*day) (decays toward 1: peaks merge into trend)",
p, q
);
println!(" floor multiple ~ 0.45 x trend (price at aggregate cost basis, where capitulation exhausts supply)");
// =========================================================================
// LATEST READING
// =========================================================================
if let Some(now) = df_fitted.last() {
println!(
"\nLATEST ({}): price ${} fair ${} cost basis ${} MVRV {:.2}",
now.time.format("%Y-%m-%d"),
format_usd(now.price_usd),
format_usd(now.centre),
format_usd(now.rp),
now.cap_mvrv_cur
);
}
println!("============================================================");
}
// =============================================================================
// Helper Math & Data Processing Functions
// =============================================================================
/// Polynomial fit of degree 1 (Simple Linear Regression: y = intercept + slope * x)
/// Returns (slope, intercept)
fn polyfit_deg1(x: &[f64], y: &[f64]) -> (f64, f64) {
let n = x.len() as f64;
let sum_x: f64 = x.iter().sum();
let sum_y: f64 = y.iter().sum();
let sum_xy: f64 = x.iter().zip(y.iter()).map(|(&xi, &yi)| xi * yi).sum();
let sum_xx: f64 = x.iter().map(|&xi| xi * xi).sum();
let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x);
let intercept = (sum_y - slope * sum_x) / n;
(slope, intercept)
}
/// Finds row closest to target YYYY-MM-DD date string
fn find_nearest_date<'a>(df: &'a [BtcDataPoint], target_date_str: &str) -> Option<&'a BtcDataPoint> {
let target = NaiveDate::parse_from_str(target_date_str, "%Y-%m-%d").ok()?;
df.iter()
.min_by_key(|row| (row.time - target).num_days().abs())
}
/// Format USD values with comma separators
fn format_usd(val: f64) -> String {
let rounded = val.round() as i64;
let s = rounded.to_string();
let mut result = String::new();
let mut count = 0;
for ch in s.chars().rev() {
if count > 0 && count % 3 == 0 && ch != '-' {
result.push(',');
}
result.push(ch);
count += 1;
}
result.chars().rev().collect()
}
/// Pure-Rust Nelder-Mead Optimization for Layer 2 Non-Linear Least Squares
fn fit_damped_cycle(t: &[f64], r: &[f64]) -> [f64; 5] {
let p0 = [0.6, 0.15, 1430.0, 1000.0, 0.0];
let bounds_min = [0.1, 0.0, 1200.0, -2000.0, -0.3];
let bounds_max = [1.5, 0.6, 1700.0, 4000.0, 0.3];
let loss = |p: &[f64; 5]| -> f64 {
let mut penalty = 0.0;
for i in 0..5 {
if p[i] < bounds_min[i] {
let diff = bounds_min[i] - p[i];
penalty += 1e6 * diff * diff;
} else if p[i] > bounds_max[i] {
let diff = p[i] - bounds_max[i];
penalty += 1e6 * diff * diff;
}
}
let (a0, lam, period, phi, c) = (p[0], p[1], p[2], p[3], p[4]);
let mut sq_err = 0.0;
for (&ti, &ri) in t.iter().zip(r.iter()) {
let pred = c + a0 * (-lam * ti / 365.25).exp() * ((2.0 * PI * (ti - phi)) / period).sin();
let err = ri - pred;
sq_err += err * err;
}
sq_err + penalty
};
let dim = 5;
let num_v = dim + 1;
let mut v = vec![p0; num_v];
let steps = [0.1, 0.02, 50.0, 100.0, 0.05];
for i in 0..dim {
v[i + 1][i] += steps[i];
}
let mut f_vals: Vec<f64> = v.iter().map(|pt| loss(pt)).collect();
for _ in 0..25_000 {
let mut order: Vec<usize> = (0..num_v).collect();
order.sort_by(|&a, &b| f_vals[a].partial_cmp(&f_vals[b]).unwrap());
let mut v_sorted = vec![[0.0; 5]; num_v];
let mut f_sorted = vec![0.0; num_v];
for i in 0..num_v {
v_sorted[i] = v[order[i]];
f_sorted[i] = f_vals[order[i]];
}
v = v_sorted;
f_vals = f_sorted;
let mut centroid = [0.0; 5];
for i in 0..dim {
for d in 0..5 {
centroid[d] += v[i][d];
}
}
for d in 0..5 {
centroid[d] /= dim as f64;
}
let mut xr = [0.0; 5];
for d in 0..5 {
xr[d] = centroid[d] + 1.0 * (centroid[d] - v[dim][d]);
}
let fxr = loss(&xr);
if fxr < f_vals[0] {
let mut xe = [0.0; 5];
for d in 0..5 {
xe[d] = centroid[d] + 2.0 * (xr[d] - centroid[d]);
}
let fxe = loss(&xe);
if fxe < fxr {
v[dim] = xe;
f_vals[dim] = fxe;
} else {
v[dim] = xr;
f_vals[dim] = fxr;
}
} else if fxr < f_vals[dim - 1] {
v[dim] = xr;
f_vals[dim] = fxr;
} else {
let mut xc = [0.0; 5];
let alpha = if fxr < f_vals[dim] { 0.5 } else { -0.5 };
for d in 0..5 {
xc[d] = centroid[d] + alpha * (xr[d] - centroid[d]);
}
let fxc = loss(&xc);
if fxc < f_vals[dim] {
v[dim] = xc;
f_vals[dim] = fxc;
} else {
for i in 1..num_v {
for d in 0..5 {
v[i][d] = v[0][d] + 0.5 * (v[i][d] - v[0][d]);
}
f_vals[i] = loss(&v[i]);
}
}
}
}
v[0]
}
/// Fetch real CSV via HTTP or return error
fn fetch_csv() -> Result<String, Box<dyn std::error::Error>> {
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(15))
.build()?;
let text = client.get(CSV_URL).send()?.text()?;
Ok(text)
}
/// Parse CSV raw string into structured Bitcoin data
fn load_data() -> Result<Vec<BtcDataPoint>, Box<dyn std::error::Error>> {
let raw_csv = fetch_csv()?;
parse_csv_content(&raw_csv)
}
fn parse_csv_content(raw_csv: &str) -> Result<Vec<BtcDataPoint>, Box<dyn std::error::Error>> {
let genesis = NaiveDate::parse_from_str(GENESIS_DATE, "%Y-%m-%d")?;
let mut rdr = csv::ReaderBuilder::new().from_reader(Cursor::new(raw_csv));
let mut data = Vec::new();
for result in rdr.deserialize() {
let row: RawMetricsRow = match result {
Ok(r) => r,
Err(_) => continue,
};
if let (Some(price), Some(cap_mrkt), Some(mvrv), Some(sply)) = (
row.price_usd,
row.cap_mrkt_cur_usd,
row.cap_mvrv_cur,
row.sply_cur,
) {
if price <= 0.0 || mvrv <= 0.0 || sply <= 0.0 {
continue;
}
let date_str = if row.time.len() >= 10 {
&row.time[..10]
} else {
&row.time
};
if let Ok(time) = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
let day = (time - genesis).num_days() as f64;
if day > 0.0 {
let rp = cap_mrkt / mvrv / sply;
data.push(BtcDataPoint {
time,
day,
price_usd: price,
cap_mvrv_cur: mvrv,
rp,
centre: 0.0,
});
}
}
}
}
Ok(data)
}
/// Embedded fallback data generator for offline/playground environments
fn load_demo_data() -> Vec<BtcDataPoint> {
let genesis = NaiveDate::parse_from_str(GENESIS_DATE, "%Y-%m-%d").unwrap();
let dates = [
("2012-01-01", 5.25, 1.12, 7200000.0, 37000000.0),
("2013-11-30", 1125.0, 5.60, 13500000000.0, 12000000.0),
("2015-01-14", 172.0, 0.72, 2350000000.0, 13700000.0),
("2017-12-17", 19783.0, 4.80, 330000000000.0, 16700000.0),
("2018-12-15", 3200.0, 0.70, 55000000000.0, 17400000.0),
("2021-11-10", 69000.0, 4.00, 1280000000000.0, 18800000.0),
("2022-11-21", 15800.0, 0.82, 300000000000.0, 19200000.0),
("2025-10-06", 125000.0, 2.80, 2400000000000.0, 19700000.0),
];
dates
.iter()
.map(|(d_str, p, mvrv, cap, sply)| {
let time = NaiveDate::parse_from_str(d_str, "%Y-%m-%d").unwrap();
let day = (time - genesis).num_days() as f64;
BtcDataPoint {
time,
day,
price_usd: *p,
cap_mvrv_cur: *mvrv,
rp: cap / mvrv / sply,
centre: 0.0,
}
})
.collect()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment