|
|
@@ -0,0 +1,420 @@
|
|
|
+use std::collections::{BTreeMap, HashSet};
|
|
|
+use std::fs::File;
|
|
|
+use std::io::Write;
|
|
|
+use std::str::FromStr;
|
|
|
+use chrono::{NaiveDateTime, Utc};
|
|
|
+use handlebars::Handlebars;
|
|
|
+use rust_decimal::Decimal;
|
|
|
+use rust_decimal::prelude::ToPrimitive;
|
|
|
+use serde_json::{json, Value};
|
|
|
+use tracing::{error, info};
|
|
|
+use crate::export_template::template_ticker::ExportExchangeTickerInfo;
|
|
|
+use crate::handle_ticker;
|
|
|
+use crate::http::request::get;
|
|
|
+use crate::struct_standard::Trades;
|
|
|
+use crate::swap_okx::okx_swap_standard::SwapTrades;
|
|
|
+use crate::utils::utils::TickerConfigInfo;
|
|
|
+
|
|
|
+pub async fn export_ticker(symbol: &str, exchange: &str, time_str: &str) {
|
|
|
+
|
|
|
+ //从今天算起
|
|
|
+ let time_this = Utc::now().timestamp_millis();
|
|
|
+ let mut name_html = "";
|
|
|
+ let mut start_at = time_this - match time_str {
|
|
|
+ "10M" => {
|
|
|
+ name_html = "10分钟";
|
|
|
+ (1000 * 60 * 10)
|
|
|
+ }
|
|
|
+ "20M" => {
|
|
|
+ name_html = "20分钟";
|
|
|
+ (1000 * 60 * 20)
|
|
|
+ }
|
|
|
+ "30M" => {
|
|
|
+ name_html = "30分钟";
|
|
|
+ (1000 * 60 * 30)
|
|
|
+ }
|
|
|
+ "1H" => {
|
|
|
+ name_html = "1小时";
|
|
|
+ (1000 * 60 * 60 * 1)
|
|
|
+ }
|
|
|
+ "10H" => {
|
|
|
+ name_html = "10小时";
|
|
|
+ (1000 * 60 * 60 * 10)
|
|
|
+ }
|
|
|
+ "1D" => {
|
|
|
+ name_html = "1天";
|
|
|
+ (1000 * 60 * 60 * 24)
|
|
|
+ }
|
|
|
+ "2D" => {
|
|
|
+ name_html = "1天";
|
|
|
+ (1000 * 60 * 60 * 24 * 2)
|
|
|
+ }
|
|
|
+ "3D" => {
|
|
|
+ name_html = "3天";
|
|
|
+ (1000 * 60 * 60 * 24 * 3)
|
|
|
+ }
|
|
|
+ "5D" => {
|
|
|
+ name_html = "5天";
|
|
|
+ (1000 * 60 * 60 * 24 * 5)
|
|
|
+ }
|
|
|
+ "1Z" => {
|
|
|
+ name_html = "1周";
|
|
|
+ (1000 * 60 * 60 * 24 * 7)
|
|
|
+ }
|
|
|
+ "2Z" => {
|
|
|
+ name_html = "2周";
|
|
|
+ (1000 * 60 * 60 * 24 * 7 * 2)
|
|
|
+ }
|
|
|
+ "4Z" => {
|
|
|
+ name_html = "1个月";
|
|
|
+ (1000 * 60 * 60 * 24 * 7 * 4)
|
|
|
+ }
|
|
|
+ _ => {
|
|
|
+ info!("错误时间");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ start_at = start_at / 1000;
|
|
|
+ let end_at = time_this.clone() / 1000;
|
|
|
+ let recall_start_at = start_at;
|
|
|
+
|
|
|
+ let mut exchange_list: BTreeMap<String, Vec<Value>> = BTreeMap::new();
|
|
|
+ let exchange_up = exchange.to_uppercase();
|
|
|
+ let exchange_result = match exchange_up.as_str() {
|
|
|
+ "GATE" => {
|
|
|
+ let mut start_at_ti = start_at;
|
|
|
+ loop {
|
|
|
+ let end_at_ti = start_at_ti + (20 * 60);
|
|
|
+ if start_at_ti > end_at {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ let time_str_s = NaiveDateTime::from_timestamp_millis(((start_at_ti) + 8 * 3600) * 1000).unwrap().format("%Y-%m-%d %H:%M:%S%.3f").to_string();
|
|
|
+ let time_str_e = NaiveDateTime::from_timestamp_millis(((end_at_ti) + 8 * 3600) * 1000).unwrap().format("%Y-%m-%d %H:%M:%S%.3f").to_string();
|
|
|
+ info!("----时间:{:?}---{:?}",time_str_s,time_str_e);
|
|
|
+ //查询数据按照间隔20分钟
|
|
|
+ let url = "https://api.gateio.ws/api/v4/futures/usdt/trades";
|
|
|
+ let params = serde_json::json!({
|
|
|
+ "contract": symbol,
|
|
|
+ "from": start_at_ti,
|
|
|
+ "to": end_at_ti,
|
|
|
+ "to": end_at_ti,
|
|
|
+ "limit": "1000"
|
|
|
+ });
|
|
|
+ let res_data = get(url, params.to_string(), None).await;
|
|
|
+ if res_data.code == "200" {
|
|
|
+ let res_data_str = res_data.data;
|
|
|
+ // info!("body:{:?}",res_data_str);
|
|
|
+ let json_value: serde_json::Value = serde_json::from_str(&res_data_str).unwrap();
|
|
|
+ for rows in json_value.as_array() {
|
|
|
+ let rows_c = rows.clone();
|
|
|
+ info!("数据数量:{:?}",rows_c.len());
|
|
|
+ for row in rows_c {
|
|
|
+ // {"contract": String("XRP_USDT"), "create_time": Number(1703146260.4), "create_time_ms": Number(1703146260.4), "id": Number(24663517), "price": String("0.6163"), "size": Number(132)}
|
|
|
+ // info!("数据:{:?}",row.clone());
|
|
|
+ let row_c = row.clone();
|
|
|
+ let create_time = format!("{:?}", row["create_time"].as_f64().unwrap().to_i64().unwrap_or(0)).clone();
|
|
|
+ let k = create_time.as_str();
|
|
|
+
|
|
|
+ let mut def: Vec<Value> = vec![];
|
|
|
+ if exchange_list.contains_key(k) {
|
|
|
+ match exchange_list.get(k) {
|
|
|
+ None => {}
|
|
|
+ Some(r) => {
|
|
|
+ def = r.clone();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ def.push(row_c);
|
|
|
+ exchange_list.insert(k.parse().unwrap(), def);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ info!("数据请求错误:");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ start_at_ti = start_at_ti + (20 * 60);
|
|
|
+ }
|
|
|
+
|
|
|
+ // info!("数据:{:?}",exchange_list);
|
|
|
+ info!("----横向图--交易量:");
|
|
|
+ let mut exchange_list_sss: BTreeMap<String, Vec<Value>> = BTreeMap::new();
|
|
|
+ let mut turnover_data: BTreeMap<String, Vec<Value>> = BTreeMap::new();
|
|
|
+ let mut keys: Vec<String> = exchange_list.keys().cloned().collect();
|
|
|
+ keys.sort_by(|&(ref a), &(ref b)| a.len().cmp(&b.len()));
|
|
|
+ for k in keys.clone() {
|
|
|
+ let time_str = NaiveDateTime::from_timestamp_millis((k.clone().parse::<i64>().unwrap() + 8 * 3600) * 1000).unwrap().format("%Y-%m-%d %H:%M:%S%.3f").to_string();
|
|
|
+ let data = exchange_list.get(k.clone().as_str()).unwrap();
|
|
|
+ let mut str_array = vec![""];
|
|
|
+ for d in data.clone() {
|
|
|
+ str_array.push(" - ");
|
|
|
+ }
|
|
|
+ // info!("数据总数: {:?}: {:?}",time_str,str_array);
|
|
|
+
|
|
|
+
|
|
|
+ let mut def: Vec<Value> = vec![];
|
|
|
+ let size_k = format!("{:?}", data.len());
|
|
|
+ if exchange_list_sss.contains_key(&size_k) {
|
|
|
+ match exchange_list_sss.get(&size_k) {
|
|
|
+ None => {}
|
|
|
+ Some(r) => {
|
|
|
+ def = r.clone();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ def.extend(data.clone());
|
|
|
+ exchange_list_sss.insert((str_array.len() - 1).to_string(), def);
|
|
|
+
|
|
|
+
|
|
|
+ let mut def2: Vec<Value> = vec![];
|
|
|
+ let size_k2 = format!("{:?}", time_str);
|
|
|
+ if turnover_data.contains_key(&size_k2) {
|
|
|
+ match turnover_data.get(&size_k2) {
|
|
|
+ None => {}
|
|
|
+ Some(r) => {
|
|
|
+ def2 = r.clone();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ def2.extend(data.clone());
|
|
|
+ turnover_data.insert(String::from(&size_k2), def2);
|
|
|
+ }
|
|
|
+
|
|
|
+ //总结 交易量
|
|
|
+ info!("总结:");
|
|
|
+ let mut pancake_data_y: Vec<String> = vec![]; //{ value: 1048, name: 'Search Engine' },
|
|
|
+ let mut pancake_data_x: Vec<usize> = vec![]; //{ value: 1048, name: 'Search Engine' },
|
|
|
+ // let zzz = exchange_list_sss.keys();
|
|
|
+ let mut keys: Vec<String> = exchange_list_sss.keys().cloned().collect();
|
|
|
+ keys.sort_by(|&(ref a), &(ref b)| a.len().cmp(&b.len()));
|
|
|
+ for k in keys {
|
|
|
+ let data = exchange_list_sss.get(k.clone().as_str()).unwrap();
|
|
|
+
|
|
|
+ let mut initiative_ticker_info = vec![];
|
|
|
+ for d in data {
|
|
|
+ let create_time = format!("{:?}", d["create_time"].as_f64().unwrap().to_i64().unwrap_or(0)).clone();
|
|
|
+ let time_str = NaiveDateTime::from_timestamp_millis((create_time.clone().parse::<i64>().unwrap() + 8 * 3600) * 1000).unwrap().format("%Y-%m-%d %H:%M:%S%.3f").to_string();
|
|
|
+
|
|
|
+ initiative_ticker_info.push(time_str);
|
|
|
+ };
|
|
|
+ let mut seen = HashSet::new();
|
|
|
+ initiative_ticker_info.retain(|item| seen.insert(item.clone()));
|
|
|
+
|
|
|
+ let name = format!(" 交易量:{:?}", k.clone());
|
|
|
+ pancake_data_y.push(name);
|
|
|
+ pancake_data_x.push(initiative_ticker_info.len());
|
|
|
+ info!("交易量:{:?},一共有{:?}个时间点:{:?}",k.clone(),initiative_ticker_info.len(),0);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ //柱形图
|
|
|
+ let mut turnover_data_y: Vec<f64> = vec![];
|
|
|
+ let mut turnover_data_x: Vec<String> = vec![];
|
|
|
+ let mut keys: Vec<String> = turnover_data.keys().cloned().collect();
|
|
|
+ keys.sort_by(|&(ref a), &(ref b)| a.len().cmp(&b.len()));
|
|
|
+ turnover_data_x = keys;
|
|
|
+ for k in turnover_data_x.clone() {
|
|
|
+ let rows = turnover_data.get(k.as_str()).unwrap();
|
|
|
+ let mut val_y = 0.0;
|
|
|
+ if rows.len() > 1 {
|
|
|
+ let mut max_v = 0.0;
|
|
|
+ let mut min_v = 0.0;
|
|
|
+ for row in rows {
|
|
|
+ let price = row["price"].as_str().unwrap().parse::<f64>().unwrap();
|
|
|
+ // info!("price:{:?} " ,price);
|
|
|
+ if max_v == 0.0{
|
|
|
+ max_v = price
|
|
|
+ }
|
|
|
+ if min_v == 0.0{
|
|
|
+ min_v = price
|
|
|
+ }
|
|
|
+
|
|
|
+ max_v = if price > max_v { price } else { max_v };
|
|
|
+ min_v = if price < min_v { price } else { min_v };
|
|
|
+ }
|
|
|
+ val_y = format!("{:.5}", (max_v - min_v) / max_v).to_string().parse::<f64>().unwrap();
|
|
|
+ }
|
|
|
+ turnover_data_y.push(val_y);
|
|
|
+ }
|
|
|
+
|
|
|
+ export_html(name_html, pancake_data_y, pancake_data_x, turnover_data_y, turnover_data_x)
|
|
|
+ }
|
|
|
+ _ => {
|
|
|
+ error!("交易所输入错误!");
|
|
|
+ panic!("交易所输入错误!")
|
|
|
+ }
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+//生成html
|
|
|
+pub fn export_html(name_html: &str, pancake_data_y: Vec<String>, pancake_data_x: Vec<usize>, turnover_data_y: Vec<f64>, turnover_data_x: Vec<String>) {
|
|
|
+ info!("正在生成网页,请稍后!");
|
|
|
+ let path = format!("./数据分析图.html");
|
|
|
+ // 创建 Handlebars 实例
|
|
|
+ let mut handlebars = Handlebars::new();
|
|
|
+
|
|
|
+
|
|
|
+ let json_data = json!({
|
|
|
+ "pancake_data_y":pancake_data_y,
|
|
|
+ "pancake_data_x":pancake_data_x,
|
|
|
+ "turnover_data_y":turnover_data_y,
|
|
|
+ "turnover_data_x":turnover_data_x,
|
|
|
+ });
|
|
|
+
|
|
|
+ info!("pancake_data:{:?}",json_data["pancake_data"].to_string());
|
|
|
+
|
|
|
+ let data = serde_json::json!({
|
|
|
+ "chart_title": format!("Ticker数据分析"),
|
|
|
+ "name_html": name_html.to_string().clone(),
|
|
|
+ });
|
|
|
+ // HTML 模板
|
|
|
+ let template = r#"
|
|
|
+ <!DOCTYPE html>
|
|
|
+ <html>
|
|
|
+ <head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <title>{{chart_title}}</title>
|
|
|
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
|
|
|
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.10/dayjs.min.js"></script>
|
|
|
+
|
|
|
+ <style>
|
|
|
+ * {
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ }
|
|
|
+ #main {
|
|
|
+ margin: 50px auto 0;
|
|
|
+ width: calc(100vw - 100px);
|
|
|
+ height: calc(100vh - 100px);
|
|
|
+
|
|
|
+ }
|
|
|
+ #main2 {
|
|
|
+ margin: 50px auto 0;
|
|
|
+ width: calc(100vw - 100px);
|
|
|
+ height: calc(100vh - 100px);
|
|
|
+
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+ </head>
|
|
|
+ <body>
|
|
|
+ <div id="main"></div>
|
|
|
+ <div id="main2"></div>
|
|
|
+ </body>
|
|
|
+ <script>
|
|
|
+ var exchangeColor = {binance: '#F4BC0C', gate: '#0068FF', okx: '#171F30'};
|
|
|
+ var chartDom = document.getElementById('main');
|
|
|
+ var chartDom2 = document.getElementById('main2');
|
|
|
+ var myChart2 = echarts.init(chartDom2);
|
|
|
+ var myChart = echarts.init(chartDom);
|
|
|
+ var option,option2;
|
|
|
+
|
|
|
+ option = {
|
|
|
+ title: {
|
|
|
+ text: '{{name_html}}帧交易量分析图'
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: {
|
|
|
+ type: 'shadow'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ legend: {},
|
|
|
+ grid: {
|
|
|
+ left: '3%',
|
|
|
+ right: '4%',
|
|
|
+ bottom: '3%',
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'value',
|
|
|
+ boundaryGap: [0, 0.01]
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: "#.to_owned() + json_data["pancake_data_y"].to_string().as_str() + "
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'bar',
|
|
|
+ data: " + json_data["pancake_data_x"].to_string().as_str() + "
|
|
|
+ },
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ option && myChart.setOption(option);
|
|
|
+
|
|
|
+
|
|
|
+ option2 = {
|
|
|
+ title: {
|
|
|
+ text: ' Data',
|
|
|
+ left: 10
|
|
|
+ },
|
|
|
+ toolbox: {
|
|
|
+ feature: {
|
|
|
+ dataZoom: {
|
|
|
+ yAxisIndex: false
|
|
|
+ },
|
|
|
+ saveAsImage: {
|
|
|
+ pixelRatio: 2
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: {
|
|
|
+ type: 'shadow'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ bottom: 90
|
|
|
+ },
|
|
|
+ dataZoom: [
|
|
|
+ {
|
|
|
+ type: 'inside'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'slider'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ xAxis: {
|
|
|
+ data: " + json_data["turnover_data_x"].to_string().as_str() + ",
|
|
|
+ silent: false,
|
|
|
+ splitLine: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ splitArea: {
|
|
|
+ show: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ splitArea: {
|
|
|
+ show: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'bar',
|
|
|
+ data: " + json_data["turnover_data_y"].to_string().as_str() + ",
|
|
|
+ large: true
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ option2 && myChart2.setOption(option2);
|
|
|
+
|
|
|
+ </script>
|
|
|
+ </html>
|
|
|
+ ";
|
|
|
+
|
|
|
+ // 编译模板
|
|
|
+ handlebars
|
|
|
+ .register_template_string("page", template)
|
|
|
+ .expect("编译模版失败!");
|
|
|
+
|
|
|
+ // 渲染模板
|
|
|
+ let output = handlebars
|
|
|
+ .render("page", &data)
|
|
|
+ .expect("渲染模版失败!");
|
|
|
+
|
|
|
+ // 将 HTML 写入文件
|
|
|
+ let mut file = File::create(&path).expect("创建文件失败!");
|
|
|
+ file.write_all(output.as_bytes()).expect("写入文件到本地失败!");
|
|
|
+ info!("Ticker信息网页生成成功!路径:{:?}\n\n", path);
|
|
|
+}
|