|
|
@@ -1,8 +1,13 @@
|
|
|
use std::ops::{Div, Mul};
|
|
|
+use std::path::Path;
|
|
|
use chrono::Utc;
|
|
|
use rand::Rng;
|
|
|
use rust_decimal::Decimal;
|
|
|
+use tokio::fs;
|
|
|
+use tokio::fs::File;
|
|
|
+use tokio::io::AsyncWriteExt;
|
|
|
use tracing::{error};
|
|
|
+use global::predictor_state::PredictorState;
|
|
|
use global::public_params;
|
|
|
|
|
|
// 生成订单的id,可以根据交易所名字来
|
|
|
@@ -127,6 +132,427 @@ pub fn get_limit_order_requests_num_per_second(exchange: String) -> i64 {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+pub async fn write_to_file(json_data: &String, file_path: String) {
|
|
|
+ // 尝试创建文件路径
|
|
|
+ if let Err(e) = fs::create_dir_all(
|
|
|
+ // 获取文件目录路径
|
|
|
+ Path::new(&file_path)
|
|
|
+ .parent() // 获取父目录(即文件路径除去文件名后的部分)
|
|
|
+ .unwrap_or_else(|| Path::new("")), // 如果没有父目录,使用当前目录
|
|
|
+ )
|
|
|
+ .await
|
|
|
+ {
|
|
|
+ // 如果创建路径失败,打印错误日志
|
|
|
+ error!("创建目录错误: {:?}", e);
|
|
|
+ return; // 结束任务
|
|
|
+ }
|
|
|
+
|
|
|
+ // 异步地执行文件写入操作
|
|
|
+ if let Err(e) = async {
|
|
|
+ let mut file = File::create(&file_path).await?;
|
|
|
+ file.write_all(json_data.as_bytes()).await?;
|
|
|
+ Result::<(), std::io::Error>::Ok(())
|
|
|
+ }
|
|
|
+ .await
|
|
|
+ {
|
|
|
+ // 如果发生错误,只打印错误日志
|
|
|
+ error!("json db写入错误: {:?}", e);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+pub async fn build_html_file(data_c: &Vec<PredictorState>) -> String {
|
|
|
+ let temp_json_str = serde_json::to_string(&data_c).unwrap();
|
|
|
+
|
|
|
+ let str1 = r##"
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="en">
|
|
|
+ <head>
|
|
|
+ <meta charset="UTF-8" />
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
+ <title>Document</title>
|
|
|
+ <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
|
+ <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;
|
|
|
+ }
|
|
|
+ #echarts {
|
|
|
+ margin: 50px auto 0;
|
|
|
+ width: calc(100vw - 100px);
|
|
|
+ height: 1030px;
|
|
|
+ }
|
|
|
+ .page-group-wp {
|
|
|
+ padding: 20px 40px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
+ .page-button {
|
|
|
+ cursor: pointer;
|
|
|
+ border: 1px solid #16b777;
|
|
|
+ color: #16b777;
|
|
|
+ padding: 2px 10px;
|
|
|
+ user-select: none;
|
|
|
+ }
|
|
|
+ .disabled {
|
|
|
+ border: 1px solid #ccc;
|
|
|
+ color: #ccc;
|
|
|
+ cursor: not-allowed;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+ </head>
|
|
|
+ <body>
|
|
|
+ <div class="page-group-wp">
|
|
|
+ <div class="page-button upper-btn">上一页</div>
|
|
|
+ <div class="show-page">第1/1页</div>
|
|
|
+ <div class="page-button next-btn">下一页</div>
|
|
|
+ </div>
|
|
|
+ <div id="echarts"></div>
|
|
|
+ </body>
|
|
|
+ <script>
|
|
|
+ let initData = "##;
|
|
|
+
|
|
|
+ let str2 = r##";
|
|
|
+ let init_echarts_data = (data) => {
|
|
|
+ const xData = data.map((item) => dayjs(item.update_time * 1).format("MM-DD HH:mm:ss:SSS"));
|
|
|
+ let inventory = 0;
|
|
|
+ let inventoryList = [];
|
|
|
+
|
|
|
+ data.map((item, index) => {
|
|
|
+ if (inventory != item.inventory) {
|
|
|
+ inventory = item.inventory;
|
|
|
+ let color = "";
|
|
|
+ if(item.inventory > 0) color = "#F2495E"
|
|
|
+ if(item.inventory < 0) color = "#13BF86"
|
|
|
+ inventoryList.push({ name: "inventory",itemStyle:{color: color}, value: item.inventory, xAxis: index, yAxis: item.mid_price });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return {
|
|
|
+ dataZoom: [
|
|
|
+ {
|
|
|
+ type: "inside",
|
|
|
+ xAxisIndex: [0, 1, 2, 3, 4],
|
|
|
+ start: 0,
|
|
|
+ end: 100,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ xAxisIndex: [0, 1, 2, 3, 4],
|
|
|
+ start: 0,
|
|
|
+ end: 100,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ tooltip: {
|
|
|
+ trigger: "axis",
|
|
|
+ formatter: (value) => {
|
|
|
+ return `时间:${value[0].name}<br />${value.map((item) => `${item.marker}${item.seriesName}:${item.value}`).join("<br/>")}`;
|
|
|
+ },
|
|
|
+ axisPointer: {
|
|
|
+ animation: false,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ toolbox: {
|
|
|
+ feature: {
|
|
|
+ dataZoom: {},
|
|
|
+ brush: {
|
|
|
+ type: ["rect", "clear"],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ legend: [
|
|
|
+ {
|
|
|
+ data: ["mid_price", "ask_price", "bid_price", "optimal_ask_price", "optimal_bid_price", "ref_price"],
|
|
|
+ top: "230px",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ data: ["spread", "spread_max", "spread_min"],
|
|
|
+ top: "480px",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ data: ["sigma_square"],
|
|
|
+ top: "630px",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ data: ["gamma"],
|
|
|
+ top: "780px",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ data: ["kappa"],
|
|
|
+ top: "930px",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ grid: [
|
|
|
+ {
|
|
|
+ top: "50px",
|
|
|
+ left: "60px",
|
|
|
+ right: "60px",
|
|
|
+ height: "150px", // 主图高度
|
|
|
+ },
|
|
|
+ {
|
|
|
+ top: "300px",
|
|
|
+ left: "60px",
|
|
|
+ right: "60px",
|
|
|
+ height: "150px",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ top: "550px",
|
|
|
+ left: "60px",
|
|
|
+ right: "60px",
|
|
|
+ height: "50px",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ top: "700px",
|
|
|
+ left: "60px",
|
|
|
+ right: "60px",
|
|
|
+ height: "50px",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ top: "850px",
|
|
|
+ left: "60px",
|
|
|
+ right: "60px",
|
|
|
+ height: "50px",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ xAxis: [
|
|
|
+ {
|
|
|
+ type: "category",
|
|
|
+ data: xData,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ gridIndex: 1, // 第1个网格的 x 轴
|
|
|
+ type: "category",
|
|
|
+ data: xData,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ gridIndex: 2, // 第2个网格的 x 轴
|
|
|
+ type: "category",
|
|
|
+ data: xData,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ gridIndex: 3, // 第3个网格的 x 轴
|
|
|
+ type: "category",
|
|
|
+ data: xData,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ gridIndex: 4, // 第4个网格的 x 轴
|
|
|
+ type: "category",
|
|
|
+ data: xData,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ yAxis: [
|
|
|
+ {
|
|
|
+ type: "value",
|
|
|
+ min: "dataMin",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ gridIndex: 1, // 第1个网格的 y 轴
|
|
|
+ type: "value",
|
|
|
+ min: "dataMin",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ gridIndex: 2, // 第2个网格的 y 轴
|
|
|
+ type: "value",
|
|
|
+ min: "dataMin",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ gridIndex: 3, // 第3个网格的 y 轴
|
|
|
+ type: "value",
|
|
|
+ min: "dataMin",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ gridIndex: 4, // 第4个网格的 y 轴
|
|
|
+ type: "value",
|
|
|
+ min: "dataMin",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: "mid_price",
|
|
|
+ type: "line",
|
|
|
+ showSymbol: false,
|
|
|
+ data: data.map((item) => item.mid_price),
|
|
|
+ markPoint: {
|
|
|
+ symbolSize: 30,
|
|
|
+ data: inventoryList,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "ask_price",
|
|
|
+ type: "line",
|
|
|
+ showSymbol: false,
|
|
|
+ data: data.map((item) => item.ask_price),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "bid_price",
|
|
|
+ type: "line",
|
|
|
+ showSymbol: false,
|
|
|
+ data: data.map((item) => item.bid_price),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "optimal_ask_price",
|
|
|
+ type: "line",
|
|
|
+ showSymbol: false,
|
|
|
+ data: data.map((item) => item.optimal_ask_price),
|
|
|
+ lineStyle: {
|
|
|
+ type: "dashed",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "optimal_bid_price",
|
|
|
+ type: "line",
|
|
|
+ showSymbol: false,
|
|
|
+ data: data.map((item) => item.optimal_bid_price),
|
|
|
+ lineStyle: {
|
|
|
+ type: "dashed",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "ref_price",
|
|
|
+ type: "line",
|
|
|
+ showSymbol: false,
|
|
|
+ data: data.map((item) => item.ref_price),
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ name: "spread",
|
|
|
+ type: "line",
|
|
|
+ xAxisIndex: 1,
|
|
|
+ yAxisIndex: 1,
|
|
|
+ showSymbol: false,
|
|
|
+ data: data.map((item) => item.spread),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "spread_max",
|
|
|
+ type: "line",
|
|
|
+ xAxisIndex: 1,
|
|
|
+ yAxisIndex: 1,
|
|
|
+ showSymbol: false,
|
|
|
+ data: data.map((item) => item.spread_max),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "spread_min",
|
|
|
+ type: "line",
|
|
|
+ xAxisIndex: 1,
|
|
|
+ yAxisIndex: 1,
|
|
|
+ showSymbol: false,
|
|
|
+ data: data.map((item) => item.spread_min),
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ name: "sigma_square",
|
|
|
+ type: "line",
|
|
|
+ xAxisIndex: 2,
|
|
|
+ yAxisIndex: 2,
|
|
|
+ showSymbol: false,
|
|
|
+ data: data.map((item) => item.sigma_square),
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ name: "gamma",
|
|
|
+ type: "line",
|
|
|
+ xAxisIndex: 3,
|
|
|
+ yAxisIndex: 3,
|
|
|
+ showSymbol: false,
|
|
|
+ data: data.map((item) => item.gamma),
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ name: "kappa",
|
|
|
+ type: "line",
|
|
|
+ xAxisIndex: 4,
|
|
|
+ yAxisIndex: 4,
|
|
|
+ showSymbol: false,
|
|
|
+ data: data.map((item) => item.kappa),
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ let update_echarts_data = (data) => {
|
|
|
+ const xData = data.map((item) => dayjs(item.update_time * 1).format("MM-DD HH:mm:ss:SSS"));
|
|
|
+ let inventory = 0;
|
|
|
+ let inventoryList = [];
|
|
|
+
|
|
|
+ data.map((item, index) => {
|
|
|
+ if (inventory != item.inventory) {
|
|
|
+ inventory = item.inventory;
|
|
|
+ let color = "";
|
|
|
+ if(item.inventory > 0) color = "#F2495E"
|
|
|
+ if(item.inventory < 0) color = "#13BF86"
|
|
|
+ inventoryList.push({ name: "inventory",itemStyle:{color: color}, value: item.inventory, xAxis: index, yAxis: item.mid_price });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return {
|
|
|
+ xAxis: [{ data: xData }, { data: xData }, { data: xData }, { data: xData }, { data: xData }],
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ data: data.map((item) => item.mid_price),
|
|
|
+ markPoint: {
|
|
|
+ data: inventoryList,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ { data: data.map((item) => item.ask_price) },
|
|
|
+ { data: data.map((item) => item.bid_price) },
|
|
|
+ { data: data.map((item) => item.optimal_ask_price) },
|
|
|
+ { data: data.map((item) => item.optimal_bid_price) },
|
|
|
+ { data: data.map((item) => item.ref_price) },
|
|
|
+ { data: data.map((item) => item.inventory) },
|
|
|
+ { data: data.map((item) => item.spread) },
|
|
|
+ { data: data.map((item) => item.spread_max) },
|
|
|
+ { data: data.map((item) => item.spread_min) },
|
|
|
+ { data: data.map((item) => item.sigma_square) },
|
|
|
+ { data: data.map((item) => item.gamma) },
|
|
|
+ { data: data.map((item) => item.kappa) },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ let page = 1;
|
|
|
+ let maxPage = initData.length % 50000 > 0 ? Math.ceil(initData.length / 50000) : initData.length / 50000 || 1;
|
|
|
+ let showData = initData.slice((page - 1) * 100000, page * 100000 - 1);
|
|
|
+ let chartDom = document.getElementById("echarts");
|
|
|
+ let myChart = echarts.init(chartDom);
|
|
|
+ let initOption = init_echarts_data(showData);
|
|
|
+ initOption && myChart.setOption(initOption);
|
|
|
+
|
|
|
+ let updata_page = () => {
|
|
|
+ $(".show-page").eq(0).html(`第${page}/${maxPage}页`);
|
|
|
+ $(".next-btn").eq(0).removeClass("disabled");
|
|
|
+ $(".upper-btn").eq(0).removeClass("disabled");
|
|
|
+ if (page >= maxPage) $(".next-btn").eq(0).addClass("disabled");
|
|
|
+ if (page <= 1) $(".upper-btn").eq(0).addClass("disabled");
|
|
|
+ };
|
|
|
+ updata_page();
|
|
|
+ let update_echarts = () => {
|
|
|
+ showData = initData.slice((page - 1) * 50000, page * 50000 + 49999);
|
|
|
+ let option = update_echarts_data(showData);
|
|
|
+ myChart.setOption(option, false, true);
|
|
|
+ };
|
|
|
+
|
|
|
+ $(".next-btn")
|
|
|
+ .eq(0)
|
|
|
+ .click(() => {
|
|
|
+ if (page < maxPage) {
|
|
|
+ page += 1;
|
|
|
+ update_echarts();
|
|
|
+ updata_page();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ $(".upper-btn")
|
|
|
+ .eq(0)
|
|
|
+ .click(() => {
|
|
|
+ if (page > 1) {
|
|
|
+ page -= 1;
|
|
|
+ update_echarts();
|
|
|
+ updata_page();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ </script>
|
|
|
+</html>"##;
|
|
|
+
|
|
|
+ format!("{}{}{}", str1, temp_json_str, str2)
|
|
|
+}
|
|
|
+
|
|
|
#[cfg(test)]
|
|
|
mod tests {
|
|
|
use chrono::Utc;
|