|
@@ -0,0 +1,365 @@
|
|
|
|
|
+use std::fs::File;
|
|
|
|
|
+use std::io::Write;
|
|
|
|
|
+use std::str::FromStr;
|
|
|
|
|
+use chrono::NaiveDateTime;
|
|
|
|
|
+use handlebars::Handlebars;
|
|
|
|
|
+use rust_decimal::Decimal;
|
|
|
|
|
+use rust_decimal::prelude::{FromPrimitive, ToPrimitive};
|
|
|
|
|
+use uuid::Uuid;
|
|
|
|
|
+use rust_decimal_macros::dec;
|
|
|
|
|
+use serde::{Deserialize, Serialize};
|
|
|
|
|
+use tracing::info;
|
|
|
|
|
+use crate::struct_standard::Trades;
|
|
|
|
|
+use crate::utils::utils::TickerConfigInfo;
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
|
|
|
+pub struct ExportExchangeTickerInfo {
|
|
|
|
|
+ pub name: String,
|
|
|
|
|
+ pub ticker_info: Vec<Trades>,
|
|
|
|
|
+ pub recall_ticker_info: Vec<Trades>,
|
|
|
|
|
+ pub max_price: String,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
|
|
|
+pub struct SeriesInfo {
|
|
|
|
|
+ pub name: String,
|
|
|
|
|
+ pub classify: String,
|
|
|
|
|
+ pub data: Vec<Trades>,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+impl SeriesInfo {
|
|
|
|
|
+ fn new() -> SeriesInfo {
|
|
|
|
|
+ SeriesInfo {
|
|
|
|
|
+ name: "".to_string(),
|
|
|
|
|
+ classify: "".to_string(),
|
|
|
|
|
+ data: vec![],
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+pub fn export_html(export_info: Vec<ExportExchangeTickerInfo>, start_at: &str, end_at: &str, config: TickerConfigInfo, robot_info: Vec<Trades>) {
|
|
|
|
|
+ info!("正在生成网页,请稍后!");
|
|
|
|
|
+ let export_path = if config.export_path == "" { "./" } else { config.export_path.as_str() };
|
|
|
|
|
+ let export_name = if config.export_name == "" { "export_ticker" } else { config.export_name.as_str() };
|
|
|
|
|
+ let path = format!("{}/{}.html", export_path, export_name).replace("//", "/");
|
|
|
|
|
+ // 创建 Handlebars 实例
|
|
|
|
|
+ let mut handlebars = Handlebars::new();
|
|
|
|
|
+
|
|
|
|
|
+ let mut initiative_info = SeriesInfo::new();
|
|
|
|
|
+ if config.recall_time != 0 {
|
|
|
|
|
+ let mut max_price = Decimal::ZERO;
|
|
|
|
|
+ for item in export_info.clone() {
|
|
|
|
|
+ let exchange_max_price = Decimal::from_str(&item.max_price).unwrap();
|
|
|
|
|
+ max_price = if max_price < exchange_max_price { exchange_max_price } else { max_price };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let mut old_ticker_info: Vec<Trades> = export_info.iter().flat_map(|exchange| exchange.ticker_info.clone()).collect();
|
|
|
|
|
+ old_ticker_info.sort_by(|a, b| a.create_time.cmp(&b.create_time));
|
|
|
|
|
+
|
|
|
|
|
+ let mut old_recall_ticker_info: Vec<Trades> = export_info.iter().flat_map(|exchange| exchange.recall_ticker_info.clone()).collect();
|
|
|
|
|
+ old_recall_ticker_info.sort_by(|a, b| a.create_time.cmp(&b.create_time));
|
|
|
|
|
+
|
|
|
|
|
+ let mut ticker_info = vec![];
|
|
|
|
|
+ let mut last_bool = "";
|
|
|
|
|
+ for trades in old_ticker_info {
|
|
|
|
|
+ // 计算交易量差
|
|
|
|
|
+ let mut volume = Decimal::ZERO;
|
|
|
|
|
+ let mut short_volume = Decimal::ZERO;
|
|
|
|
|
+ let mut long_volume = Decimal::ZERO;
|
|
|
|
|
+ let mut sum_volume = Decimal::ZERO;
|
|
|
|
|
+ let recall_ticker_info: Vec<Trades> = old_recall_ticker_info.iter().filter(|recall_trades| {
|
|
|
|
|
+ let recall_create_time = Decimal::from_str(&recall_trades.create_time).unwrap();
|
|
|
|
|
+ let create_time = Decimal::from_str(&trades.create_time).unwrap();
|
|
|
|
|
+ let recall_time = Decimal::from_i64(config.recall_time).unwrap();
|
|
|
|
|
+ recall_create_time <= create_time && create_time - recall_create_time <= recall_time
|
|
|
|
|
+ }).cloned().collect();
|
|
|
|
|
+ for recall_trades in recall_ticker_info.clone() {
|
|
|
|
|
+ let size = Decimal::from_str(&recall_trades.size).unwrap();
|
|
|
|
|
+ volume += size;
|
|
|
|
|
+ sum_volume += size.abs();
|
|
|
|
|
+ if size > dec!(0) { long_volume += size } else { short_volume += size.abs() }
|
|
|
|
|
+ };
|
|
|
|
|
+ let long_volume_bool = long_volume / sum_volume >= config.long_volume_rate;
|
|
|
|
|
+ let short_volume_bool = short_volume / sum_volume >= config.short_volume_rate;
|
|
|
|
|
+
|
|
|
|
|
+ if (long_volume_bool && last_bool != "initiative_long") || (short_volume_bool && last_bool != "initiative_short") || (!long_volume_bool && !short_volume_bool && last_bool != "initiative_none") {
|
|
|
|
|
+ // 新增订单流主动性数据
|
|
|
|
|
+ let max_price = max_price * dec!(1.005);
|
|
|
|
|
+ let mut side = "";
|
|
|
|
|
+ if long_volume_bool {
|
|
|
|
|
+ last_bool = "initiative_long";
|
|
|
|
|
+ side = "LONG";
|
|
|
|
|
+ }
|
|
|
|
|
+ if short_volume_bool {
|
|
|
|
|
+ last_bool = "initiative_short";
|
|
|
|
|
+ side = "SHORT";
|
|
|
|
|
+ }
|
|
|
|
|
+ if !long_volume_bool && !short_volume_bool {
|
|
|
|
|
+ last_bool = "initiative_none";
|
|
|
|
|
+ side = "NONE";
|
|
|
|
|
+ }
|
|
|
|
|
+ ticker_info.push(Trades {
|
|
|
|
|
+ id: Uuid::new_v4().to_string()[0..8].to_string(),
|
|
|
|
|
+ data_type: last_bool.to_string(),
|
|
|
|
|
+ symbol: trades.symbol,
|
|
|
|
|
+ create_time: trades.create_time,
|
|
|
|
|
+ size: volume.to_string(),
|
|
|
|
|
+ price: max_price.to_string(),
|
|
|
|
|
+ side: side.to_string(),
|
|
|
|
|
+ is_effect: true,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 对订单流主动性数据(recall)去重
|
|
|
|
|
+ let mut ticker_set = std::collections::HashSet::new();
|
|
|
|
|
+ let mut initiative_ticker_info = vec![];
|
|
|
|
|
+ for trades in ticker_info {
|
|
|
|
|
+ if ticker_set.insert((trades.data_type.clone(), trades.create_time.clone())) {
|
|
|
|
|
+ initiative_ticker_info.push(trades.clone());
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ initiative_info = SeriesInfo {
|
|
|
|
|
+ name: "主动性".to_string(),
|
|
|
|
|
+ classify: "initiative".to_string(),
|
|
|
|
|
+ data: initiative_ticker_info.clone(),
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let start_time_d = Decimal::from_str(start_at).unwrap() * dec!(1000) + dec!(8) * dec!(3600000);
|
|
|
|
|
+ let end_time_d = Decimal::from_str(end_at).unwrap() * dec!(1000) + dec!(8) * dec!(3600000);
|
|
|
|
|
+ let start_time = NaiveDateTime::from_timestamp_millis(start_time_d.to_i64().unwrap()).unwrap().format("%Y-%m-%d %H:%M:%S%.3f").to_string();
|
|
|
|
|
+ let end_time = NaiveDateTime::from_timestamp_millis(end_time_d.to_i64().unwrap()).unwrap().format("%Y-%m-%d %H:%M:%S%.3f").to_string();
|
|
|
|
|
+
|
|
|
|
|
+ let mut name_list: Vec<String> = vec![];
|
|
|
|
|
+ let mut legend_list: Vec<String> = vec![];
|
|
|
|
|
+ let mut series_info: Vec<SeriesInfo> = vec![];
|
|
|
|
|
+
|
|
|
|
|
+ for item in export_info.clone() {
|
|
|
|
|
+ name_list.push(format!("'{}'", item.name));
|
|
|
|
|
+ series_info.push(SeriesInfo {
|
|
|
|
|
+ name: item.name.clone(),
|
|
|
|
|
+ classify: "ticker".to_string(),
|
|
|
|
|
+ data: item.ticker_info.clone(),
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ if config.recall_time != 0 {
|
|
|
|
|
+ series_info.push(initiative_info);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if config.robot_name != "" {
|
|
|
|
|
+ let ref_robot_info: Vec<Trades> = robot_info.iter().filter(|trades| trades.data_type == "robot_ref_info").cloned().collect();
|
|
|
|
|
+ series_info.push(SeriesInfo {
|
|
|
|
|
+ name: "预定价格".to_string(),
|
|
|
|
|
+ classify: "robot_ref".to_string(),
|
|
|
|
|
+ data: ref_robot_info.clone(),
|
|
|
|
|
+ });
|
|
|
|
|
+ let reg_robot_info: Vec<Trades> = robot_info.iter().filter(|trades| trades.data_type == "robot_reg_info").cloned().collect();
|
|
|
|
|
+ series_info.push(SeriesInfo {
|
|
|
|
|
+ name: "挂单价格".to_string(),
|
|
|
|
|
+ classify: "robot_reg".to_string(),
|
|
|
|
|
+ data: reg_robot_info.clone(),
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for item in series_info.clone() {
|
|
|
|
|
+ legend_list.push(format!("'{}'", item.name));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let data = serde_json::json!({
|
|
|
|
|
+ "chart_title": format!("{} Ticker数据", name_list.join("、").replace("'","")),
|
|
|
|
|
+ "legend_data": format!("[{}]", legend_list.join(", ")),
|
|
|
|
|
+ "series_info": series_info.clone(),
|
|
|
|
|
+ "symbol": config.symbol.to_uppercase(),
|
|
|
|
|
+ "start_at": start_time,
|
|
|
|
|
+ "end_at": end_time,
|
|
|
|
|
+ });
|
|
|
|
|
+ // 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);
|
|
|
|
|
+ }
|
|
|
|
|
+ </style>
|
|
|
|
|
+ </head>
|
|
|
|
|
+ <body>
|
|
|
|
|
+ <div id="main"></div>
|
|
|
|
|
+ </body>
|
|
|
|
|
+ <script>
|
|
|
|
|
+ var exchangeColor = {binance: '#F4BC0C', gate: '#0068FF', okx: '#171F30'};
|
|
|
|
|
+ var chartDom = document.getElementById('main');
|
|
|
|
|
+ var myChart = echarts.init(chartDom);
|
|
|
|
|
+ var option;
|
|
|
|
|
+
|
|
|
|
|
+ option = {
|
|
|
|
|
+ title: {
|
|
|
|
|
+ text: '{{{chart_title}}}',
|
|
|
|
|
+ subtext: '币对:{{symbol}} 时间:{{start_at}} ~ {{end_at}}'
|
|
|
|
|
+ },
|
|
|
|
|
+ grid: {
|
|
|
|
|
+ left: '3%',
|
|
|
|
|
+ right: '7%',
|
|
|
|
|
+ bottom: '7%',
|
|
|
|
|
+ containLabel: true
|
|
|
|
|
+ },
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ // trigger: 'axis',
|
|
|
|
|
+ showDelay: 0,
|
|
|
|
|
+ formatter: function (params) {
|
|
|
|
|
+ if(params.value.length <= 0) return "";
|
|
|
|
|
+ var time = dayjs(params.value[0]).format('YYYY-MM-DD HH:mm:ss.SSS');
|
|
|
|
|
+ switch (params.value[4]){
|
|
|
|
|
+ case "ticker": return `${params.seriesName}:<br/>时间: ${time}<br/>价格: ${params.value[1]}<br/>数量: ${params.value[2]}<br/>买卖方向: ${params.value[3]}`;
|
|
|
|
|
+ case "initiative": return `${params.seriesName}:<br/>时间: ${time}<br/>数量: ${params.value[2]}<br/>主动方向: ${params.value[3]}`;
|
|
|
|
|
+ case "robot_ref": return `${params.seriesName}:<br/>时间: ${time}<br/>价格: ${params.value[1]}<br/>数量: ${params.value[2]}`;
|
|
|
|
|
+ case "robot_reg": return `${params.seriesName}:<br/>时间: ${time}<br/>价格: ${params.value[1]}<br/>数量: ${params.value[2]}<br/>仓位方向: ${params.value[3]}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ axisPointer: {
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ type: 'cross',
|
|
|
|
|
+ lineStyle: {
|
|
|
|
|
+ type: 'dashed',
|
|
|
|
|
+ width: 1
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ toolbox: {
|
|
|
|
|
+ feature: {
|
|
|
|
|
+ dataZoom: {},
|
|
|
|
|
+ brush: {
|
|
|
|
|
+ type: ['rect', 'clear']
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ brush: {},
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ data: {{{legend_data}}},
|
|
|
|
|
+ left: 'center',
|
|
|
|
|
+ bottom: 10
|
|
|
|
|
+ },
|
|
|
|
|
+ xAxis: [
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'value',
|
|
|
|
|
+ scale: true,
|
|
|
|
|
+ axisLabel: {
|
|
|
|
|
+ formatter: function (value) {
|
|
|
|
|
+ var time = dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
|
+ return time;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ splitLine: {
|
|
|
|
|
+ show: false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ yAxis: [
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'value',
|
|
|
|
|
+ scale: true,
|
|
|
|
|
+ axisLabel: {
|
|
|
|
|
+ formatter: '{value}'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ series: [
|
|
|
|
|
+ {{#each series_info as | series |}}
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '{{series.name}}',
|
|
|
|
|
+ type: 'scatter',
|
|
|
|
|
+ {{#if (eq series.classify 'ticker')}}symbol: 'triangle',{{/if}}
|
|
|
|
|
+ {{#if (eq series.classify 'initiative')}}symbol: 'circle',{{/if}}
|
|
|
|
|
+ {{#if (eq series.classify 'robot_ref')}}symbol: 'rect',{{/if}}
|
|
|
|
|
+ {{#if (eq series.classify 'robot_reg')}}symbol: 'rect',{{/if}}
|
|
|
|
|
+
|
|
|
|
|
+ {{#if (eq series.classify 'ticker')}}color: exchangeColor['{{series.name}}'.toLocaleLowerCase()],{{/if}}
|
|
|
|
|
+ {{#if (eq series.classify 'initiative')}}color: '#D2D2D2',{{/if}}
|
|
|
|
|
+ {{#if (eq series.classify 'robot_reg')}}
|
|
|
|
|
+ color: {
|
|
|
|
|
+ type: 'linear',
|
|
|
|
|
+ x: 0,
|
|
|
|
|
+ y: 0,
|
|
|
|
|
+ x2: 1,
|
|
|
|
|
+ y2: 0,
|
|
|
|
|
+ colorStops: [{offset: 0, color: 'green'},{offset: 0.5, color: 'green'},{offset: 0.5, color: 'red'}, {offset: 1, color: 'red'}],
|
|
|
|
|
+ global: false
|
|
|
|
|
+ },
|
|
|
|
|
+ {{/if}}
|
|
|
|
|
+ emphasis: {
|
|
|
|
|
+ focus: 'series'
|
|
|
|
|
+ },
|
|
|
|
|
+ data:[
|
|
|
|
|
+ {{#each data as |value|}}
|
|
|
|
|
+ {
|
|
|
|
|
+ value: [{{value.create_time}}, {{value.price}}, {{value.size}}, '{{value.side}}', '{{series.classify}}', '{{value.data_type}}'],
|
|
|
|
|
+ symbolRotate: {{value.size}} * 1 > 0 && '{{series.classify}}' == 'ticker' ? '0' : '180',
|
|
|
|
|
+ itemStyle: {
|
|
|
|
|
+ {{#if (eq value.data_type 'robot_reg_info')}}color: {{value.size}} * 1 > 0 ? 'green' : 'red',{{/if}}
|
|
|
|
|
+ {{#if (eq value.data_type 'initiative_long')}}borderColor: 'green',{{/if}}
|
|
|
|
|
+ {{#if (eq value.data_type 'initiative_short')}}borderColor: 'red',{{/if}}
|
|
|
|
|
+ {{#if (eq value.data_type 'initiative_none')}}borderColor: 'black',{{/if}}
|
|
|
|
|
+ {{#if (eq series.classify 'ticker')}}borderColor: {{value.size}} * 1 > 0 ? 'green' : 'red',{{/if}}
|
|
|
|
|
+ borderWidth: 1,
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {{/each}}
|
|
|
|
|
+ ],
|
|
|
|
|
+ markArea: {
|
|
|
|
|
+ silent: true,
|
|
|
|
|
+ itemStyle: {
|
|
|
|
|
+ color: 'transparent',
|
|
|
|
|
+ borderWidth: 1,
|
|
|
|
|
+ borderType: 'dashed'
|
|
|
|
|
+ },
|
|
|
|
|
+ data: [
|
|
|
|
|
+ [
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '{{name}}数据',
|
|
|
|
|
+ xAxis: 'min',
|
|
|
|
|
+ yAxis: 'min'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ xAxis: 'max',
|
|
|
|
|
+ yAxis: 'max'
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {{/each}}
|
|
|
|
|
+ ]
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ option && myChart.setOption(option);
|
|
|
|
|
+ </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);
|
|
|
|
|
+}
|