use alloy::primitives::U256;
use std::collections::HashMap;
use strike_sdk::indexer::types::Market;
use strike_sdk::prelude::*;
struct QuotedMarket {
bid_ids: Vec<U256>,
ask_ids: Vec<U256>,
position: i64, }
const SPREAD: u8 = 5; const LOTS: u64 = 100;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let private_key = std::env::var("PRIVATE_KEY").expect("PRIVATE_KEY env var required");
let mut client = StrikeClient::new(StrikeConfig::bsc_testnet())
.with_private_key(&private_key)
.build()?;
let signer = client.signer_address().unwrap();
println!("bot wallet: {signer}");
client.init_nonce_sender().await?;
client.vault().approve_usdt().await?;
println!("USDT approved");
let from_block = client.block_number().await?.saturating_sub(5000);
let open_orders = client.scan_orders(from_block, signer).await?;
if !open_orders.is_empty() {
println!(
"found {} markets with open orders from previous run",
open_orders.len()
);
let all_ids: Vec<U256> = open_orders
.values()
.flat_map(|(bids, asks)| bids.iter().chain(asks.iter()).cloned())
.collect();
if !all_ids.is_empty() {
client.orders().cancel(&all_ids).await?;
println!("cancelled {} stale orders", all_ids.len());
}
}
let mut markets: HashMap<u64, QuotedMarket> = HashMap::new();
let mut active_markets: HashMap<u64, Market> = HashMap::new();
for market in client.indexer().get_active_markets().await? {
let orderbook_market_id = match market.tradable_market_id() {
Ok(id) => id,
Err(e) => {
println!(
"skipping active factory market {}: {e}",
market.factory_market_id
);
continue;
}
};
active_markets.insert(orderbook_market_id, market.clone());
if let Some(quoted) = quote_market(&client, &market).await {
markets.insert(orderbook_market_id, quoted);
}
}
println!("listening for events...\n");
let mut events = client.events().await?;
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
let _ = shutdown_tx.send(());
});
loop {
tokio::select! {
_ = &mut shutdown_rx => {
println!("\nshutting down — cancelling all orders...");
let all_ids: Vec<U256> = markets.values()
.flat_map(|m| m.bid_ids.iter().chain(m.ask_ids.iter()).cloned())
.collect();
if !all_ids.is_empty() {
client.orders().cancel(&all_ids).await?;
println!("cancelled {} orders", all_ids.len());
}
return Ok(());
}
Some(event) = events.next() => {
match event {
StrikeEvent::MarketCreated { market_id, strike_price, expiry_time, .. } => {
println!("MarketCreated | id: {market_id} | strike: {strike_price} | expiry: {expiry_time}");
if markets.contains_key(&market_id) {
println!(" market {market_id} already bootstrapped/quoted; skipping duplicate initial quote");
continue;
}
let market = if let Some(market) = active_markets.get(&market_id) {
market.clone()
} else {
let fetched = client
.indexer()
.get_active_markets()
.await?
.into_iter()
.find(|market| market.tradable_market_id().ok() == Some(market_id));
let Some(market) = fetched else {
println!(" market {market_id} not yet available in active indexer markets");
continue;
};
active_markets.insert(market_id, market.clone());
market
};
if let Some(quoted) = quote_market(&client, &market).await {
markets.insert(market_id, quoted);
}
}
StrikeEvent::BatchCleared { market_id, clearing_tick, matched_lots, .. } => {
if matched_lots > 0 {
println!("BatchCleared | market: {market_id} | tick: {clearing_tick} | matched: {matched_lots}");
}
if let Some(quoted) = markets.get(&market_id) {
let cancel_ids: Vec<U256> = quoted.bid_ids.iter()
.chain(quoted.ask_ids.iter())
.cloned()
.collect();
if cancel_ids.is_empty() { continue; }
let Some(fair) = get_fair_tick(&client, market_id).await else {
continue;
};
let bid_tick = fair.saturating_sub(SPREAD).max(1);
let ask_tick = (fair + SPREAD).min(99);
let Some(market) = active_markets.get(&market_id) else {
println!(" requote skipped: market {market_id} missing from active market cache");
continue;
};
match client.orders().replace_market(
&cancel_ids,
market,
&[OrderParam::bid(bid_tick, LOTS), OrderParam::ask(ask_tick, LOTS)],
).await {
Ok(orders) => {
let mut bid_ids = Vec::new();
let mut ask_ids = Vec::new();
for o in &orders {
match o.side {
Side::Bid => bid_ids.push(o.order_id),
Side::Ask => ask_ids.push(o.order_id),
_ => {}
}
}
if let Some(m) = markets.get_mut(&market_id) {
m.bid_ids = bid_ids;
m.ask_ids = ask_ids;
}
println!(" requoted market {market_id} around fair={fair}");
}
Err(e) => println!(" requote failed: {e}"),
}
}
}
StrikeEvent::OrderSettled { order_id, filled_lots, owner, .. } => {
if owner != signer || filled_lots == 0 { continue; }
for (mid, quoted) in markets.iter_mut() {
if quoted.bid_ids.contains(&order_id) {
quoted.position += filled_lots as i64;
println!("FILL | market {mid} | bid filled {filled_lots} lots | position: {}", quoted.position);
} else if quoted.ask_ids.contains(&order_id) {
quoted.position -= filled_lots as i64;
println!("FILL | market {mid} | ask filled {filled_lots} lots | position: {}", quoted.position);
}
}
}
_ => {}
}
}
}
}
}
async fn get_fair_tick(client: &StrikeClient, market_id: u64) -> Option<u8> {
let ob = client.indexer().get_orderbook(market_id).await.ok()?;
let best_bid = ob.bids.first()?.tick as u8;
let best_ask = ob.asks.first()?.tick as u8;
Some((best_bid + best_ask) / 2)
}
async fn quote_market(client: &StrikeClient, market: &Market) -> Option<QuotedMarket> {
let market_id = market.tradable_market_id().ok()?;
let fair = get_fair_tick(client, market_id).await.unwrap_or(50);
let bid_tick = fair.saturating_sub(SPREAD).max(1);
let ask_tick = (fair + SPREAD).min(99);
match client
.orders()
.place_market(
market,
&[
OrderParam::bid(bid_tick, LOTS),
OrderParam::ask(ask_tick, LOTS),
],
)
.await
{
Ok(orders) => {
let mut bid_ids = Vec::new();
let mut ask_ids = Vec::new();
for o in &orders {
match o.side {
Side::Bid => bid_ids.push(o.order_id),
Side::Ask => ask_ids.push(o.order_id),
_ => {}
}
println!(" placed {:?} @ market {market_id}", o.side);
}
Some(QuotedMarket {
bid_ids,
ask_ids,
position: 0,
})
}
Err(e) => {
println!(" place failed for market {market_id}: {e}");
None
}
}
}