use std::collections::HashMap;
use serde::Serialize;
use crate::models::skyblock::{Bazaar, BazaarProduct, SkyBlockAuction};
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub struct BazaarSpread {
pub buy_order_price: f64,
pub sell_order_price: f64,
}
impl BazaarSpread {
pub fn margin(&self) -> f64 {
(self.sell_order_price - self.buy_order_price).abs()
}
}
pub fn bazaar_spread(product: &BazaarProduct) -> Option<BazaarSpread> {
let sell_order_price = product.sell_summary.first()?.price_per_unit;
let buy_order_price = product.buy_summary.first()?.price_per_unit;
Some(BazaarSpread {
buy_order_price,
sell_order_price,
})
}
pub fn bazaar_spreads(bazaar: &Bazaar) -> HashMap<String, BazaarSpread> {
bazaar
.products
.iter()
.filter_map(|(id, product)| bazaar_spread(product).map(|s| (id.clone(), s)))
.collect()
}
pub fn lowest_bin(auctions: &[SkyBlockAuction]) -> HashMap<String, i64> {
let mut lowest: HashMap<String, i64> = HashMap::new();
for auction in auctions {
if !auction.bin {
continue;
}
lowest
.entry(auction.item_name.clone())
.and_modify(|price| {
if auction.starting_bid < *price {
*price = auction.starting_bid;
}
})
.or_insert(auction.starting_bid);
}
lowest
}
#[cfg(feature = "nbt")]
pub fn lowest_bin_by_id(auctions: &[SkyBlockAuction]) -> HashMap<String, i64> {
use crate::util::{nbt, networth};
let mut lowest: HashMap<String, i64> = HashMap::new();
for auction in auctions {
if !auction.bin {
continue;
}
let Ok(decoded) = nbt::decode_inventory(&auction.item_bytes) else {
continue;
};
let Some(stack) = networth::extract_item_stacks(&decoded).into_iter().next() else {
continue;
};
lowest
.entry(stack.item_id)
.and_modify(|price| *price = (*price).min(auction.starting_bid))
.or_insert(auction.starting_bid);
}
lowest
}
pub const BAZAAR_TAX: f64 = 0.0125;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct BazaarFlip {
pub product_id: String,
pub buy_order_price: f64,
pub sell_order_price: f64,
pub profit_per_item: f64,
pub margin: f64,
pub buy_moving_week: i64,
pub sell_moving_week: i64,
}
pub fn bazaar_flips(bazaar: &Bazaar, min_weekly_volume: i64) -> Vec<BazaarFlip> {
let mut flips: Vec<BazaarFlip> = bazaar
.products
.iter()
.filter_map(|(id, product)| {
let spread = bazaar_spread(product)?;
let status = product.quick_status.as_ref()?;
if status.buy_moving_week < min_weekly_volume
|| status.sell_moving_week < min_weekly_volume
{
return None;
}
let profit = spread.sell_order_price * (1.0 - BAZAAR_TAX) - spread.buy_order_price;
if profit <= 0.0 || spread.buy_order_price <= 0.0 {
return None;
}
Some(BazaarFlip {
product_id: id.clone(),
buy_order_price: spread.buy_order_price,
sell_order_price: spread.sell_order_price,
profit_per_item: profit,
margin: profit / spread.buy_order_price,
buy_moving_week: status.buy_moving_week,
sell_moving_week: status.sell_moving_week,
})
})
.collect();
flips.sort_by(|a, b| {
b.profit_per_item
.partial_cmp(&a.profit_per_item)
.unwrap_or(std::cmp::Ordering::Equal)
});
flips
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::skyblock::{BazaarOrder, BazaarProduct};
fn product(sell: f64, buy: f64) -> BazaarProduct {
BazaarProduct {
product_id: "X".into(),
sell_summary: vec![BazaarOrder {
amount: 1,
price_per_unit: sell,
orders: 1,
}],
buy_summary: vec![BazaarOrder {
amount: 1,
price_per_unit: buy,
orders: 1,
}],
quick_status: None,
}
}
#[test]
fn computes_spread_margin() {
let spread = bazaar_spread(&product(5.0, 4.2)).unwrap();
assert_eq!(spread.sell_order_price, 5.0);
assert_eq!(spread.buy_order_price, 4.2);
assert!((spread.margin() - 0.8).abs() < 1e-9);
}
#[test]
fn flips_filter_volume_and_apply_tax() {
use crate::models::skyblock::BazaarQuickStatus;
let mut liquid = product(100.0, 50.0);
liquid.quick_status = Some(BazaarQuickStatus {
buy_moving_week: 100_000,
sell_moving_week: 100_000,
..serde_json::from_str("{}").unwrap()
});
let mut illiquid = product(1_000.0, 1.0);
illiquid.quick_status = Some(BazaarQuickStatus {
buy_moving_week: 10,
sell_moving_week: 100_000,
..serde_json::from_str("{}").unwrap()
});
let bazaar = Bazaar {
last_updated: 0,
products: [("LIQUID".into(), liquid), ("ILLIQUID".into(), illiquid)]
.into_iter()
.collect(),
};
let flips = bazaar_flips(&bazaar, 1_000);
assert_eq!(flips.len(), 1);
assert_eq!(flips[0].product_id, "LIQUID");
assert!((flips[0].profit_per_item - (100.0 * (1.0 - BAZAAR_TAX) - 50.0)).abs() < 1e-9);
}
}