// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #![deny(missing_docs)] use crate::{CoreIndex, SaleInfoRecord}; use pezsp_arithmetic::{traits::One, FixedU64}; use pezsp_core::{Get, RuntimeDebug}; use pezsp_runtime::{FixedPointNumber, FixedPointOperand, Saturating}; /// Performance of a past sale. #[derive(Copy, Clone)] pub struct SalePerformance { /// The price at which the last core was sold. /// /// Will be `None` if no cores have been offered. pub sellout_price: Option, /// The minimum price that was achieved in this sale. pub end_price: Balance, /// The number of cores we want to sell, ideally. pub ideal_cores_sold: CoreIndex, /// Number of cores which are/have been offered for sale. pub cores_offered: CoreIndex, /// Number of cores which have been sold; never more than cores_offered. pub cores_sold: CoreIndex, } /// Result of `AdaptPrice::adapt_price`. #[derive(Copy, Clone, RuntimeDebug, Eq, PartialEq)] pub struct AdaptedPrices { /// New minimum price to use. pub end_price: Balance, /// Price the controller is optimizing for. /// /// This is the price "expected" by the controller based on the previous sale. We assume that /// sales in this period will be around this price, assuming stable market conditions. /// /// Think of it as the expected market price. This can be used for determining what to charge /// for renewals, that don't yet have any price information for example. E.g. for expired /// legacy leases. pub target_price: Balance, } impl SalePerformance { /// Construct performance via data from a `SaleInfoRecord`. pub fn from_sale(record: &SaleInfoRecord) -> 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, end_price: Balance) -> Self { Self { sellout_price, end_price, ideal_cores_sold: 0, cores_offered: 0, cores_sold: 0 } } } /// Type for determining how to set price. pub trait AdaptPrice { /// Return the factor by which the regular price must be multiplied during the leadin period. /// /// - `when`: The amount through the leadin period; between zero and one. fn leadin_factor_at(when: FixedU64) -> FixedU64; /// Return adapted prices for next sale. /// /// Based on the previous sale's performance. fn adapt_price(performance: SalePerformance) -> AdaptedPrices; } impl AdaptPrice for () { fn leadin_factor_at(_: FixedU64) -> FixedU64 { FixedU64::one() } fn adapt_price(performance: SalePerformance) -> AdaptedPrices { let price = performance.sellout_price.unwrap_or(performance.end_price); AdaptedPrices { end_price: price, target_price: price } } } /// Simple implementation of `AdaptPrice` with two linear phases. /// /// One steep one downwards to the target price, which is 1/10 of the maximum price and a more flat /// one down to the minimum price, which is 1/100 of the maximum price. pub struct CenterTargetPrice(core::marker::PhantomData); impl AdaptPrice for CenterTargetPrice { 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) -> AdaptedPrices { 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() { // We could not recover from a price equal 0 ever. sellout_price } else { price }; AdaptedPrices { end_price: price, target_price: sellout_price } } } /// `AdaptPrice` like `CenterTargetPrice`, but with a minimum price. /// /// This price adapter behaves exactly like `CenterTargetPrice`, except that it takes a minimum /// price and makes sure that the returned `end_price` is never lower than that. /// /// Target price will also get adjusted if necessary (it will never be less than the end_price). pub struct MinimumPrice(core::marker::PhantomData<(Balance, MinPrice)>); impl> AdaptPrice for MinimumPrice { fn leadin_factor_at(when: FixedU64) -> FixedU64 { CenterTargetPrice::::leadin_factor_at(when) } fn adapt_price(performance: SalePerformance) -> AdaptedPrices { let mut proposal = CenterTargetPrice::::adapt_price(performance); let min_price = MinPrice::get(); if proposal.end_price < min_price { proposal.end_price = min_price; } // Fix target price if necessary: if proposal.target_price < proposal.end_price { proposal.target_price = proposal.end_price; } proposal } } #[cfg(test)] mod tests { use pezsp_core::ConstU64; 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::::leadin_factor_at(FixedU64::from(0)), FixedU64::from(100) ); assert_eq!( CenterTargetPrice::::leadin_factor_at(FixedU64::from_rational(1, 4)), FixedU64::from(55) ); assert_eq!( CenterTargetPrice::::leadin_factor_at(FixedU64::from_float(0.5)), FixedU64::from(10) ); assert_eq!( CenterTargetPrice::::leadin_factor_at(FixedU64::from_rational(3, 4)), FixedU64::from_float(5.5) ); assert_eq!(CenterTargetPrice::::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() { // Check price stays stable if sold at the optimal price: 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() { // Check price stays stable if sold at the optimal price: 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); } #[test] fn minimum_price_works() { let performance = SalePerformance::new(Some(10), 10); let prices = MinimumPrice::>::adapt_price(performance); assert_eq!(prices.end_price, 10); assert_eq!(prices.target_price, 10); } #[test] fn minimum_price_does_not_affect_valid_target_price() { let performance = SalePerformance::new(Some(12), 10); let prices = MinimumPrice::>::adapt_price(performance); assert_eq!(prices.end_price, 10); assert_eq!(prices.target_price, 12); } #[test] fn no_minimum_price_works_as_center_target_price() { let performances = [ (Some(100), 10), (None, 20), (Some(1000), 10), (Some(10), 10), (Some(1), 1), (Some(0), 10), ]; for (sellout, end) in performances { let performance = SalePerformance::new(sellout, end); let prices_minimum = MinimumPrice::>::adapt_price(performance); let prices = CenterTargetPrice::adapt_price(performance); assert_eq!(prices, prices_minimum); } } }