2022-03-23 01:07:00 -07:00
|
|
|
use {
|
|
|
|
log::*,
|
2022-03-23 10:39:42 -07:00
|
|
|
serde_derive::{Deserialize, Serialize},
|
2022-03-23 01:07:00 -07:00
|
|
|
solana_geyser_connector_lib::chain_data::ChainData,
|
|
|
|
solana_geyser_connector_lib::*,
|
2022-03-23 09:25:03 -07:00
|
|
|
solana_sdk::pubkey::Pubkey,
|
|
|
|
std::str::FromStr,
|
2022-03-23 01:07:00 -07:00
|
|
|
std::{
|
|
|
|
fs::File,
|
|
|
|
io::Read,
|
2022-11-20 19:47:20 -08:00
|
|
|
mem::size_of,
|
2022-03-23 01:07:00 -07:00
|
|
|
sync::{Arc, RwLock},
|
2022-11-20 19:47:20 -08:00
|
|
|
time::Duration,
|
2022-03-23 01:07:00 -07:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2022-11-20 19:47:20 -08:00
|
|
|
use anchor_client::Cluster;
|
|
|
|
use anchor_lang::Discriminator;
|
|
|
|
use client::{chain_data, health_cache, AccountFetcher, Client, MangoGroupContext};
|
2022-03-23 09:25:03 -07:00
|
|
|
use fixed::types::I80F48;
|
2022-11-20 19:47:20 -08:00
|
|
|
use mango_v4::state::{MangoAccount, MangoAccountValue, PerpMarketIndex};
|
2022-10-08 10:13:26 -07:00
|
|
|
use solana_geyser_connector_lib::metrics::*;
|
2022-11-20 19:47:20 -08:00
|
|
|
use solana_sdk::commitment_config::CommitmentConfig;
|
|
|
|
use solana_sdk::{account::ReadableAccount, signature::Keypair};
|
2022-03-24 05:44:24 -07:00
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
|
|
pub struct PnlConfig {
|
|
|
|
pub update_interval_millis: u64,
|
|
|
|
pub mango_program: String,
|
|
|
|
pub mango_group: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
|
|
pub struct JsonRpcConfig {
|
|
|
|
pub bind_address: String,
|
|
|
|
}
|
|
|
|
|
2022-03-23 01:07:00 -07:00
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
|
|
pub struct Config {
|
|
|
|
pub source: SourceConfig,
|
2022-11-20 19:47:20 -08:00
|
|
|
pub snapshot_source: SnapshotSourceConfig,
|
2022-10-07 03:44:53 -07:00
|
|
|
pub metrics: MetricsConfig,
|
2022-03-24 05:44:24 -07:00
|
|
|
pub pnl: PnlConfig,
|
|
|
|
pub jsonrpc_server: JsonRpcConfig,
|
2022-03-23 01:07:00 -07:00
|
|
|
}
|
|
|
|
|
2022-11-20 19:47:20 -08:00
|
|
|
type PnlData = Vec<(Pubkey, Vec<(PerpMarketIndex, I80F48)>)>;
|
2022-03-23 10:39:42 -07:00
|
|
|
|
2023-01-01 08:42:36 -08:00
|
|
|
async fn compute_pnl(
|
2022-11-20 19:47:20 -08:00
|
|
|
context: Arc<MangoGroupContext>,
|
|
|
|
account_fetcher: Arc<impl AccountFetcher>,
|
|
|
|
account: &MangoAccountValue,
|
|
|
|
) -> anyhow::Result<Vec<(PerpMarketIndex, I80F48)>> {
|
2023-01-01 08:42:36 -08:00
|
|
|
let health_cache = health_cache::new(&context, account_fetcher.as_ref(), account).await?;
|
2022-11-20 19:47:20 -08:00
|
|
|
let perp_settle_health = health_cache.perp_settle_health();
|
|
|
|
|
|
|
|
let pnls = account
|
|
|
|
.active_perp_positions()
|
|
|
|
.filter_map(|pp| {
|
|
|
|
if pp.base_position_lots() != 0 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
let pnl = pp.quote_position_native();
|
|
|
|
let settleable_pnl = if pnl > 0 {
|
|
|
|
pnl
|
|
|
|
} else if pnl < 0 && perp_settle_health > 0 {
|
|
|
|
pnl.max(-perp_settle_health)
|
|
|
|
} else {
|
|
|
|
return None;
|
|
|
|
};
|
|
|
|
Some((pp.market_index, settleable_pnl))
|
|
|
|
})
|
|
|
|
.collect::<Vec<(PerpMarketIndex, I80F48)>>();
|
|
|
|
|
|
|
|
Ok(pnls)
|
2022-03-23 09:25:03 -07:00
|
|
|
}
|
|
|
|
|
2022-03-23 10:39:42 -07:00
|
|
|
// regularly updates pnl_data from chain_data
|
2022-03-24 05:44:24 -07:00
|
|
|
fn start_pnl_updater(
|
|
|
|
config: PnlConfig,
|
2022-11-20 19:47:20 -08:00
|
|
|
context: Arc<MangoGroupContext>,
|
|
|
|
account_fetcher: Arc<impl AccountFetcher + 'static>,
|
2022-03-24 05:44:24 -07:00
|
|
|
chain_data: Arc<RwLock<ChainData>>,
|
|
|
|
pnl_data: Arc<RwLock<PnlData>>,
|
2022-10-08 10:13:26 -07:00
|
|
|
metrics_pnls_tracked: MetricU64,
|
2022-03-24 05:44:24 -07:00
|
|
|
) {
|
|
|
|
let program_pk = Pubkey::from_str(&config.mango_program).unwrap();
|
|
|
|
let group_pk = Pubkey::from_str(&config.mango_group).unwrap();
|
2022-03-23 09:25:03 -07:00
|
|
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
loop {
|
2022-03-24 05:44:24 -07:00
|
|
|
tokio::time::sleep(std::time::Duration::from_millis(
|
|
|
|
config.update_interval_millis,
|
|
|
|
))
|
|
|
|
.await;
|
2022-03-23 09:25:03 -07:00
|
|
|
|
2022-03-23 10:39:42 -07:00
|
|
|
let snapshot = chain_data.read().unwrap().accounts_snapshot();
|
2022-03-23 09:25:03 -07:00
|
|
|
|
|
|
|
// get the group and cache now
|
|
|
|
let group = snapshot.get(&group_pk);
|
2022-11-20 19:47:20 -08:00
|
|
|
if group.is_none() {
|
2022-03-23 09:25:03 -07:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut pnls = Vec::with_capacity(snapshot.len());
|
|
|
|
for (pubkey, account) in snapshot.iter() {
|
|
|
|
let owner = account.account.owner();
|
|
|
|
let data = account.account.data();
|
2022-11-20 19:47:20 -08:00
|
|
|
|
2022-03-23 09:25:03 -07:00
|
|
|
if data.len() != size_of::<MangoAccount>()
|
2022-11-20 19:47:20 -08:00
|
|
|
|| data[0..8] != MangoAccount::discriminator()
|
2022-03-23 09:25:03 -07:00
|
|
|
|| owner != &program_pk
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-11-20 19:47:20 -08:00
|
|
|
let mango_account = MangoAccountValue::from_bytes(&data[8..]).unwrap();
|
|
|
|
if mango_account.fixed.group != group_pk {
|
2022-03-23 09:25:03 -07:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-11-20 19:47:20 -08:00
|
|
|
let pnl_vals =
|
2023-01-03 16:04:35 -08:00
|
|
|
compute_pnl(context.clone(), account_fetcher.clone(), &mango_account).await.unwrap();
|
2022-03-23 10:39:42 -07:00
|
|
|
|
|
|
|
// Alternatively, we could prepare the sorted and limited lists for each
|
|
|
|
// market here. That would be faster and cause less contention on the pnl_data
|
|
|
|
// lock, but it looks like it's very far from being an issue.
|
2022-03-23 09:25:03 -07:00
|
|
|
pnls.push((pubkey.clone(), pnl_vals));
|
|
|
|
}
|
|
|
|
|
2022-03-23 10:39:42 -07:00
|
|
|
*pnl_data.write().unwrap() = pnls;
|
2022-11-16 06:57:50 -08:00
|
|
|
metrics_pnls_tracked
|
|
|
|
.clone()
|
|
|
|
.set(pnl_data.read().unwrap().len() as u64)
|
2022-03-23 09:25:03 -07:00
|
|
|
}
|
|
|
|
});
|
2022-03-23 10:39:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
|
|
struct UnsettledPnlRankedRequest {
|
|
|
|
market_index: u8,
|
|
|
|
limit: u8,
|
|
|
|
order: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
struct PnlResponseItem {
|
|
|
|
pnl: f64,
|
|
|
|
pubkey: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
use jsonrpsee::http_server::HttpServerHandle;
|
2022-03-24 05:44:24 -07:00
|
|
|
fn start_jsonrpc_server(
|
|
|
|
config: JsonRpcConfig,
|
|
|
|
pnl_data: Arc<RwLock<PnlData>>,
|
2022-10-08 10:13:26 -07:00
|
|
|
metrics_reqs: MetricU64,
|
|
|
|
metrics_invalid_reqs: MetricU64,
|
2022-03-24 05:44:24 -07:00
|
|
|
) -> anyhow::Result<HttpServerHandle> {
|
2022-03-23 10:39:42 -07:00
|
|
|
use jsonrpsee::core::Error;
|
|
|
|
use jsonrpsee::http_server::{HttpServerBuilder, RpcModule};
|
|
|
|
use jsonrpsee::types::error::CallError;
|
|
|
|
use std::net::SocketAddr;
|
|
|
|
|
2022-03-24 05:44:24 -07:00
|
|
|
let server = HttpServerBuilder::default().build(config.bind_address.parse::<SocketAddr>()?)?;
|
2022-03-23 10:39:42 -07:00
|
|
|
let mut module = RpcModule::new(());
|
|
|
|
module.register_method("unsettledPnlRanked", move |params, _| {
|
|
|
|
let req = params.parse::<UnsettledPnlRankedRequest>()?;
|
2022-10-08 10:13:26 -07:00
|
|
|
metrics_reqs.clone().increment();
|
2022-03-24 05:44:24 -07:00
|
|
|
let invalid =
|
|
|
|
|s: &'static str| Err(Error::Call(CallError::InvalidParams(anyhow::anyhow!(s))));
|
2022-03-23 10:39:42 -07:00
|
|
|
let limit = req.limit as usize;
|
|
|
|
if limit > 20 {
|
2022-10-08 10:13:26 -07:00
|
|
|
metrics_invalid_reqs.clone().increment();
|
2022-03-23 10:39:42 -07:00
|
|
|
return invalid("'limit' must be <= 20");
|
|
|
|
}
|
2022-11-20 19:47:20 -08:00
|
|
|
let market_index = req.market_index as u16;
|
|
|
|
// if market_index >= MAX_PAIRS {
|
|
|
|
// metrics_invalid_reqs.clone().increment();
|
|
|
|
// return invalid("'market_index' must be < MAX_PAIRS");
|
|
|
|
// }
|
2022-03-23 10:39:42 -07:00
|
|
|
if req.order != "ASC" && req.order != "DESC" {
|
2022-10-08 10:13:26 -07:00
|
|
|
metrics_invalid_reqs.clone().increment();
|
2022-03-23 10:39:42 -07:00
|
|
|
return invalid("'order' must be ASC or DESC");
|
|
|
|
}
|
|
|
|
|
|
|
|
// write lock, because we sort in-place...
|
|
|
|
let mut pnls = pnl_data.write().unwrap();
|
|
|
|
if req.order == "ASC" {
|
2022-11-20 19:47:20 -08:00
|
|
|
pnls.sort_unstable_by(|a, b| {
|
|
|
|
a.1.iter()
|
|
|
|
.find(|x| x.0 == market_index)
|
|
|
|
.cmp(&b.1.iter().find(|x| x.0 == market_index))
|
|
|
|
});
|
2022-03-23 10:39:42 -07:00
|
|
|
} else {
|
2022-11-20 19:47:20 -08:00
|
|
|
pnls.sort_unstable_by(|a, b| {
|
|
|
|
b.1.iter()
|
|
|
|
.find(|x| x.0 == market_index)
|
|
|
|
.cmp(&a.1.iter().find(|x| x.0 == market_index))
|
|
|
|
});
|
2022-03-23 10:39:42 -07:00
|
|
|
}
|
|
|
|
let response = pnls
|
|
|
|
.iter()
|
|
|
|
.take(limit)
|
|
|
|
.map(|p| PnlResponseItem {
|
2022-11-20 19:47:20 -08:00
|
|
|
pnl: p
|
|
|
|
.1
|
|
|
|
.iter()
|
|
|
|
.find(|x| x.0 == market_index)
|
|
|
|
.unwrap()
|
|
|
|
.1
|
|
|
|
.to_num::<f64>(),
|
2022-03-23 10:39:42 -07:00
|
|
|
pubkey: p.0.to_string(),
|
|
|
|
})
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
Ok(response)
|
|
|
|
})?;
|
|
|
|
|
|
|
|
Ok(server.start(module)?)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::main]
|
|
|
|
async fn main() -> anyhow::Result<()> {
|
|
|
|
let args: Vec<String> = std::env::args().collect();
|
|
|
|
if args.len() < 2 {
|
|
|
|
println!("requires a config file argument");
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
let config: Config = {
|
|
|
|
let mut file = File::open(&args[1])?;
|
|
|
|
let mut contents = String::new();
|
|
|
|
file.read_to_string(&mut contents)?;
|
|
|
|
toml::from_str(&contents).unwrap()
|
|
|
|
};
|
|
|
|
|
|
|
|
solana_logger::setup_with_default("info");
|
|
|
|
info!("startup");
|
|
|
|
|
2022-11-20 19:47:20 -08:00
|
|
|
let rpc_url = config.snapshot_source.rpc_http_url;
|
|
|
|
let ws_url = rpc_url.replace("https", "wss");
|
|
|
|
let rpc_timeout = Duration::from_secs(10);
|
|
|
|
let cluster = Cluster::Custom(rpc_url.clone(), ws_url.clone());
|
|
|
|
let commitment = CommitmentConfig::processed();
|
|
|
|
let client = Client::new(
|
|
|
|
cluster.clone(),
|
|
|
|
commitment,
|
|
|
|
&Keypair::new(),
|
|
|
|
Some(rpc_timeout),
|
|
|
|
);
|
|
|
|
let group_context = Arc::new(MangoGroupContext::new_from_rpc(
|
2023-01-01 08:42:36 -08:00
|
|
|
&client.rpc_async(),
|
2022-11-20 19:47:20 -08:00
|
|
|
Pubkey::from_str(&config.pnl.mango_group).unwrap(),
|
2023-01-01 08:42:36 -08:00
|
|
|
).await?);
|
2022-11-20 19:47:20 -08:00
|
|
|
let chain_data = Arc::new(RwLock::new(chain_data::ChainData::new()));
|
|
|
|
let account_fetcher = Arc::new(chain_data::AccountFetcher {
|
|
|
|
chain_data: chain_data.clone(),
|
2023-01-01 08:42:36 -08:00
|
|
|
rpc: client.rpc_async(),
|
2022-11-20 19:47:20 -08:00
|
|
|
});
|
|
|
|
|
2022-10-08 04:57:47 -07:00
|
|
|
let metrics_tx = metrics::start(config.metrics, "pnl".into());
|
2022-03-23 10:39:42 -07:00
|
|
|
|
2022-10-08 10:13:26 -07:00
|
|
|
let metrics_reqs =
|
|
|
|
metrics_tx.register_u64("pnl_jsonrpc_reqs_total".into(), MetricType::Counter);
|
|
|
|
let metrics_invalid_reqs =
|
2022-11-16 06:57:50 -08:00
|
|
|
metrics_tx.register_u64("pnl_jsonrpc_reqs_invalid_total".into(), MetricType::Counter);
|
|
|
|
let metrics_pnls_tracked = metrics_tx.register_u64("pnl_num_tracked".into(), MetricType::Gauge);
|
2022-10-08 10:13:26 -07:00
|
|
|
|
2022-03-23 10:39:42 -07:00
|
|
|
let chain_data = Arc::new(RwLock::new(ChainData::new()));
|
|
|
|
let pnl_data = Arc::new(RwLock::new(PnlData::new()));
|
|
|
|
|
2022-11-16 06:57:50 -08:00
|
|
|
start_pnl_updater(
|
|
|
|
config.pnl.clone(),
|
2022-11-20 19:47:20 -08:00
|
|
|
group_context.clone(),
|
|
|
|
account_fetcher.clone(),
|
2022-11-16 06:57:50 -08:00
|
|
|
chain_data.clone(),
|
|
|
|
pnl_data.clone(),
|
|
|
|
metrics_pnls_tracked,
|
|
|
|
);
|
2022-03-23 10:39:42 -07:00
|
|
|
|
|
|
|
// dropping the handle would exit the server
|
2022-11-16 06:57:50 -08:00
|
|
|
let _http_server_handle = start_jsonrpc_server(
|
|
|
|
config.jsonrpc_server.clone(),
|
|
|
|
pnl_data,
|
|
|
|
metrics_reqs,
|
|
|
|
metrics_invalid_reqs,
|
|
|
|
)?;
|
2022-03-23 09:25:03 -07:00
|
|
|
|
2022-03-23 10:39:42 -07:00
|
|
|
// start filling chain_data from the grpc plugin source
|
|
|
|
let (account_write_queue_sender, slot_queue_sender) = memory_target::init(chain_data).await?;
|
2022-12-24 04:49:17 -08:00
|
|
|
let filter_config = FilterConfig {
|
|
|
|
program_ids: vec![
|
|
|
|
"4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg".into(),
|
|
|
|
],
|
|
|
|
};
|
2022-03-23 01:07:00 -07:00
|
|
|
grpc_plugin_source::process_events(
|
|
|
|
&config.source,
|
2022-12-24 04:49:17 -08:00
|
|
|
&filter_config,
|
2022-03-23 01:07:00 -07:00
|
|
|
account_write_queue_sender,
|
|
|
|
slot_queue_sender,
|
2022-12-24 04:49:17 -08:00
|
|
|
metrics_tx.clone(),
|
2022-03-23 01:07:00 -07:00
|
|
|
)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|