b6d35f6faf
Updated 4763 files with dual copyright: - Parity Technologies (UK) Ltd. - Dijital Kurdistan Tech Institute
310 lines
9.8 KiB
Rust
310 lines
9.8 KiB
Rust
// 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<Balance> {
|
|
/// The price at which the last core was sold.
|
|
///
|
|
/// Will be `None` if no cores have been offered.
|
|
pub sellout_price: Option<Balance>,
|
|
|
|
/// 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<Balance> {
|
|
/// 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<Balance: Copy> SalePerformance<Balance> {
|
|
/// Construct performance via data from a `SaleInfoRecord`.
|
|
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 }
|
|
}
|
|
}
|
|
|
|
/// Type for determining how to set price.
|
|
pub trait AdaptPrice<Balance> {
|
|
/// 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<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 }
|
|
}
|
|
}
|
|
|
|
/// 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<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() {
|
|
// 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<Balance, MinPrice>(core::marker::PhantomData<(Balance, MinPrice)>);
|
|
|
|
impl<Balance: FixedPointOperand, MinPrice: Get<Balance>> AdaptPrice<Balance>
|
|
for MinimumPrice<Balance, MinPrice>
|
|
{
|
|
fn leadin_factor_at(when: FixedU64) -> FixedU64 {
|
|
CenterTargetPrice::<Balance>::leadin_factor_at(when)
|
|
}
|
|
|
|
fn adapt_price(performance: SalePerformance<Balance>) -> AdaptedPrices<Balance> {
|
|
let mut proposal = CenterTargetPrice::<Balance>::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::<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() {
|
|
// 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::<u64, ConstU64<10>>::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::<u64, ConstU64<10>>::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::<u64, ConstU64<0>>::adapt_price(performance);
|
|
let prices = CenterTargetPrice::adapt_price(performance);
|
|
assert_eq!(prices, prices_minimum);
|
|
}
|
|
}
|
|
}
|