use bot_core::{
Environment, ExchangeInstance, HyperliquidMarket, InstrumentId, Market, MarketIndex, StrategyId,
};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SkewMode {
None,
Size,
Price,
Both,
}
impl Default for SkewMode {
fn default() -> Self {
Self::Both
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketMakerConfig {
pub strategy_id: StrategyId,
pub environment: Environment,
pub market: Market,
pub base_order_size: Decimal,
pub base_spread: Decimal,
pub target_position_pct: Decimal,
pub min_position_pct: Decimal,
pub max_position_pct: Decimal,
pub max_position_size: Decimal,
pub skew_mode: SkewMode,
pub price_skew_gamma: Decimal,
pub size_skew_floor: Decimal,
pub min_price_change_pct: Decimal,
pub stop_loss: Option<Decimal>,
pub take_profit: Option<Decimal>,
}
impl MarketMakerConfig {
pub fn is_spot(&self) -> bool {
self.market.is_spot_like()
}
pub fn instrument_id(&self) -> InstrumentId {
self.market.instrument_id()
}
pub fn exchange_instance(&self) -> ExchangeInstance {
self.market.exchange_instance(self.environment)
}
pub fn market_index(&self) -> MarketIndex {
self.market.market_index()
}
pub fn validate(&self) -> Vec<String> {
let mut errors = Vec::new();
if self.max_position_size <= Decimal::ZERO {
errors.push("max_position_size must be > 0 (division by zero)".into());
}
if self.base_spread <= Decimal::ZERO {
errors.push("base_spread must be > 0 (prevents BUY/SELL at same price)".into());
}
if self.base_order_size <= Decimal::ZERO {
errors.push("base_order_size must be > 0".into());
}
if self.size_skew_floor <= Decimal::ZERO || self.size_skew_floor > Decimal::ONE {
errors.push("size_skew_floor must be in (0, 1] range".into());
}
let max_safe_gamma = Decimal::ONE - self.base_spread - Decimal::new(1, 2); if self.price_skew_gamma < Decimal::ZERO {
errors.push("price_skew_gamma must be >= 0".into());
} else if self.price_skew_gamma > max_safe_gamma {
errors.push(format!(
"price_skew_gamma={} too high! Max safe value: {} (prevents negative prices)",
self.price_skew_gamma, max_safe_gamma
));
}
if self.target_position_pct < Decimal::ZERO || self.target_position_pct > Decimal::ONE {
errors.push("target_position_pct must be in [0, 1] range".into());
}
if self.min_position_pct < Decimal::ZERO || self.min_position_pct > Decimal::ONE {
errors.push("min_position_pct must be in [0, 1] range".into());
}
if self.max_position_pct < Decimal::ZERO || self.max_position_pct > Decimal::ONE {
errors.push("max_position_pct must be in [0, 1] range".into());
}
if self.min_position_pct >= self.max_position_pct {
errors.push("min_position_pct must be < max_position_pct".into());
}
errors
}
}
impl Default for MarketMakerConfig {
fn default() -> Self {
Self {
strategy_id: StrategyId::new("market-maker-default"),
environment: Environment::Testnet,
market: Market::Hyperliquid(HyperliquidMarket::Perp {
base: "BTC".to_string(),
quote: "USDC".to_string(),
index: 0,
instrument_meta: None,
}),
base_order_size: Decimal::new(1, 3), base_spread: Decimal::new(1, 3),
target_position_pct: Decimal::new(5, 1), min_position_pct: Decimal::new(1, 1), max_position_pct: Decimal::new(9, 1), max_position_size: Decimal::ONE,
skew_mode: SkewMode::Both,
price_skew_gamma: Decimal::new(5, 2), size_skew_floor: Decimal::new(2, 1),
min_price_change_pct: Decimal::new(5, 4),
stop_loss: None,
take_profit: None,
}
}
}