|
|
|
@ -5,7 +5,7 @@ use config::get_config; |
|
|
|
|
use error::AppResult; |
|
|
|
|
use futures_util::{future::try_join_all, StreamExt}; |
|
|
|
|
use markets::{poloniex::PoloniexClient, Market}; |
|
|
|
|
use models::{Candle, CandleExtended, CandleInterval, TradeDirection}; |
|
|
|
|
use models::{Candle, CandleExtended, CandleInterval, Trade, TradeDirection}; |
|
|
|
|
use repos::{sqlite::SqliteRepo, Repo}; |
|
|
|
|
|
|
|
|
|
mod config; |
|
|
|
@ -27,7 +27,11 @@ async fn fetch_candles_until_now( |
|
|
|
|
let limit = 500; |
|
|
|
|
|
|
|
|
|
loop { |
|
|
|
|
println!("pulling candles from {start_time}"); |
|
|
|
|
println!( |
|
|
|
|
"pulling {}:{} candles from {start_time}", |
|
|
|
|
pair, |
|
|
|
|
interval.as_ref() |
|
|
|
|
); |
|
|
|
|
let candles = market_client |
|
|
|
|
.get_historical_candles(&pair, interval, start_time, Utc::now().naive_utc(), limit) |
|
|
|
|
.await?; |
|
|
|
@ -64,19 +68,56 @@ async fn fetch_candles_until_now( |
|
|
|
|
Ok((result, pair.to_string())) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async fn trades_processor( |
|
|
|
|
async fn calculate_new_candles( |
|
|
|
|
repo: Arc<impl Repo>, |
|
|
|
|
market_client: Arc<impl Market>, |
|
|
|
|
pairs: &[String], |
|
|
|
|
interval: CandleInterval, |
|
|
|
|
trade: Trade, |
|
|
|
|
) -> AppResult<()> { |
|
|
|
|
let mut trades = market_client.recent_trades_stream(&pairs).await?; |
|
|
|
|
let is_buy = matches!(trade.taker_side, TradeDirection::Buy); |
|
|
|
|
let insert_new_candle = || async { |
|
|
|
|
let interval_secs = match interval { |
|
|
|
|
CandleInterval::M1 => 60, |
|
|
|
|
CandleInterval::M15 => 60 * 15, |
|
|
|
|
CandleInterval::H1 => 60 * 60, |
|
|
|
|
CandleInterval::D1 => 60 * 60 * 24, |
|
|
|
|
}; |
|
|
|
|
let new_candle_ts = DateTime::from_timestamp( |
|
|
|
|
(trade.ts.and_utc().timestamp() / interval_secs) * interval_secs, |
|
|
|
|
0, |
|
|
|
|
) |
|
|
|
|
.unwrap() |
|
|
|
|
.naive_utc(); |
|
|
|
|
|
|
|
|
|
let new_candle = CandleExtended { |
|
|
|
|
candle: Candle { |
|
|
|
|
low: trade.price, |
|
|
|
|
high: trade.price, |
|
|
|
|
open: trade.price, |
|
|
|
|
close: trade.price, |
|
|
|
|
amount: trade.amount, |
|
|
|
|
quantity: trade.quantity, |
|
|
|
|
buy_taker_amount: if is_buy { trade.amount } else { 0.0 }, |
|
|
|
|
buy_taker_quantity: if is_buy { trade.quantity } else { 0.0 }, |
|
|
|
|
trade_count: 1, |
|
|
|
|
ts: trade.ts, |
|
|
|
|
weighted_average: trade.amount / trade.quantity, |
|
|
|
|
interval, |
|
|
|
|
start_time: new_candle_ts, |
|
|
|
|
close_time: NaiveDateTime::UNIX_EPOCH, |
|
|
|
|
}, |
|
|
|
|
pair: trade.symbol.clone(), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
while let Some(t) = trades.next().await { |
|
|
|
|
println!("{t:?}"); |
|
|
|
|
repo.upsert_candles(&[new_candle]).await?; |
|
|
|
|
|
|
|
|
|
let Ok(trade) = t else { break }; |
|
|
|
|
let mut last_candle = repo.get_latest_candle_from_interval(&trade.symbol, interval)?; |
|
|
|
|
AppResult::Ok(()) |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let last_candle = repo |
|
|
|
|
.get_latest_candle_from_interval(&trade.symbol, interval) |
|
|
|
|
.await?; |
|
|
|
|
|
|
|
|
|
if let Some(mut last_candle) = last_candle { |
|
|
|
|
let interval_delta = match last_candle.candle.interval { |
|
|
|
|
CandleInterval::M1 => TimeDelta::minutes(1), |
|
|
|
|
CandleInterval::M15 => TimeDelta::minutes(15), |
|
|
|
@ -84,44 +125,9 @@ async fn trades_processor( |
|
|
|
|
CandleInterval::D1 => TimeDelta::days(1), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let is_buy = matches!(trade.taker_side, TradeDirection::Buy); |
|
|
|
|
|
|
|
|
|
// если трейд не входит в интервал последней свечи, то создаём новую свечу, иначе обновляем предыдущую
|
|
|
|
|
if trade.ts > (last_candle.candle.ts + interval_delta) { |
|
|
|
|
let interval_secs = match last_candle.candle.interval { |
|
|
|
|
CandleInterval::M1 => 60, |
|
|
|
|
CandleInterval::M15 => 60 * 15, |
|
|
|
|
CandleInterval::H1 => 60 * 60, |
|
|
|
|
CandleInterval::D1 => 60 * 60 * 24, |
|
|
|
|
}; |
|
|
|
|
let new_candle_ts = DateTime::from_timestamp( |
|
|
|
|
(trade.ts.and_utc().timestamp() / interval_secs) * interval_secs, |
|
|
|
|
0, |
|
|
|
|
) |
|
|
|
|
.unwrap() |
|
|
|
|
.naive_utc(); |
|
|
|
|
|
|
|
|
|
let new_candle = CandleExtended { |
|
|
|
|
candle: Candle { |
|
|
|
|
low: trade.price, |
|
|
|
|
high: trade.price, |
|
|
|
|
open: trade.price, |
|
|
|
|
close: trade.price, |
|
|
|
|
amount: trade.amount, |
|
|
|
|
quantity: trade.quantity, |
|
|
|
|
buy_taker_amount: if is_buy { trade.amount } else { 0.0 }, |
|
|
|
|
buy_taker_quantity: if is_buy { trade.quantity } else { 0.0 }, |
|
|
|
|
trade_count: 1, |
|
|
|
|
ts: trade.ts, |
|
|
|
|
weighted_average: trade.amount / trade.quantity, |
|
|
|
|
interval, |
|
|
|
|
start_time: new_candle_ts, |
|
|
|
|
close_time: NaiveDateTime::UNIX_EPOCH, |
|
|
|
|
}, |
|
|
|
|
pair: trade.symbol.clone(), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
repo.upsert_candle(&new_candle)?; |
|
|
|
|
insert_new_candle().await?; |
|
|
|
|
} else { |
|
|
|
|
last_candle.candle.low = last_candle.candle.low.min(trade.price); |
|
|
|
|
last_candle.candle.high = last_candle.candle.high.max(trade.price); |
|
|
|
@ -140,12 +146,14 @@ async fn trades_processor( |
|
|
|
|
last_candle.candle.buy_taker_quantity += trade.quantity; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
repo.upsert_candle(&last_candle)?; |
|
|
|
|
repo.upsert_candles(&[last_candle]).await?; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
repo.insert_trade(&trade)?; |
|
|
|
|
} else { |
|
|
|
|
insert_new_candle().await?; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
repo.insert_trade(&trade).await?; |
|
|
|
|
|
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -157,9 +165,8 @@ async fn _main() -> AppResult<()> { |
|
|
|
|
&config.poloniex_rest_url, |
|
|
|
|
&config.poloniex_ws_url, |
|
|
|
|
)); |
|
|
|
|
let repo = Arc::new(SqliteRepo::new_init(config.db_name)?); |
|
|
|
|
|
|
|
|
|
let start_time = NaiveDate::from_ymd_opt(2024, 12, 1) |
|
|
|
|
let repo = Arc::new(SqliteRepo::new(&config.database_url).await?); |
|
|
|
|
let base_start_time = NaiveDate::from_ymd_opt(2024, 12, 1) |
|
|
|
|
.unwrap() |
|
|
|
|
.and_hms_opt(0, 0, 0) |
|
|
|
|
.unwrap(); |
|
|
|
@ -168,6 +175,17 @@ async fn _main() -> AppResult<()> { |
|
|
|
|
|
|
|
|
|
for pair in &config.pairs { |
|
|
|
|
for interval in &config.intervals { |
|
|
|
|
let start_time = { |
|
|
|
|
let last_candle = repo |
|
|
|
|
.get_latest_candle_from_interval(&pair, *interval) |
|
|
|
|
.await?; |
|
|
|
|
|
|
|
|
|
match last_candle { |
|
|
|
|
Some(c) => c.candle.close_time + TimeDelta::seconds(1), |
|
|
|
|
None => base_start_time, |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let fetcher = fetch_candles_until_now( |
|
|
|
|
poloniex_client.clone(), |
|
|
|
|
pair.to_string(), |
|
|
|
@ -185,31 +203,41 @@ async fn _main() -> AppResult<()> { |
|
|
|
|
// config.interval.as_ref()
|
|
|
|
|
// );
|
|
|
|
|
|
|
|
|
|
// нельзя так делать, нужно использовать транзакцию
|
|
|
|
|
// и батч-вставку для уменьшения количества обращений к бд,
|
|
|
|
|
// но в контексте тестового и так сойдёт
|
|
|
|
|
for (candles, pair) in fetched_candles { |
|
|
|
|
for candle in candles { |
|
|
|
|
repo.upsert_candle(&CandleExtended { |
|
|
|
|
candle, |
|
|
|
|
pair: pair.clone(), |
|
|
|
|
})?; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
let candles_to_upsert = fetched_candles |
|
|
|
|
.into_iter() |
|
|
|
|
.flat_map(|(candles, pair)| { |
|
|
|
|
candles |
|
|
|
|
.into_iter() |
|
|
|
|
.map(|candle| CandleExtended { |
|
|
|
|
candle, |
|
|
|
|
pair: pair.clone(), |
|
|
|
|
}) |
|
|
|
|
.collect::<Vec<_>>() |
|
|
|
|
}) |
|
|
|
|
.collect::<Vec<CandleExtended>>(); |
|
|
|
|
|
|
|
|
|
repo.upsert_candles(&candles_to_upsert).await?; |
|
|
|
|
|
|
|
|
|
for interval in &config.intervals { |
|
|
|
|
tokio::spawn({ |
|
|
|
|
let poloniex_client = poloniex_client.clone(); |
|
|
|
|
let repo = repo.clone(); |
|
|
|
|
let pairs = config.pairs.clone(); |
|
|
|
|
let interval = *interval; |
|
|
|
|
async move { |
|
|
|
|
let result = trades_processor(repo, poloniex_client, &pairs, interval).await; |
|
|
|
|
if let Err(e) = result { |
|
|
|
|
eprintln!("processor stopped with error: {e}") |
|
|
|
|
let mut trades = poloniex_client.recent_trades_stream(&config.pairs).await?; |
|
|
|
|
|
|
|
|
|
while let Some(t) = trades.next().await { |
|
|
|
|
println!("{t:?}"); |
|
|
|
|
|
|
|
|
|
let Ok(trade) = t else { break }; |
|
|
|
|
|
|
|
|
|
for interval in &config.intervals { |
|
|
|
|
tokio::spawn({ |
|
|
|
|
let repo = repo.clone(); |
|
|
|
|
let interval = *interval; |
|
|
|
|
let trade = trade.clone(); |
|
|
|
|
async move { |
|
|
|
|
let result = calculate_new_candles(repo, interval, trade).await; |
|
|
|
|
if let Err(e) = result { |
|
|
|
|
eprintln!("processor stopped with error: {e:?}") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
@ -217,6 +245,6 @@ async fn _main() -> AppResult<()> { |
|
|
|
|
#[tokio::main] |
|
|
|
|
async fn main() { |
|
|
|
|
if let Err(e) = _main().await { |
|
|
|
|
eprintln!("{e}"); |
|
|
|
|
eprintln!("{e:?}"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|