303 lines
11 KiB
Rust
303 lines
11 KiB
Rust
use anchor_lang::prelude::*;
|
|
use derivative::Derivative;
|
|
use static_assertions::const_assert_eq;
|
|
use std::mem::size_of;
|
|
|
|
/// Maintains a "stable_price" based on the oracle price.
|
|
///
|
|
/// The stable price follows the oracle price, but its relative rate of
|
|
/// change is limited (to `stable_growth_limit`) and futher reduced if
|
|
/// the oracle price is far from the `delay_price`.
|
|
///
|
|
/// Conceptually the `delay_price` is itself a time delayed
|
|
/// (`24 * delay_interval_seconds`, assume 24h) and relative rate of change limited
|
|
/// function of the oracle price. It is implemented as averaging the oracle
|
|
/// price over every `delay_interval_seconds` (assume 1h) and then applying the
|
|
/// `delay_growth_limit` between intervals.
|
|
#[zero_copy]
|
|
#[derive(Derivative)]
|
|
#[derivative(Debug)]
|
|
pub struct StablePriceModel {
|
|
/// Current stable price to use in health
|
|
pub stable_price: f64,
|
|
|
|
pub last_update_timestamp: u64,
|
|
|
|
/// Stored delay_price for each delay_interval.
|
|
/// If we want the delay_price to be 24h delayed, we would store one for each hour.
|
|
/// This is used in a cyclical way: We use the maximally-delayed value at delay_interval_index
|
|
/// and once enough time passes to move to the next delay interval, that gets overwritten and
|
|
/// we use the next one.
|
|
pub delay_prices: [f64; 24],
|
|
|
|
/// The delay price is based on an average over each delay_interval. The contributions
|
|
/// to the average are summed up here.
|
|
pub delay_accumulator_price: f64,
|
|
|
|
/// Accumulating the total time for the above average.
|
|
pub delay_accumulator_time: u32,
|
|
|
|
/// Length of a delay_interval
|
|
pub delay_interval_seconds: u32,
|
|
|
|
/// Maximal relative difference between two delay_price in consecutive intervals.
|
|
pub delay_growth_limit: f32,
|
|
|
|
/// Maximal per-second relative difference of the stable price.
|
|
/// It gets further reduced if stable and delay price disagree.
|
|
pub stable_growth_limit: f32,
|
|
|
|
/// The delay_interval_index that update() was last called on.
|
|
pub last_delay_interval_index: u8,
|
|
|
|
/// If set to 1, the stable price will reset on the next non-zero price it sees.
|
|
pub reset_on_nonzero_price: u8,
|
|
|
|
#[derivative(Debug = "ignore")]
|
|
pub padding: [u8; 6],
|
|
|
|
#[derivative(Debug = "ignore")]
|
|
pub reserved: [u8; 48],
|
|
}
|
|
const_assert_eq!(
|
|
size_of::<StablePriceModel>(),
|
|
8 + 8 + 8 * 24 + 8 + 4 + 4 + 4 + 4 + 1 * 2 + 6 + 48
|
|
);
|
|
const_assert_eq!(size_of::<StablePriceModel>(), 288);
|
|
const_assert_eq!(size_of::<StablePriceModel>() % 8, 0);
|
|
|
|
impl Default for StablePriceModel {
|
|
fn default() -> Self {
|
|
Self {
|
|
stable_price: 0.0,
|
|
last_update_timestamp: 0,
|
|
delay_prices: [0.0; 24],
|
|
delay_accumulator_price: 0.0,
|
|
delay_accumulator_time: 0,
|
|
delay_interval_seconds: 60 * 60, // 1h, for a total delay of 24h
|
|
delay_growth_limit: 0.06, // 6% per hour, 400% per day
|
|
stable_growth_limit: 0.0003, // 0.03% per second, 293% in 1h if updated every 10s, 281% in 1h if updated every 5min
|
|
last_delay_interval_index: 0,
|
|
reset_on_nonzero_price: 0,
|
|
padding: Default::default(),
|
|
reserved: [0; 48],
|
|
}
|
|
}
|
|
}
|
|
|
|
impl StablePriceModel {
|
|
pub fn reset_to_price(&mut self, oracle_price: f64, now_ts: u64) {
|
|
self.stable_price = oracle_price;
|
|
self.delay_prices = [oracle_price; 24];
|
|
self.delay_accumulator_price = 0.0;
|
|
self.delay_accumulator_time = 0;
|
|
self.last_update_timestamp = now_ts;
|
|
self.reset_on_nonzero_price = if oracle_price > 0.0 { 0 } else { 1 };
|
|
}
|
|
|
|
pub fn delay_interval_index(&self, timestamp: u64) -> u8 {
|
|
((timestamp / self.delay_interval_seconds as u64) % self.delay_prices.len() as u64) as u8
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn growth_clamped(target: f64, prev: f64, growth_limit: f64) -> f64 {
|
|
let max = prev * (1.0 + growth_limit);
|
|
// for the lower bound, we technically should divide by (1 + growth_limit), but
|
|
// the error is small when growth_limit is small and this saves a division
|
|
let min = prev * (1.0 - growth_limit);
|
|
target.clamp(min, max)
|
|
}
|
|
|
|
pub fn update(&mut self, now_ts: u64, oracle_price: f64) {
|
|
// If a reset is requested (maybe there never was a non-zero price), jump to the current value
|
|
if self.reset_on_nonzero_price == 1 && oracle_price > 0.0 {
|
|
self.reset_to_price(oracle_price, now_ts);
|
|
}
|
|
|
|
let dt = now_ts.saturating_sub(self.last_update_timestamp);
|
|
// Hardcoded. Requiring a minimum time between updates reduces the possible difference
|
|
// between frequent updates and infrequent ones.
|
|
// Limiting the max dt prevents very strong updates if update() hasn't been
|
|
// called for hours.
|
|
let min_dt = 10;
|
|
let max_dt = 10 * 60; // 10 min
|
|
if dt < min_dt {
|
|
return;
|
|
}
|
|
// did we wrap around all delay intervals?
|
|
let full_delay_passed =
|
|
dt > self.delay_prices.len() as u64 * self.delay_interval_seconds as u64;
|
|
let dt_limited = dt.min(max_dt) as f64;
|
|
self.last_update_timestamp = now_ts;
|
|
|
|
//
|
|
// Update delay price
|
|
//
|
|
self.delay_accumulator_time += dt as u32;
|
|
self.delay_accumulator_price += oracle_price * dt_limited;
|
|
|
|
let delay_interval_index = self.delay_interval_index(now_ts);
|
|
if delay_interval_index != self.last_delay_interval_index {
|
|
// last_delay_interval_index points to the most delayed price, which we will
|
|
// overwrite with a new delay price
|
|
let new_delay_price = {
|
|
// Get the previous new delay_price.
|
|
let prev = if self.last_delay_interval_index == 0 {
|
|
self.delay_prices[self.delay_prices.len() - 1]
|
|
} else {
|
|
self.delay_prices[self.last_delay_interval_index as usize - 1]
|
|
};
|
|
let avg = self.delay_accumulator_price / (self.delay_accumulator_time as f64);
|
|
Self::growth_clamped(avg, prev, self.delay_growth_limit as f64)
|
|
};
|
|
|
|
// Store the new delay price, accounting for skipped intervals
|
|
if full_delay_passed {
|
|
self.delay_prices.fill(new_delay_price);
|
|
} else if delay_interval_index > self.last_delay_interval_index {
|
|
self.delay_prices
|
|
[self.last_delay_interval_index as usize..delay_interval_index as usize]
|
|
.fill(new_delay_price);
|
|
} else {
|
|
self.delay_prices[self.last_delay_interval_index as usize..].fill(new_delay_price);
|
|
self.delay_prices[..delay_interval_index as usize].fill(new_delay_price);
|
|
}
|
|
|
|
self.delay_accumulator_price = 0.0;
|
|
self.delay_accumulator_time = 0;
|
|
self.last_delay_interval_index = delay_interval_index;
|
|
}
|
|
|
|
let delay_price = self.delay_prices[delay_interval_index as usize];
|
|
|
|
//
|
|
// Update stable price
|
|
//
|
|
self.stable_price = {
|
|
let prev_stable_price = self.stable_price;
|
|
let fraction = if delay_price >= prev_stable_price {
|
|
prev_stable_price / delay_price
|
|
} else {
|
|
delay_price / prev_stable_price
|
|
};
|
|
let growth_limit = (self.stable_growth_limit as f64) * fraction * fraction * dt_limited;
|
|
Self::growth_clamped(oracle_price, prev_stable_price, growth_limit)
|
|
};
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn run_and_print(
|
|
model: &mut StablePriceModel,
|
|
start: u64,
|
|
dt: u64,
|
|
steps: u64,
|
|
price: fn(u64) -> f64,
|
|
) -> u64 {
|
|
println!("step,timestamp,stable_price,delay_price");
|
|
for i in 0..steps {
|
|
let time = start + dt * (i + 1);
|
|
model.update(time, price(time));
|
|
println!(
|
|
"{i},{time},{},{}",
|
|
model.stable_price, model.delay_prices[model.last_delay_interval_index as usize]
|
|
);
|
|
}
|
|
start + dt * steps
|
|
}
|
|
|
|
#[test]
|
|
fn test_stable_price_10x() {
|
|
let mut model = StablePriceModel::default();
|
|
model.reset_to_price(1.0, 0);
|
|
|
|
let mut t;
|
|
t = run_and_print(&mut model, 0, 60, 60, |_| 10.0);
|
|
assert!((model.stable_price - 1.8).abs() < 0.1);
|
|
assert_eq!(model.delay_prices[1..], [1.0; 23]);
|
|
assert!((model.delay_prices[0] - 1.06).abs() < 0.01);
|
|
assert_eq!(model.last_delay_interval_index, 1);
|
|
assert_eq!(model.delay_accumulator_time, 0);
|
|
assert_eq!(model.delay_accumulator_price, 0.0);
|
|
|
|
t = run_and_print(&mut model, t, 10, 6 * 60, |_| 10.0);
|
|
assert!((model.stable_price - 2.3).abs() < 0.1);
|
|
assert_eq!(model.delay_prices[2..], [1.0; 22]);
|
|
assert!((model.delay_prices[0] - 1.06).abs() < 0.01);
|
|
assert!((model.delay_prices[1] - 1.06 * 1.06).abs() < 0.01);
|
|
assert_eq!(model.last_delay_interval_index, 2);
|
|
assert_eq!(model.delay_accumulator_time, 0);
|
|
assert_eq!(model.delay_accumulator_price, 0.0);
|
|
|
|
// check delay price wraparound (go to 25h since start)
|
|
t = run_and_print(&mut model, t, 300, 12 * 23, |_| 10.0);
|
|
assert!((model.stable_price - 7.4).abs() < 0.1);
|
|
assert!(model.delay_prices[0] > model.delay_prices[23]);
|
|
assert!(model.delay_prices[23] > model.delay_prices[22]);
|
|
assert!(model.delay_prices[1] < model.delay_prices[0]);
|
|
assert!(model.delay_prices[1] < model.delay_prices[2]);
|
|
assert_eq!(model.last_delay_interval_index, 1);
|
|
|
|
println!("{t}");
|
|
}
|
|
|
|
#[test]
|
|
fn test_stable_price_characteristics_upwards() {
|
|
let mut model = StablePriceModel::default();
|
|
|
|
model.reset_to_price(1.0, 0);
|
|
|
|
let mut last = 1;
|
|
for i in 0..100000 {
|
|
model.update(60 * (i + 1), 1000.0);
|
|
let now = model.stable_price as i32;
|
|
if now > last {
|
|
last = now;
|
|
println!("reached {now}x after {i} steps, {} hours", i as f64 / 60.0);
|
|
if now == 10 {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_stable_price_characteristics_downwards() {
|
|
let mut model = StablePriceModel::default();
|
|
let init = 10000.0;
|
|
model.reset_to_price(init, 0);
|
|
|
|
let mut last = 1;
|
|
for i in 0..100000 {
|
|
model.update(60 * (i + 1), 0.0);
|
|
let now = (init / model.stable_price) as i32;
|
|
if now > last {
|
|
last = now;
|
|
println!(
|
|
"reached 1/{now}x after {i} steps, {} hours",
|
|
i as f64 / 60.0
|
|
);
|
|
if now == 10 {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_stable_price_average() {
|
|
let mut model = StablePriceModel {
|
|
delay_growth_limit: 10.00,
|
|
..StablePriceModel::default()
|
|
};
|
|
model.reset_to_price(1.0, 0);
|
|
|
|
run_and_print(&mut model, 0, 60, 60, |t| if t > 1800 { 2.0 } else { 1.0 });
|
|
println!("{}", model.delay_prices[0]);
|
|
assert!((model.delay_prices[0] - 1.5).abs() < 0.01);
|
|
}
|
|
}
|