#![deny(missing_docs)]
use crate::{CoreIndex, SaleInfoRecord};
use sp_arithmetic::{traits::One, FixedU64};
use sp_runtime::{FixedPointNumber, FixedPointOperand, Saturating};
#[derive(Copy, Clone)]
pub struct SalePerformance<Balance> {
pub sellout_price: Option<Balance>,
pub end_price: Balance,
pub ideal_cores_sold: CoreIndex,
pub cores_offered: CoreIndex,
pub cores_sold: CoreIndex,
}
#[derive(Copy, Clone)]
pub struct AdaptedPrices<Balance> {
pub end_price: Balance,
pub target_price: Balance,
}
impl<Balance: Copy> SalePerformance<Balance> {
pub fn from_sale<BlockNumber>(record: &SaleInfoRecord<Balance, BlockNumber>) -> Self {
Self {
sellout_price: record.sellout_price,
end_price: record.end_price,
ideal_cores_sold: record.ideal_cores_sold,
cores_offered: record.cores_offered,
cores_sold: record.cores_sold,
}
}
#[cfg(test)]
fn new(sellout_price: Option<Balance>, end_price: Balance) -> Self {
Self { sellout_price, end_price, ideal_cores_sold: 0, cores_offered: 0, cores_sold: 0 }
}
}
pub trait AdaptPrice<Balance> {
fn leadin_factor_at(when: FixedU64) -> FixedU64;
fn adapt_price(performance: SalePerformance<Balance>) -> AdaptedPrices<Balance>;
}
impl<Balance: Copy> AdaptPrice<Balance> for () {
fn leadin_factor_at(_: FixedU64) -> FixedU64 {
FixedU64::one()
}
fn adapt_price(performance: SalePerformance<Balance>) -> AdaptedPrices<Balance> {
let price = performance.sellout_price.unwrap_or(performance.end_price);
AdaptedPrices { end_price: price, target_price: price }
}
}
pub struct CenterTargetPrice<Balance>(core::marker::PhantomData<Balance>);
impl<Balance: FixedPointOperand> AdaptPrice<Balance> for CenterTargetPrice<Balance> {
fn leadin_factor_at(when: FixedU64) -> FixedU64 {
if when <= FixedU64::from_rational(1, 2) {
FixedU64::from(100).saturating_sub(when.saturating_mul(180.into()))
} else {
FixedU64::from(19).saturating_sub(when.saturating_mul(18.into()))
}
}
fn adapt_price(performance: SalePerformance<Balance>) -> AdaptedPrices<Balance> {
let Some(sellout_price) = performance.sellout_price else {
return AdaptedPrices {
end_price: performance.end_price,
target_price: FixedU64::from(10).saturating_mul_int(performance.end_price),
}
};
let price = FixedU64::from_rational(1, 10).saturating_mul_int(sellout_price);
let price = if price == Balance::zero() {
sellout_price
} else {
price
};
AdaptedPrices { end_price: price, target_price: sellout_price }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linear_no_panic() {
for sellout in 0..11 {
for price in 0..10 {
let sellout_price = if sellout == 11 { None } else { Some(sellout) };
CenterTargetPrice::adapt_price(SalePerformance::new(sellout_price, price));
}
}
}
#[test]
fn leadin_price_bound_check() {
assert_eq!(
CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::from(0)),
FixedU64::from(100)
);
assert_eq!(
CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::from_rational(1, 4)),
FixedU64::from(55)
);
assert_eq!(
CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::from_float(0.5)),
FixedU64::from(10)
);
assert_eq!(
CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::from_rational(3, 4)),
FixedU64::from_float(5.5)
);
assert_eq!(CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::one()), FixedU64::one());
}
#[test]
fn no_op_sale_is_good() {
let prices = CenterTargetPrice::adapt_price(SalePerformance::new(None, 1));
assert_eq!(prices.target_price, 10);
assert_eq!(prices.end_price, 1);
}
#[test]
fn price_stays_stable_on_optimal_sale() {
let mut performance = SalePerformance::new(Some(1000), 100);
for _ in 0..10 {
let prices = CenterTargetPrice::adapt_price(performance);
performance.sellout_price = Some(1000);
performance.end_price = prices.end_price;
assert!(prices.end_price <= 101);
assert!(prices.end_price >= 99);
assert!(prices.target_price <= 1001);
assert!(prices.target_price >= 999);
}
}
#[test]
fn price_adjusts_correctly_upwards() {
let performance = SalePerformance::new(Some(10_000), 100);
let prices = CenterTargetPrice::adapt_price(performance);
assert_eq!(prices.target_price, 10_000);
assert_eq!(prices.end_price, 1000);
}
#[test]
fn price_adjusts_correctly_downwards() {
let performance = SalePerformance::new(Some(100), 100);
let prices = CenterTargetPrice::adapt_price(performance);
assert_eq!(prices.target_price, 100);
assert_eq!(prices.end_price, 10);
}
#[test]
fn price_never_goes_to_zero_and_recovers() {
let sellout_price = 1;
let mut performance = SalePerformance::new(Some(sellout_price), 1);
for _ in 0..11 {
let prices = CenterTargetPrice::adapt_price(performance);
performance.sellout_price = Some(sellout_price);
performance.end_price = prices.end_price;
assert!(prices.end_price <= sellout_price);
assert!(prices.end_price > 0);
}
}
#[test]
fn renewal_price_is_correct_on_no_sale() {
let performance = SalePerformance::new(None, 100);
let prices = CenterTargetPrice::adapt_price(performance);
assert_eq!(prices.target_price, 1000);
assert_eq!(prices.end_price, 100);
}
#[test]
fn renewal_price_is_sell_out() {
let performance = SalePerformance::new(Some(1000), 100);
let prices = CenterTargetPrice::adapt_price(performance);
assert_eq!(prices.target_price, 1000);
}
}