|
|
|
@ -1,10 +1,12 @@ |
|
|
|
|
use chrono::{NaiveDate, NaiveDateTime, TimeDelta, Utc}; |
|
|
|
|
use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeDelta, Utc}; |
|
|
|
|
use config::get_config; |
|
|
|
|
use error::AppResult; |
|
|
|
|
use futures_util::StreamExt; |
|
|
|
|
use models::{Candle, CandleInterval, Pair}; |
|
|
|
|
use models::{Candle, CandleInterval, PoloniuxCandle, TradeDirection}; |
|
|
|
|
use poloniex::PoloniexClient; |
|
|
|
|
use repo::Repo; |
|
|
|
|
|
|
|
|
|
mod config; |
|
|
|
|
mod error; |
|
|
|
|
mod models; |
|
|
|
|
mod poloniex; |
|
|
|
@ -12,10 +14,10 @@ mod repo; |
|
|
|
|
|
|
|
|
|
async fn fetch_candles_until_now( |
|
|
|
|
poloniex_client: &PoloniexClient, |
|
|
|
|
pair: &Pair, |
|
|
|
|
pair: &str, |
|
|
|
|
interval: CandleInterval, |
|
|
|
|
mut start_time: NaiveDateTime, |
|
|
|
|
) -> AppResult<Vec<Candle>> { |
|
|
|
|
) -> AppResult<Vec<PoloniuxCandle>> { |
|
|
|
|
let mut result = vec![]; |
|
|
|
|
|
|
|
|
|
loop { |
|
|
|
@ -51,28 +53,35 @@ async fn fetch_candles_until_now( |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async fn _main() -> AppResult<()> { |
|
|
|
|
let poloniex_client = PoloniexClient::new( |
|
|
|
|
"https://api.poloniex.com", |
|
|
|
|
"wss://ws.poloniex.com/ws/public", |
|
|
|
|
)?; |
|
|
|
|
let repo = Repo::new_init("poloniex_data.db")?; |
|
|
|
|
dotenv::dotenv().ok(); |
|
|
|
|
|
|
|
|
|
let config = get_config(); |
|
|
|
|
let poloniex_client = PoloniexClient::new(&config.poloniex_rest_url, &config.poloniex_ws_url); |
|
|
|
|
let repo = Repo::new_init(config.db_name)?; |
|
|
|
|
|
|
|
|
|
let start_time = NaiveDate::from_ymd_opt(2024, 12, 1) |
|
|
|
|
.unwrap() |
|
|
|
|
.and_hms_opt(0, 0, 0) |
|
|
|
|
.unwrap(); |
|
|
|
|
|
|
|
|
|
let pair = Pair::new("BTC", "USDT"); |
|
|
|
|
let pair = config.pair.clone(); |
|
|
|
|
let candles = |
|
|
|
|
fetch_candles_until_now(&poloniex_client, &pair, CandleInterval::M1, start_time).await?; |
|
|
|
|
fetch_candles_until_now(&poloniex_client, &pair, config.interval, start_time).await?; |
|
|
|
|
|
|
|
|
|
println!("fetched {} candles", candles.len()); |
|
|
|
|
println!( |
|
|
|
|
"{pair}: fetched {} candles with interval {}", |
|
|
|
|
candles.len(), |
|
|
|
|
config.interval.as_ref() |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// нельзя так делать, нужно использовать транзакцию
|
|
|
|
|
// и батч-вставку для уменьшения количества обращений к бд,
|
|
|
|
|
// но в контексте тестового и так сойдёт
|
|
|
|
|
for candle in candles { |
|
|
|
|
repo.insert_candle(&candle)?; |
|
|
|
|
repo.upsert_candle(&Candle { |
|
|
|
|
candle, |
|
|
|
|
pair: pair.to_string(), |
|
|
|
|
})?; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let mut trades = poloniex_client.recent_trades_stream(&pair).await?; |
|
|
|
@ -80,9 +89,75 @@ async fn _main() -> AppResult<()> { |
|
|
|
|
while let Some(t) = trades.next().await { |
|
|
|
|
println!("{t:?}"); |
|
|
|
|
|
|
|
|
|
if let Ok(trade) = t { |
|
|
|
|
repo.insert_trade(&trade)?; |
|
|
|
|
let Ok(trade) = t else { break }; |
|
|
|
|
let mut last_candle = repo.get_latest_candle_from_interval(&pair, config.interval)?; |
|
|
|
|
let interval_delta = match last_candle.candle.interval { |
|
|
|
|
CandleInterval::M1 => TimeDelta::minutes(1), |
|
|
|
|
CandleInterval::M15 => TimeDelta::minutes(15), |
|
|
|
|
CandleInterval::H1 => TimeDelta::hours(1), |
|
|
|
|
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 = Candle { |
|
|
|
|
candle: PoloniuxCandle { |
|
|
|
|
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: config.interval, |
|
|
|
|
start_time: new_candle_ts, |
|
|
|
|
close_time: NaiveDateTime::UNIX_EPOCH, |
|
|
|
|
}, |
|
|
|
|
pair: trade.symbol.clone(), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
repo.upsert_candle(&new_candle)?; |
|
|
|
|
} else { |
|
|
|
|
last_candle.candle.low = last_candle.candle.low.min(trade.price); |
|
|
|
|
last_candle.candle.high = last_candle.candle.high.max(trade.price); |
|
|
|
|
last_candle.candle.close = trade.price; |
|
|
|
|
last_candle.candle.amount += trade.amount; |
|
|
|
|
last_candle.candle.quantity += trade.quantity; |
|
|
|
|
last_candle.candle.trade_count += 1; |
|
|
|
|
last_candle.candle.ts = trade.ts; |
|
|
|
|
last_candle.candle.weighted_average = |
|
|
|
|
last_candle.candle.amount / last_candle.candle.quantity; |
|
|
|
|
last_candle.candle.interval = config.interval; |
|
|
|
|
last_candle.candle.close_time = trade.ts; |
|
|
|
|
|
|
|
|
|
if is_buy { |
|
|
|
|
last_candle.candle.buy_taker_amount += trade.amount; |
|
|
|
|
last_candle.candle.buy_taker_quantity += trade.quantity; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
repo.upsert_candle(&last_candle)?; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
repo.insert_trade(&trade)?; |
|
|
|
|
} |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
|