|
|
@@ -1,177 +1,21 @@
|
|
|
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("//", "/");
|
|
|
+#[allow(dead_code)]
|
|
|
+//生成html
|
|
|
+pub fn export_html(title: &str, symbol: &str, x_values: Vec<Decimal>, y_values: Vec<Decimal>) {
|
|
|
+ info!("正在生成网页,请稍候……");
|
|
|
+ let path = format!("./{}新版无敌大数据ticker.html", symbol);
|
|
|
// 创建 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,
|
|
|
+ "chart_title": format!("{}的{}新版无敌大数据Ticker", title, symbol),
|
|
|
+ "x_values": x_values,
|
|
|
+ "y_values": y_values,
|
|
|
});
|
|
|
// HTML 模板
|
|
|
let template = r#"
|
|
|
@@ -193,10 +37,14 @@ pub fn export_html(export_info: Vec<ExportExchangeTickerInfo>, start_at: &str, e
|
|
|
width: calc(100vw - 100px);
|
|
|
height: calc(100vh - 100px);
|
|
|
}
|
|
|
+ .info_wp{
|
|
|
+ padding: 0 40px;
|
|
|
+ }
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<div id="main"></div>
|
|
|
+ <div class="info_wp">{{statistic_result}}</div>
|
|
|
</body>
|
|
|
<script>
|
|
|
var exchangeColor = {binance: '#F4BC0C', gate: '#0068FF', okx: '#171F30'};
|
|
|
@@ -205,143 +53,84 @@ pub fn export_html(export_info: Vec<ExportExchangeTickerInfo>, start_at: &str, e
|
|
|
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: {
|
|
|
+ title: {
|
|
|
+ text: '{{chart_title}}'
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: {
|
|
|
+ type: 'cross',
|
|
|
+ lineStyle: {
|
|
|
+ type: 'dashed',
|
|
|
+ width: 1
|
|
|
+ }
|
|
|
+ },
|
|
|
formatter: function (value) {
|
|
|
- var time = dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
|
|
- return time;
|
|
|
+ let time = dayjs(value[0].name *1).format('YYYY-MM-DD HH:mm:ss.SSS');
|
|
|
+ let price = value[0].value;
|
|
|
+ return `时间:${time}<br/>价格:${price}`
|
|
|
+ },
|
|
|
+ },
|
|
|
+ toolbox: {
|
|
|
+ feature: {
|
|
|
+ dataZoom: {},
|
|
|
+ brush: {
|
|
|
+ type: ['rect', 'clear']
|
|
|
+ }
|
|
|
}
|
|
|
},
|
|
|
- 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
|
|
|
+ legend: {
|
|
|
+ data: {{x_values}}
|
|
|
},
|
|
|
- {{/if}}
|
|
|
- emphasis: {
|
|
|
- focus: 'series'
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ boundaryGap: false,
|
|
|
+ data: {{x_values}},
|
|
|
+ show : false
|
|
|
},
|
|
|
- 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'
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ boundaryGap: [0, '100%'],
|
|
|
+ formatter: function (value) {
|
|
|
+ return dayjs(value[0].name *1).format('YYYY-MM-DD HH:mm:ss.SSS');
|
|
|
},
|
|
|
- data: [
|
|
|
- [
|
|
|
- {
|
|
|
- name: '{{name}}数据',
|
|
|
- xAxis: 'min',
|
|
|
- yAxis: 'min'
|
|
|
- },
|
|
|
- {
|
|
|
- xAxis: 'max',
|
|
|
- yAxis: 'max'
|
|
|
- }
|
|
|
- ]
|
|
|
- ]
|
|
|
},
|
|
|
- },
|
|
|
- {{/each}}
|
|
|
- ]
|
|
|
- };
|
|
|
+ dataZoom: [
|
|
|
+ {
|
|
|
+ type: 'inside',
|
|
|
+ start: 0,
|
|
|
+ end: 10
|
|
|
+ },
|
|
|
+ {
|
|
|
+ start: 0,
|
|
|
+ end: 10
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '价格',
|
|
|
+ type: 'line',
|
|
|
+ symbol: 'none',
|
|
|
+ sampling: 'lttb',
|
|
|
+ itemStyle: {
|
|
|
+ color: 'rgb(255, 70, 131)'
|
|
|
+ },
|
|
|
+ areaStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ {
|
|
|
+ offset: 0,
|
|
|
+ color: 'rgb(255, 158, 68)'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ offset: 1,
|
|
|
+ color: 'rgb(255, 70, 131)'
|
|
|
+ }
|
|
|
+ ])
|
|
|
+ },
|
|
|
+ data: {{y_values}}
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
|
|
|
option && myChart.setOption(option);
|
|
|
</script>
|
|
|
@@ -361,5 +150,5 @@ pub fn export_html(export_info: Vec<ExportExchangeTickerInfo>, start_at: &str, e
|
|
|
// 将 HTML 写入文件
|
|
|
let mut file = File::create(&path).expect("创建文件失败!");
|
|
|
file.write_all(output.as_bytes()).expect("写入文件到本地失败!");
|
|
|
- info!("Ticker信息网页生成成功!路径:{:?}\n\n", path);
|
|
|
+ info!("Balance信息网页生成成功!路径:{:?}\n\n", path);
|
|
|
}
|