فهرست منبع

添加套利资金曲线, 添加币对黑名单,币对排行榜

DESKTOP-NE65RNK\Citrus_limon 1 سال پیش
والد
کامیت
db765d8e08

+ 51 - 8
src/api/index.ts

@@ -30,8 +30,6 @@ export const update_user_password = (params: any, callback: any) => {
   });
 };
 
-
-
 // 日志管理
 // 日志管理-登录日志
 export const get_login_log_list = (params: any, callback: any) => {
@@ -53,6 +51,12 @@ export const get_remaining = (params: any, callback: any) => {
     if (data) callback && callback(data);
   });
 };
+// 账户信息-账户余额详情
+export const get_remaining_detail = (params: any, callback: any) => {
+  return http.request("/api/remaining/listDetail", "get", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};
 
 // 机器人管理
 // 机器人管理-机器人列表
@@ -62,13 +66,13 @@ export const get_robot_list = (params: any, callback: any) => {
   });
 };
 // 机器人管理-机器人详情
-export const get_robot_detail  = (params: any, callback: any) => {
+export const get_robot_detail = (params: any, callback: any) => {
   return http.request("/api/robot/findById", "get", params).then((data) => {
     if (data) callback && callback(data);
   });
 };
 // 机器人管理-机器人日志
-export const get_robot_logs  = (params: any, callback: any) => {
+export const get_robot_logs = (params: any, callback: any) => {
   return http.request("/api/robot/getRobotLog", "get", params).then((data) => {
     if (data) callback && callback(data);
   });
@@ -139,6 +143,12 @@ export const restore_robot = (params: any, callback: any) => {
     if (data) callback && callback(data);
   });
 };
+// 机器人管理-自动运维
+export const set_robot_auto = (params: any, callback: any) => {
+  return http.request("/api/robot/setAuto", "post", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};
 
 // 策略管理
 // 策略管理-策略列表
@@ -439,6 +449,36 @@ export const get_ia_exchanges = (params: any, callback: any) => {
     if (data) callback && callback(data);
   });
 };
+// 情报中心-币对排行列表
+export const get_symbols_rank = (params: any, callback: any) => {
+  return http.request("/rk/get_rank_list", "get", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};
+// 情报中心-币对排行交易所
+export const get_exchange_rank = (params: any, callback: any) => {
+  return http.request("/rk/get_exchanges", "get", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};
+// 情报中心-币对黑名单列表
+export const get_symbols_black = (params: any, callback: any) => {
+  return http.request("/api/robot/getBlackList", "get", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};
+// 情报中心-添加币对黑名单
+export const add_symbols_black = (params: any, callback: any) => {
+  return http.request("/api/robot/addBlackList", "post", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};
+// 情报中心-删除币对黑名单
+export const del_symbols_black = (params: any, callback: any) => {
+  return http.request("/api/robot/delBlackList", "post", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};
 
 // 资金曲线
 // 资金曲线-按用户
@@ -454,6 +494,13 @@ export const get_exchange_balance = (params: any, callback: any) => {
   });
 };
 
+// 资金曲线-套利
+export const get_arbitrage_balance = (params: any, callback: any) => {
+  return http.request("/api/remaining/capitalNingBo", "get", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};
+
 // 利润分析查询
 export const get_exchange_pair_profit = (params: any, callback: any) => {
   return http.request("/api/profitAnalysis/getExchangePairProfitPage", "get", params).then((data) => {
@@ -468,8 +515,6 @@ export const get_exchange_pair_profit_statistics = (params: any, callback: any)
   });
 };
 
-
-
 // 系统管理
 // 用户管理-用户列表
 export const get_user_list = (params: any, callback: any) => {
@@ -588,8 +633,6 @@ export const get_menu_all = (params: any, callback: any) => {
   });
 };
 
-
-
 // 数据收集-数据交易日志
 export const get_acquire_log = (params: any, callback: any) => {
   return http.request("/api/acquireULog/getPage", "get", params).then((data) => {

+ 11 - 4
src/assets/css/index.scss

@@ -5,16 +5,23 @@
   --button-warm-color: #ffb800;
   --button-danger-color: #ff5722;
   --button-checked-color: #16b777;
-  --normal-color: #1e9fff;
   --primary-color: rgb(28, 175, 131);
+  --normal-color: #1e9fff;
+  --warm-color: #ffb800;
   --danger-color: #ff5722;
 }
-.danger-color{
-  color: var(--danger-color);
-}
 .primary-color{
   color: var(--primary-color);
 }
+.normal-color{
+  color: var(--normal-color);
+}
+.warm-color{
+  color: var(--warm-color);
+}
+.danger-color{
+  color: var(--danger-color);
+}
 * {
   padding: 0;
   margin: 0;

+ 21 - 3
src/router/routes.ts

@@ -71,17 +71,35 @@ const routes: Array<RouteRecordRaw> = [
         meta: { title: "波动率指标", keepAlive: true },
       },
       {
-        path: "/indicator/sybmol_filter",
-        name: "IndicatorSybmolFilter",
-        component: () => import("@/views/indicator/sybmol_filter/index.vue"),
+        path: "/indicator/symbol_filter",
+        name: "IndicatorSymbolFilter",
+        component: () => import("@/views/indicator/symbol_filter/index.vue"),
         meta: { title: "币对筛选器", keepAlive: true },
       },
+      {
+        path: "/indicator/symbol_rank",
+        name: "IndicatorSymbolRank",
+        component: () => import("@/views/indicator/symbol_rank/index.vue"),
+        meta: { title: "币对排行", keepAlive: true },
+      },
+      {
+        path: "/indicator/blacklists",
+        name: "IndicatorBlacklists",
+        component: () => import("@/views/indicator/blacklists/index.vue"),
+        meta: { title: "币对黑名单", keepAlive: true },
+      },
       {
         path: "/statistic/balance_user",
         name: "StatisticBalanceUser",
         component: () => import("@/views/statistic/balance_user/index.vue"),
         meta: { title: "资金曲线-用户", keepAlive: true },
       },
+      {
+        path: "/statistic/balance_arbitrage",
+        name: "StatisticBalanceArbitrage",
+        component: () => import("@/views/statistic/balance_arbitrage/index.vue"),
+        meta: { title: "资金曲线-套利", keepAlive: true },
+      },
       {
         path: "/statistic/profit_analysis",
         name: "StatisticProfitAnalysis",

+ 1 - 0
src/utils/enum.ts

@@ -29,5 +29,6 @@ export const INDICATOR_EXCHANG_TYPE: Enum = {
 export const BALANCE_TYPE: Enum = {
   "0": "盈亏",
   "1": "转账",
+  "2": "其它",
 };
 

+ 137 - 0
src/views/bot/manage/components/Automate.vue

@@ -0,0 +1,137 @@
+<template>
+  <lay-layer :title="modelConfig.title" v-model="modelConfig.visible" area="auto" :btn="operator">
+    <div class="width-500 custom-layer" style="padding: 20px">
+      <lay-form :model="modelParams" ref="modelFormRef" required>
+        <lay-form-item label="是否自动调参" prop="isAuto">
+          <lay-switch v-model="modelParams.isAuto" :onswitch-value="1" :unswitch-value="0" />
+          <div class="tips">开启后会自动开关机机器,自动修改参数</div>
+        </lay-form-item>
+        <lay-form-item label="是否参考币安" prop="isRefBinance">
+          <lay-switch v-model="modelParams.isRefBinance" />
+          <div class="tips">开启后如果币安交易所存在该币对就会优先参考币安交易所</div>
+        </lay-form-item>
+        <lay-form-item label="调参间隔时间" prop="timeInterval">
+          <lay-input v-model="modelParams.timeInterval" placeholder="调参间隔时间(分钟)" />
+        </lay-form-item>
+        <lay-form-item label="取第几个" prop="coinIndex">
+          <lay-input v-model="modelParams.coinIndex" placeholder="设置排行榜排名第几的币对" />
+        </lay-form-item>
+        <lay-form-item label="开仓系数" prop="openRatio">
+          <lay-input v-model="modelParams.openRatio" placeholder="排行榜开仓值乘以系数" />
+        </lay-form-item>
+        <lay-form-item label="最小总分数" prop="scoreLimit">
+          <lay-input v-model="modelParams.scoreLimit" placeholder="排行榜总分数最小值" />
+        </lay-form-item>
+        <lay-form-item label="最小波动分数" prop="fluctuationScore">
+          <lay-input v-model="modelParams.fluctuationScore" placeholder="排行榜最小波动分数最小值" />
+        </lay-form-item>
+        <lay-form-item label="最小频次分数" prop="frequencyScore">
+          <lay-input v-model="modelParams.frequencyScore" placeholder="排行榜最小交易频次分数最小值" />
+        </lay-form-item>
+        <lay-form-item label="最小量分数" prop="transactionVolumeScore">
+          <lay-input v-model="modelParams.transactionVolumeScore" placeholder="排行榜最小交易量分数最小值" />
+        </lay-form-item>
+      </lay-form>
+    </div>
+  </lay-layer>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, getCurrentInstance } from "vue";
+import { set_robot_auto } from "@/api";
+
+const { proxy }: any = getCurrentInstance();
+
+const modelFormRef = ref();
+
+interface ModelConfig {
+  title: string;
+  visible: boolean;
+  isUpdate: boolean;
+  loading: boolean;
+}
+interface ModelParams {
+  id?: string;
+  exchange?: string;
+  isAuto?: number;
+  isRefBinance?: boolean;
+  timeInterval?: string;
+  coinIndex?: string;
+  openRatio?: string;
+  scoreLimit?: string;
+  fluctuationScore?: string;
+  frequencyScore?: string;
+  transactionVolumeScore?: string;
+}
+
+let modelParams = ref<ModelParams>({});
+let modelConfig: ModelConfig = reactive({ title: "", visible: false, isUpdate: false, loading: false });
+
+let handleResult = reactive<{ resolve?: any; reject?: any }>({});
+
+const show = (params?: any) => {
+  modelConfig.visible = true;
+  modelConfig.title = "设置自动调参";
+  let exchange = params.configList.find((item: any) => item.code == "exchange")?.val || undefined;
+  let refExchange = params.configList.find((item: any) => item.code == "ref_exchange")?.val || undefined;
+  modelParams.value = { id: params.id, isAuto: params.isAuto || 0, isRefBinance: false, ...params.autoConfig, exchange, refExchange };
+  return new Promise(async (resolve, reject) => {
+    handleResult.resolve = resolve;
+    handleResult.reject = reject;
+  });
+};
+
+const operator = reactive([
+  {
+    text: "确认",
+    callback: () => {
+      modelFormRef.value.validate((isValidate: boolean) => {
+        if (isValidate) {
+          modelConfig.loading = true;
+          const params = {
+            robotId: modelParams.value.id,
+            exchange: modelParams.value.exchange,
+            isAuto: modelParams.value.isAuto,
+            isRefBinance: modelParams.value.isRefBinance,
+            timeInterval: modelParams.value.timeInterval,
+            coinIndex: modelParams.value.coinIndex,
+            openRatio: modelParams.value.openRatio,
+            scoreLimit: modelParams.value.scoreLimit,
+            fluctuationScore: modelParams.value.fluctuationScore,
+            frequencyScore: modelParams.value.frequencyScore,
+            transactionVolumeScore: modelParams.value.transactionVolumeScore,
+          };
+          set_robot_auto(params, (data: any) => {
+            modelConfig.loading = false;
+            if (data.code == 200) {
+              proxy.$message("设置成功!");
+              modelConfig.visible = false;
+              handleResult.resolve(true);
+            }
+          });
+        }
+      });
+    },
+  },
+  {
+    text: "取消",
+    callback: () => {
+      modelConfig.visible = false;
+      handleResult.resolve(false);
+    },
+  },
+]);
+defineExpose({ show });
+</script>
+<style lang="scss" scoped>
+.tips {
+  font-size: 12px;
+  color: #2c2c2c;
+  &.danger {
+    color: var(--button-danger-color);
+  }
+}
+.split-text {
+  padding: 10px 0 30px;
+}
+</style>

+ 17 - 8
src/views/bot/manage/detail.vue

@@ -25,7 +25,7 @@
           <span class="card-title">净值图</span>
         </template>
         <template v-slot:body>
-          <div class="profit-chart" id="profit-chart" ref="balanceChartRef"></div>
+          <div class="profit-chart" ref="balanceChartRef"></div>
         </template>
       </lay-card>
     </div>
@@ -49,11 +49,11 @@
   <LogText ref="logtextRef" />
 </template>
 <script lang="ts" setup name="BotManageDetail">
-import { ref, reactive, onUnmounted, shallowRef } from "vue";
+import { ref, reactive, onMounted, onUnmounted, shallowRef } from "vue";
 import { useRoute } from "vue-router";
 import * as echarts from "echarts";
 import LogText from "./components/LogText.vue";
-import { get_robot_detail, get_robot_logs, get_remaining } from "@/api";
+import { get_robot_detail, get_robot_logs, get_remaining_detail } from "@/api";
 
 const ROBOT_STATUS: any = reactive({
   STOPPED: "已停止",
@@ -107,20 +107,19 @@ const getRobotDetail = () => {
     }
   });
 };
-getRobotDetail();
 
 // 获取账户余额
 const getBalanceInfo = (id: number) => {
   const params = { id: id };
   pageConfig.loading = true;
-  get_remaining(params, (data: any) => {
+  get_remaining_detail(params, (data: any) => {
     pageConfig.loading = false;
     if (data.code == 200) {
       balanceList.value = data.data;
 
       const xData = data.data.map((item: any) => item.creationTime);
-      const sData = data.data.map((item: any) => item.balance);
-      const yMinData = Math.min(sData);
+      const sData = data.data.map((item: any) => [item.creationTime, item.afterU, item.changeU, item.pair, item.openNum, item.closeNum]);
+      const yMinData = Math.min(sData.map((item: any) => item[1]));
 
       !balanceChart.value
         ? initChart(data.data)
@@ -177,13 +176,15 @@ const handlePageInfo = (data: any) => {
 };
 
 const initChart = (data: any) => {
+  if (balanceChart.value != null && !balanceChart.value.isDisposed()) echarts.dispose(balanceChart.value);
+
   balanceChart.value = echarts.init(balanceChartRef.value);
 
   window.removeEventListener("resize", () => balanceChart.value.resize());
   window.addEventListener("resize", () => balanceChart.value.resize());
 
   const xData = data.map((item: any) => item.creationTime);
-  const sData = data.map((item: any) => item.balance);
+  const sData = data.map((item: any) => [item.creationTime, item.afterU, item.changeU, item.pair, item.openNum, item.closeNum]);
   const yMinData = Math.min(sData);
 
   const balanceChartOption = {
@@ -192,6 +193,10 @@ const initChart = (data: any) => {
       axisPointer: {
         type: "cross",
       },
+      formatter: (params: any) => {
+        let info = params[0];
+        return `${info.marker}${info.seriesName}<br/>时间:${info.value[0]} <br/>余额:${info.value[1]}<br/>收益:${info.value[2]}<br/>币对:${info.value[3]}<br/>开仓:${info.value[4]}<br/>平仓:${info.value[5]}`;
+      },
     },
     dataZoom: [
       {
@@ -229,6 +234,10 @@ timer.value = setInterval(() => {
   getLogsInfo();
 }, 5000);
 
+onMounted(() => {
+  getRobotDetail();
+});
+
 onUnmounted(() => {
   window.removeEventListener("resize", () => balanceChart.value.resize());
   clearInterval(timer.value);

+ 14 - 2
src/views/bot/manage/index.vue

@@ -99,7 +99,7 @@
           </template>
           <template v-slot:configs="{ row }">
             <lay-tooltip :content="row.configs || ''">
-              <div class="ellipsis-2" @click="handleUpdate(row)">{{ `${row.configs || ""}` }}</div>
+              <div class="ellipsis-2" @click="handleUpdate(row)"><span class="danger-color" v-if="apiList?.includes('/robot/setAuto') && row.isAuto == 1">[自动调参]</span>{{ `${row.configs || ""}` }}</div>
             </lay-tooltip>
           </template>
           <template v-slot:lastReportTime="{ row }">
@@ -123,6 +123,8 @@
                 @click="handleStatus([row.id], 'RESTART')"
               />
               <TableButton v-if="apiList?.includes('/robot/delete') && ['STOPPED', 'STOP_PENDING', 'ERROR'].includes(row.status)" text="删除" @click="handleDelete(row)" />
+
+              <TableButton v-if="apiList?.includes('/robot/setAuto')" text="调参" @click="handleAutomate(row)" />
             </lay-space>
           </template>
         </lay-table>
@@ -160,6 +162,7 @@
   <Update ref="updateRef" />
   <Copy ref="copyRef" />
   <Transfers ref="transfersRef" />
+  <Automate ref="automateRef" />
 </template>
 
 <script lang="ts" setup name="BotManage">
@@ -168,6 +171,7 @@ import BatchUpdate from "./components/BatchUpdate.vue";
 import Update from "./components/Update.vue";
 import Transfers from "./components/Transfers.vue";
 import Copy from "./components/Copy.vue";
+import Automate from "./components/Automate.vue";
 import TableButton from "@/components/TableButton.vue";
 import { timeConverts } from "@/utils/index";
 import { get_robot_list, delete_robot, set_robot_status, set_robot_reset_capital, update_robot_batch, survive_robot, restore_robot, get_user_list_all, get_exchange_list_all } from "@/api";
@@ -187,6 +191,7 @@ const batchUpdateRef = ref();
 const updateRef = ref();
 const copyRef = ref();
 const transfersRef = ref();
+const automateRef = ref();
 
 const apiList = ref(window.sessionStorage.getItem("_4L_API_LIST"));
 
@@ -231,7 +236,7 @@ const columns = ref([
   { title: "所属人", width: "80px", key: "userName", align: "center", ellipsisTooltip: true },
   {
     title: "操作",
-    width: "120px",
+    width: "160px",
     customSlot: "operator",
     key: "operator",
     ignoreExport: true,
@@ -441,6 +446,13 @@ const handleDelete = async (value: any) => {
     }
   });
 };
+
+// 自动运维
+const handleAutomate = async (value: any) => {
+  const result = await automateRef.value.show(value);
+  if (result) getPageInfo();
+};
+
 // 分页设置
 const handleCurrentChange = (val: any) => {
   pageParams.pageNum = val.current;

+ 20 - 4
src/views/exchange/apikey/index.vue

@@ -6,6 +6,7 @@
     <template v-slot:extra>
       <lay-button class="card-button" v-if="apiList?.includes('/userProof/save')" @click="handleBatch()">批量添加ApiKey</lay-button>
       <lay-button class="card-button" v-if="apiList?.includes('/userProof/save')" @click="handleUpdate()">添加ApiKey</lay-button>
+      <lay-button class="card-button" v-if="apiList?.includes('/userProof/delete')" @click="handleDelete(selectedKeys)">批量删除ApiKey</lay-button>
     </template>
 
     <template v-slot:body>
@@ -20,11 +21,20 @@
         </lay-form>
       </div>
       <div>
-        <lay-table :page="tablePage" :columns="columns" resize :data-source="dataSource" :loading="pageConfig.loading" @change="handleCurrentChange">
+        <lay-table
+          :page="tablePage"
+          :columns="columns"
+          resize
+          id="userProofId"
+          :data-source="dataSource"
+          v-model:selected-keys="selectedKeys"
+          :loading="pageConfig.loading"
+          @change="handleCurrentChange"
+        >
           <template v-slot:operator="{ row }">
             <lay-space>
               <TableButton v-if="apiList?.includes('/userProof/update')" text="编辑" @click="handleUpdate(row)" />
-              <TableButton v-if="apiList?.includes('/userProof/delete')" type="danger" text="删除" @click="handleDelete(row)" />
+              <TableButton v-if="apiList?.includes('/userProof/delete')" type="danger" text="删除" @click="handleDelete([row.userProofId])" />
             </lay-space>
           </template>
         </lay-table>
@@ -70,6 +80,7 @@ interface TablePage {
 }
 const tablePage: TablePage = reactive({ current: 1, limit: 20, total: 0, limits: [20, 50, 100, 200, 500] });
 const columns = ref([
+  { title: "选项", width: "44px", type: "checkbox" },
   { title: "名称", key: "name" },
   { title: "交易所", key: "exchangeName", ellipsisTooltip: true },
   { title: "API", key: "accessKey", ellipsisTooltip: true },
@@ -86,10 +97,14 @@ const columns = ref([
   },
 ]);
 let dataSource = ref([]);
+let selectedKeys = ref([]);
 
 // 请求ApiKey列表
 const getPageInfo = (isSearch?: boolean) => {
-  if (isSearch) pageParams.pageNum = 1;
+  if (isSearch) {
+    pageParams.pageNum = 1;
+    selectedKeys.value = [];
+  }
   pageConfig.loading = true;
   get_apikey_list(pageParams, (data: any) => {
     pageConfig.loading = false;
@@ -115,13 +130,14 @@ const handleBatch = async () => {
 const handleDelete = async (value: any) => {
   let result = await proxy.$waitingConfirm("是否确认删除该ApiKey?");
   if (!result) return;
-  let params = [value.userProofId];
+  let params = value;
   pageConfig.loading = true;
   delete_apikey(params, (data: any) => {
     pageConfig.loading = false;
     if (data.code == 200) {
       proxy.$message(`删除成功!`);
       getPageInfo();
+      selectedKeys.value = selectedKeys.value.filter((item) => !value.includes(item));
     }
   });
 };

+ 83 - 0
src/views/indicator/blacklists/components/Update.vue

@@ -0,0 +1,83 @@
+<template>
+  <lay-layer :title="modelConfig.title" v-model="modelConfig.visible" area="auto" :btn="operator">
+    <div class="width-500 custom-layer" style="padding: 20px">
+      <lay-form :model="modelParams" ref="modelFormRef" required>
+        <lay-form-item label="交易所" prop="exchange">
+          <lay-select v-model="modelParams.exchange" :show-search="true" allowClear>
+            <lay-select-option v-for="item of exchangeList" :value="item" :label="item" />
+          </lay-select>
+        </lay-form-item>
+        <lay-form-item label="币对" prop="coinPair">
+          <lay-input v-model="modelParams.coinPair" />
+        </lay-form-item>
+      </lay-form>
+    </div>
+  </lay-layer>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, getCurrentInstance } from "vue";
+import { add_symbols_black } from "@/api";
+
+const { proxy }: any = getCurrentInstance();
+
+const modelFormRef = ref();
+
+interface ModelConfig {
+  title: string;
+  visible: boolean;
+  isUpdate: boolean;
+  loading: boolean;
+}
+interface ModelParams {
+  coinPair?: string;
+  exchange?: Array<string>;
+}
+
+let modelParams = ref<ModelParams>({});
+let modelConfig: ModelConfig = reactive({ title: "", visible: false, isUpdate: false, loading: false });
+
+let handleResult = reactive<{ resolve?: any; reject?: any }>({});
+let exchangeList = ref<any>([]);
+
+const show = (exchanges: any) => {
+  modelConfig.visible = true;
+  modelConfig.title = "添加黑名单";
+  modelParams.value = {};
+  exchangeList.value = exchanges;
+  return new Promise(async (resolve, reject) => {
+    handleResult.resolve = resolve;
+    handleResult.reject = reject;
+  });
+};
+
+const operator = reactive([
+  {
+    text: "确认",
+    callback: () => {
+      modelFormRef.value.validate((isValidate: boolean) => {
+        if (isValidate) {
+          const params = { ...modelParams.value, coinPair: (modelParams.value.coinPair || "").toUpperCase() };
+          modelConfig.loading = true;
+          add_symbols_black(params, (data: any) => {
+            modelConfig.loading = false;
+            if (data.code == 200) {
+              proxy.$message("添加成功!");
+              modelConfig.visible = false;
+              handleResult.resolve(true);
+            }
+          });
+        }
+      });
+    },
+  },
+  {
+    text: "取消",
+    callback: () => {
+      modelConfig.visible = false;
+      handleResult.resolve(false);
+    },
+  },
+]);
+defineExpose({ show });
+</script>

+ 116 - 0
src/views/indicator/blacklists/index.vue

@@ -0,0 +1,116 @@
+<template>
+  <lay-card class="custom-card">
+    <template v-slot:title>
+      <span class="card-title">币对黑名单</span>
+    </template>
+    <template v-slot:extra>
+      <lay-button class="card-button" @click="handleUpdate()">添加币对</lay-button>
+    </template>
+
+    <template v-slot:body>
+      <div class="custom-form-layout">
+        <lay-form class="form-wp" :model="pageParams" mode="inline" size="sm">
+          <lay-form-item label="交易所" prop="exchange">
+            <lay-select v-model="pageParams.exchange" :show-search="true" allowClear>
+              <lay-select-option v-for="item of rkExchanges" :value="item" :label="item" />
+            </lay-select>
+          </lay-form-item>
+          <div class="form-button-wp">
+            <lay-button @click="getPageInfo()">搜索</lay-button>
+          </div>
+        </lay-form>
+      </div>
+      <div>
+        <lay-table :columns="columns" resize :data-source="dataSource" :loading="pageConfig.loading">
+          <template v-slot:operator="{ row }">
+            <lay-space>
+              <TableButton type="danger" text="删除" @click="handleDelete(row)" />
+            </lay-space>
+          </template>
+        </lay-table>
+      </div>
+    </template>
+  </lay-card>
+  <Update ref="updateRef" />
+</template>
+
+<script lang="ts" setup name="QuantManage">
+import { ref, reactive, getCurrentInstance } from "vue";
+import Update from "./components/Update.vue";
+import TableButton from "@/components/TableButton.vue";
+import { get_symbols_black, get_exchange_rank, del_symbols_black } from "@/api";
+
+const { proxy }: any = getCurrentInstance();
+const updateRef = ref();
+
+interface PageConfig {
+  loading: boolean;
+}
+
+let pageConfig: PageConfig = reactive({
+  loading: false,
+});
+
+interface FormItem {
+  exchange?: String;
+}
+const pageParams: FormItem = reactive({});
+
+const columns = ref([
+  { title: "币对", key: "coinPair" },
+  { title: "交易所", key: "exchange", ellipsisTooltip: true },
+  {
+    title: "操作",
+    width: 160,
+    customSlot: "operator",
+    key: "operator",
+    ignoreExport: true,
+  },
+]);
+let dataSource = ref<any>([]);
+let rkExchanges = ref<any>([]);
+
+const getRkExchanges = () => {
+  const params = {};
+  pageConfig.loading = true;
+  get_exchange_rank(params, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      rkExchanges.value = data.data;
+    }
+  });
+};
+getRkExchanges();
+
+// 请求策略列表
+const getPageInfo = () => {
+  pageConfig.loading = true;
+  get_symbols_black(pageParams, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      dataSource.value = data.data;
+    }
+  });
+};
+getPageInfo();
+
+const handleUpdate = async () => {
+  const result = await updateRef.value.show(rkExchanges.value);
+  if (result) getPageInfo();
+};
+
+// 移除币对
+const handleDelete = async (value: any) => {
+  let result = await proxy.$waitingConfirm("是否确认移除此币对?");
+  if (!result) return;
+  let params = value;
+  pageConfig.loading = true;
+  del_symbols_black(params, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      proxy.$message(`移除成功!`);
+      getPageInfo();
+    }
+  });
+};
+</script>

+ 134 - 38
src/views/indicator/msv/index.vue

@@ -79,7 +79,7 @@ let router_query: any = reactive({});
 watch(
   route,
   (value) => {
-    if (value.name == "ServerCommand") return;
+    if (value.name != "IndicatorMsv") return;
     if (value.query.symbol == router_query.symbol && value.query.exchange == router_query.exchange && value.query.minute_time_range == router_query.minute_time_range) return;
     router_query = { ...value.query };
     pageParams.symbol = value.query.symbol?.toString() || "BTC_USDT";
@@ -127,34 +127,76 @@ const initChart = (data: any) => {
 
   // const xData = data.x;
   const yData = data.msv;
-  const trYData = data.tr;
+  const liqsData = data.liqs;
+  const eprsData = data.eprs;
+  const sigmasData = data.sigmas;
+  const sigmasMasData = data.sigma_mas;
   const chartOption = {
-    title: {
-      text: `${pageParams.exchange} ${pageParams.symbol?.toUpperCase()} ${parseFloat(((Number(pageParams.minute_time_range) || 240) / 60).toFixed(2)).toString()}小时波动率指标`,
-    },
+    title: [
+      {
+        text: `${pageParams.exchange} ${pageParams.symbol?.toUpperCase()} ${parseFloat(((Number(pageParams.minute_time_range) || 240) / 60).toFixed(2)).toString()}小时波动幅度指标`,
+        textStyle: {
+          fontSize: 20,
+        },
+      },
+      {
+        text: "预估利润",
+        top: "280px",
+        textStyle: {
+          fontSize: 14,
+        },
+      },
+      {
+        text: "行情推动量",
+        top: "440px",
+        textStyle: {
+          fontSize: 14,
+        },
+      },
+      {
+        text: "波动率",
+        top: "600px",
+        textStyle: {
+          fontSize: 14,
+        },
+      },
+    ],
     grid: [
       {
-        top: "80px",
+        top: "50px",
+        left: "60px",
+        right: "60px",
+        height: "25%", // 主图高度
+        backgroundColor: "red",
+      },
+      {
         left: "60px",
         right: "60px",
-        height: "50%", // 主图高度
+        bottom: "390px",
+        height: "12%", // 副图高度
       },
       {
         left: "60px",
         right: "60px",
-        bottom: "100px",
-        height: "5%", // 副图高度
+        bottom: "230px",
+        height: "12%", // 副图高度
+      },
+      {
+        left: "60px",
+        right: "60px",
+        bottom: "70px",
+        height: "12%", // 副图高度
       },
     ],
     dataZoom: [
       {
         type: "inside",
-        xAxisIndex: [0, 1],
+        xAxisIndex: [0, 1, 2, 3],
         start: 0,
         end: 100,
       },
       {
-        xAxisIndex: [0, 1],
+        xAxisIndex: [0, 1, 2, 3],
         start: 0,
         end: 100,
       },
@@ -163,27 +205,29 @@ const initChart = (data: any) => {
       trigger: "axis",
       axisPointer: {
         type: "cross",
-        lineStyle: {
-          type: "dashed",
-          width: 1,
-        },
       },
       formatter: (params: any) => {
         if (params.length === 0) return "";
         let result = "";
-        params.forEach((param: any) => {
-          let time = dayjs(param.name * 1).format("YYYY-MM-DD HH:mm:ss.SSS");
-          if (param.seriesIndex == 0) {
-            // 主图的 tooltip
-            let volatilities = param.value[1];
-            let dissociation = unitConverts(param.value[2]);
-            result = `时间:${time}<br/>波动率:${volatilities}%<br/>强度:${dissociation} USDT<br/>`;
-          } else if (param.seriesIndex == 1) {
-            // 副图的 tooltip
-            result = `时间:${time}<br/>交易速率:${param.value[1]}<br/>`;
-          }
-        });
-
+        let param = params[0];
+        let time = dayjs(param.name * 1).format("YYYY-MM-DD HH:mm:ss.SSS");
+        if (param.seriesIndex == 0) {
+          // 主图的 tooltip
+          let volatilities = param.value[1];
+          let dissociation = unitConverts(param.value[2]);
+          result = `时间:${time}<br/>波动幅度:${volatilities}%<br/>强度:${dissociation} USDT<br/>`;
+        } else if (param.seriesIndex == 1) {
+          // 副图的 tooltip
+          result = `时间:${time}<br/>预期利润:${param.value[1]}%<br/>`;
+        } else if (param.seriesIndex == 2) {
+          // 副图的 tooltip
+          result = `时间:${time}<br/>行情推动量:${param.value[1]}K<br/>`;
+        } else if (param.seriesIndex == 3) {
+          let sigma = params[0]?.value[1] || 0;
+          let sigmaMas = params[1]?.value[1];
+          // 副图的 tooltip
+          result = `时间:${time}<br/>sigma:${sigma}${sigmaMas ? "<br/>Ma(sigma, 100):" + sigmaMas + "<br/>" : ""}`;
+        }
         return result;
       },
     },
@@ -208,6 +252,26 @@ const initChart = (data: any) => {
           },
         },
       },
+      {
+        gridIndex: 2, // 第二个网格的 y 轴
+        type: "category",
+        show: true,
+        axisLabel: {
+          formatter: (value: any) => {
+            return dayjs(value * 1).format("MM-DD HH:mm:ss");
+          },
+        },
+      },
+      {
+        gridIndex: 3, // 第二个网格的 y 轴
+        type: "category",
+        show: true,
+        axisLabel: {
+          formatter: (value: any) => {
+            return dayjs(value * 1).format("MM-DD HH:mm:ss");
+          },
+        },
+      },
     ],
     yAxis: [
       {
@@ -217,12 +281,14 @@ const initChart = (data: any) => {
         gridIndex: 1,
         type: "value",
         position: "left",
-        splitLine: {
-          show: false,
-        },
-        axisLabel: {
-          formatter: () => "",
-        },
+      },
+      {
+        gridIndex: 2,
+        type: "value",
+      },
+      {
+        gridIndex: 3,
+        type: "value",
       },
     ],
     series: [
@@ -247,7 +313,6 @@ const initChart = (data: any) => {
         data: yData,
       },
       {
-        name: "价格",
         type: "line",
         sampling: "lttb",
         xAxisIndex: 1,
@@ -255,7 +320,38 @@ const initChart = (data: any) => {
         itemStyle: {
           color: "rgb(55, 162, 255)",
         },
-        data: trYData,
+        data: eprsData,
+      },
+      {
+        type: "line",
+        sampling: "lttb",
+        xAxisIndex: 2,
+        yAxisIndex: 2,
+        itemStyle: {
+          color: "rgb(55, 162, 255)",
+        },
+        data: liqsData,
+      },
+      {
+        type: "line",
+        sampling: "lttb",
+        showSymbol: false,
+        name: "123",
+        xAxisIndex: 3,
+        yAxisIndex: 3,
+        itemStyle: {
+          color: "rgb(55, 162, 255)",
+        },
+        data: sigmasData,
+      },
+      {
+        type: "line",
+        sampling: "lttb",
+        showSymbol: false,
+        name: "123",
+        xAxisIndex: 3,
+        yAxisIndex: 3,
+        data: sigmasMasData,
       },
     ],
   };
@@ -281,7 +377,7 @@ const initChart = (data: any) => {
 };
 
 onMounted(() => {
-  getMsvData();
+  if (route.query.symbol) getMsvData();
 });
 onUnmounted(() => {
   window.removeEventListener("resize", () => msvChart.value.resize());
@@ -294,7 +390,7 @@ onUnmounted(() => {
 }
 .chart {
   padding-top: 20px;
-  min-height: 500px;
+  min-height: 800px;
 }
 .robot-info-header {
   background-color: white;

+ 1 - 1
src/views/indicator/sybmol_filter/index.vue → src/views/indicator/symbol_filter/index.vue

@@ -92,7 +92,7 @@
   </lay-card>
 </template>
 
-<script lang="ts" setup name="IndicatorSybmolFilter">
+<script lang="ts" setup name="IndicatorSymbolFilter">
 import { ref, reactive } from "vue";
 import { useRouter } from "vue-router";
 import TableButton from "@/components/TableButton.vue";

+ 226 - 0
src/views/indicator/symbol_rank/index.vue

@@ -0,0 +1,226 @@
+<template>
+  <lay-card class="custom-card">
+    <template v-slot:title>
+      <span class="card-title">币对分数排名</span>
+    </template>
+    <template v-slot:body>
+      <div class="custom-form-layout">
+        <lay-form class="form-wp" :model="pageParams" mode="inline" size="sm">
+          <lay-form-item label="盘口" prop="exchange">
+            <lay-select v-model="pageParams.exchange" :show-search="true">
+              <lay-select-option v-for="item of rkExchanges" :value="item" :label="item" />
+            </lay-select>
+          </lay-form-item>
+          <lay-form-item label="波动分数≥" prop="msvScore">
+            <lay-input v-model="pageParams.msvScore" />
+          </lay-form-item>
+          <lay-form-item label="交易量分数≥" prop="liquidityScore">
+            <lay-input v-model="pageParams.liquidityScore" />
+          </lay-form-item>
+          <lay-form-item label="交易频次分数≥" prop="frequencyScore">
+            <lay-input v-model="pageParams.frequencyScore" />
+          </lay-form-item>
+          <lay-form-item label="总评分≥" prop="sumScore">
+            <lay-input v-model="pageParams.sumScore" />
+          </lay-form-item>
+          <div class="form-button-wp">
+            <lay-button @click="getPageInfo()">搜索</lay-button>
+          </div>
+        </lay-form>
+      </div>
+      <div>
+        <lay-table :page="tablePage" size="sm" :columns="columns" resize :data-source="dataShowSource" :loading="pageConfig.loading" @change="handleCurrentChange" @sortChange="handleSortChange">
+          <template v-slot:msvScore="{ row }">
+            <span class="primary-color">{{ row.msv_score }}</span>
+          </template>
+          <template v-slot:liquidityScore="{ row }">
+            <span class="normal-color">{{ row.liquidity_score }}</span>
+          </template>
+          <template v-slot:frequencyScore="{ row }">
+            <span class="warm-color">{{ row.frequency_score }}</span>
+          </template>
+          <template v-slot:score="{ row }">
+            <span class="danger-color">{{ row.score }}</span>
+          </template>
+          <template v-slot:operator="{ row }">
+            <div>
+              <TableButton text="查看MSV" @click="toJump(row)" />
+            </div>
+          </template>
+        </lay-table>
+      </div>
+    </template>
+  </lay-card>
+</template>
+
+<script lang="ts" setup name="IndicatorSymbolRank">
+import { ref, reactive } from "vue";
+import { useRouter } from "vue-router";
+import TableButton from "@/components/TableButton.vue";
+import { get_exchange_rank, get_symbols_rank } from "@/api";
+
+const router = useRouter();
+
+interface PageConfig {
+  loading: boolean;
+}
+
+let pageConfig: PageConfig = reactive({
+  loading: false,
+});
+
+interface FormItem {
+  exchange?: string;
+  msvScore?: string;
+  liquidityScore?: string;
+  frequencyScore?: string;
+  sumScore?: string;
+}
+const pageParams: FormItem = reactive({ exchange: "gate_usdt_swap" });
+
+interface TablePage {
+  current: number;
+  limit: number;
+  total: number;
+}
+const tablePage: TablePage = reactive({ current: 1, limit: 20, total: 0, limits: [20, 50, 100, 200, 500] });
+const columns = ref([
+  { title: "币对", width: "110", key: "symbol", ellipsisTooltip: true },
+  { title: "最大Abs(波动率)", key: "msv_abs_max" },
+  { title: "平均Abs(波动率)", key: "msv_abs_avg" },
+  { title: "有效波动次数", key: "effective_count" },
+  { title: "平均行情推动量", key: "liquidity_avg" },
+  { title: "Sum(Abs(波动率))", key: "msv_abs_total" },
+  { title: "Sum(预期利润)", key: "epr_total" },
+  { title: "波动分数", width: "80", key: "msv_score", customSlot: "msvScore", ellipsisTooltip: true },
+  { title: "交易量分数", width: "100", key: "liquidity_score", customSlot: "liquidityScore" },
+  { title: "交易频次分数", width: "100", key: "frequency_score", customSlot: "frequencyScore" },
+  { title: "总评分", width: "80", key: "score", customSlot: "score", sort: "desc" },
+  { title: "开仓值", width: "90", key: "coverted_open_base", ellipsisTooltip: true },
+  { title: "操作", width: "90", key: "operator", customSlot: "operator" },
+]);
+let dataSource = ref<any>([]);
+let dataShowSource = ref<any>([]);
+let dataSortSource = ref<any>([]);
+let sortInfo = ref<any>({});
+
+let rkExchanges = ref<any>([]);
+
+const getRkExchanges = () => {
+  const params = {};
+  pageConfig.loading = true;
+  get_exchange_rank(params, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      rkExchanges.value = data.data;
+    }
+  });
+};
+getRkExchanges();
+
+// 请求交易所列表
+const getPageInfo = () => {
+  tablePage.current = 1;
+  tablePage.total = 0;
+  dataSource.value = [];
+  const params = {
+    exchange: pageParams.exchange,
+  };
+  pageConfig.loading = true;
+  get_symbols_rank(params, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      let source = data.data.filter((item: any) => {
+        let msvScore = parseFloat(pageParams.msvScore || "0");
+        let liquidityScore = parseFloat(pageParams.liquidityScore || "0");
+        let frequencyScore = parseFloat(pageParams.frequencyScore || "0");
+        let sumScore = parseFloat(pageParams.sumScore || "0");
+        return item.msv_score >= msvScore && item.liquidity_score >= liquidityScore && item.frequency_score >= frequencyScore && item.score >= sumScore;
+      });
+      tablePage.total = source.length;
+      dataSource.value = source;
+      handleSortChange(sortInfo.value.key, sortInfo.value.sort);
+    }
+  });
+};
+getPageInfo();
+
+const toJump = (value: any) => {
+  router.push({ path: "/indicator/msv", query: { symbol: value.symbol, exchange: pageParams.exchange, minute_time_range: 70 } });
+};
+
+// 分页设置
+const handleCurrentChange = (val: any) => {
+  dataShowSource.value = [...dataSortSource.value.slice((val.current - 1) * val.limit, val.current * val.limit)];
+};
+
+// 排序
+const handleSortChange = (key: any, sort: any) => {
+  sortInfo.value = { key, sort };
+  if (!sort) {
+    dataSortSource.value = [...dataSource.value];
+    dataShowSource.value = [...dataSource.value.slice((tablePage.current - 1) * tablePage.limit, tablePage.current * tablePage.limit)];
+    return;
+  }
+  dataSortSource.value = [...dataSource.value].sort((a: any, b: any) => {
+    let maxA = -9999999;
+    let maxB = -9999999;
+    Object.values(a[key]).map((item) => (maxA = Number(item) > maxA ? Number(item) : maxA));
+    Object.values(b[key]).map((item) => (maxB = Number(item) > maxB ? Number(item) : maxB));
+    return maxA - maxB;
+  });
+  if (sort == "desc") dataSortSource.value.reverse();
+  dataShowSource.value = [...dataSortSource.value.slice((tablePage.current - 1) * tablePage.limit, tablePage.current * tablePage.limit)];
+};
+</script>
+
+<style lang="scss" scoped>
+.custom-form-layout {
+  .custom-card-checkbox,
+  .custom-checkbox {
+    display: inline-flex;
+    align-items: center;
+    margin-bottom: 16px;
+    padding-right: 20px;
+    .label {
+      display: flex;
+      padding-right: 15px;
+    }
+    .checkbox-group {
+      display: flex;
+      .checkbox-wp {
+        display: flex;
+        align-items: center;
+        margin-right: 10px;
+        line-height: 38px;
+        :deep(.layui-checkbox-label) {
+          padding: 0;
+        }
+        :deep(.layui-form-radio) {
+          margin-top: 0;
+        }
+        :deep(.layui-checkcard) {
+          padding: 0 10px;
+          width: auto;
+          margin: 0 10px 0 0;
+          .layui-checkcard-content {
+            padding: 0;
+          }
+        }
+        .checkbox-input {
+          width: 40px;
+        }
+        &:last-child {
+          margin-right: 0;
+        }
+      }
+    }
+  }
+  .custom-card-checkbox {
+    .checkbox-wp {
+      padding: 0 20px;
+      border: 1px solid #d9d9d9;
+    }
+  }
+}
+</style>

+ 311 - 0
src/views/statistic/balance_arbitrage/index.vue

@@ -0,0 +1,311 @@
+<template>
+  <lay-card class="custom-card">
+    <template v-slot:title>
+      <span class="card-title">资金曲线-套利</span>
+    </template>
+    <template v-slot:body>
+      <lay-loading class="custom-loading" :loading="pageConfig.loading">
+        <div class="custom-form-layout">
+          <lay-form class="form-wp" :model="pageParams" mode="inline" size="sm">
+            <lay-form-item label="查询时间" prop="rangeTime">
+              <lay-date-picker v-model="pageParams.rangeTime" range type="datetime" :placeholder="['开始日期', '结束日期']" />
+            </lay-form-item>
+            <div class="form-button-wp">
+              <lay-button @click="getUserBalanceData()">查询</lay-button>
+            </div>
+            <lay-form-item>
+              <lay-button-group>
+                <lay-button type="default" @click="handleRangeTime(1)">本日</lay-button>
+                <lay-button type="default" @click="handleRangeTime(2)">本周</lay-button>
+                <lay-button type="default" @click="handleRangeTime(3)">本月</lay-button>
+                <lay-button type="default" @click="handleRangeTime(4)">上月</lay-button>
+              </lay-button-group>
+            </lay-form-item>
+            <lay-form-item>
+              <lay-button-group>
+                <lay-button type="default" @click="initChart(balanceData, 1)">按收益率</lay-button>
+                <lay-button type="default" @click="initChart(balanceData, 2)">按收益</lay-button>
+                <lay-button type="default" @click="initChart(balanceData, 3)">按资金</lay-button>
+              </lay-button-group>
+            </lay-form-item>
+          </lay-form>
+        </div>
+        <div>
+          <div class="chart" ref="balanceChartRef"></div>
+        </div>
+      </lay-loading>
+    </template>
+  </lay-card>
+</template>
+
+<script lang="ts" setup name="StatisticBalanceArbitrage">
+import { ref, reactive, onUnmounted, shallowRef } from "vue";
+import * as echarts from "echarts";
+import dayjs from "dayjs";
+import isoWeek from "dayjs/plugin/isoWeek";
+import Decimal from "decimal.js";
+import { get_arbitrage_balance } from "@/api";
+
+dayjs.extend(isoWeek);
+
+const balanceChartRef = ref();
+interface PageConfig {
+  loading: boolean;
+}
+
+interface FormItem {
+  rangeTime: Array<string>;
+}
+
+let pageConfig: PageConfig = reactive({
+  loading: false,
+});
+
+const pageParams: FormItem = reactive({ rangeTime: [dayjs().subtract(24, "hour").format("YYYY-MM-DD HH:mm:ss"), dayjs().format("YYYY-MM-DD HH:mm:ss")] });
+
+let balanceChart = shallowRef();
+let balanceData = ref();
+let earningsRateData = ref<Array<any>>([]);
+let earningsData = ref<Array<any>>([]);
+let totalBalanceData = ref<Array<any>>([]);
+
+const getUserBalanceData = () => {
+  balanceData.value = [];
+  earningsRateData.value = [];
+  earningsData.value = [];
+  totalBalanceData.value = [];
+  const params = {
+    startTime: +dayjs(pageParams.rangeTime[0]),
+    endTime: +dayjs(pageParams.rangeTime[1]),
+  };
+  pageConfig.loading = true;
+  get_arbitrage_balance(params, async (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      balanceData.value = data.data;
+      await handleBalanceData(data.data);
+      initChart(balanceData.value, 1);
+    }
+  });
+};
+getUserBalanceData();
+
+const handleEarningsData = (data: any) => {
+  let transferBalance = new Decimal(0);
+  let earningsList = data.map((item: any, index: any) => {
+    if (index == 0) return 0;
+    if (item.label !== 0) transferBalance = transferBalance.add(item.changeU);
+    return new Decimal(item.afterU || 0).minus(data[0].afterU).minus(transferBalance).toNumber();
+  });
+  return earningsList;
+};
+const handleEarningsRateData = (data: any) => {
+  let transferBalance = data[0].afterU;
+  let transferEarningsRate: any = 0;
+  let lastEarningsRate: any = 0;
+  let earningsList = data.map((item: any, index: any) => {
+    if (index == 0) return 0;
+    if (item.label !== 0) {
+      transferBalance = item.afterU;
+      transferEarningsRate = lastEarningsRate;
+      return transferEarningsRate
+    };
+    let earningsRate = new Decimal(item.afterU || 0).minus(transferBalance).div(transferBalance).mul(100).add(transferEarningsRate).toNumber();
+    lastEarningsRate = earningsRate
+    return earningsRate;
+  });
+  return earningsList;
+};
+const handleTotalBalanceData = (data: any) => {
+  return data.map((item: any) => item.afterU);
+};
+
+const handleBalanceData = (data: any) => {
+  return new Promise((resolve: any) => {
+    // 过滤无效数据
+    balanceData.value = { ...data, userList: data.botList.filter((item: any) => item.balance.length > 0) };
+    balanceData.value.botList.map((item: any) => {
+      let earningList: any = handleEarningsData(item.balance);
+      let earningRateList: any = handleEarningsRateData(item.balance);
+      let totalList: any = handleTotalBalanceData(item.balance);
+      earningsData.value.push({ name: item.name, type: "line", stack: item.name, data: earningList });
+      earningsRateData.value.push({ name: item.name, type: "line", stack: item.name, data: earningRateList });
+      totalBalanceData.value.push({ name: item.name, type: "line", stack: item.name, data: totalList });
+    });
+    resolve(true);
+  });
+};
+
+const handleRangeTime = (type: number) => {
+  const currentTime = +dayjs();
+  const diffTimestamp = +dayjs(currentTime).hour(10).minute(0).second(0).millisecond(0) - currentTime;
+  let timestamp = diffTimestamp <= 0 ? currentTime : currentTime - 1000 * 60 * 60 * 10;
+
+  switch (type) {
+    case 1:
+      pageParams.rangeTime = [`${dayjs(timestamp).hour(10).minute(0).second(0).millisecond(0)}`, `${dayjs()}`];
+      break;
+    case 2:
+      pageParams.rangeTime = [`${dayjs(timestamp).isoWeekday(1).hour(10).minute(0).second(0).millisecond(0)}`, `${dayjs()}`];
+      break;
+    case 3:
+      pageParams.rangeTime = [`${dayjs(timestamp).startOf("month").hour(10).minute(0).second(0).millisecond(0)}`, `${dayjs()}`];
+      break;
+    case 4:
+      let upMonthTime = +dayjs(timestamp).startOf("month") - 1;
+      pageParams.rangeTime = [`${dayjs(upMonthTime).startOf("month").hour(10).minute(0).second(0).millisecond(0)}`, `${dayjs().startOf("month").hour(10).minute(0).second(0).millisecond(0)}`];
+      break;
+  }
+  getUserBalanceData();
+};
+
+const initChart = (data: any, type: number) => {
+  if (balanceChart.value != null && !balanceChart.value.isDisposed()) echarts.dispose(balanceChart.value);
+  balanceChart.value = echarts.init(balanceChartRef.value);
+
+  window.removeEventListener("resize", () => balanceChart.value.resize());
+  window.addEventListener("resize", () => balanceChart.value.resize());
+
+  const xData = data.timeList.map((item: any) => dayjs(item * 1).format("MM-DD HH:mm"));
+  const legendData = data.userList.map((item: any) => item.name);
+  if (type == 1) {
+    const earningsRateOption = {
+      tooltip: {
+        trigger: "axis",
+        formatter: (value: any) => {
+          const axisLabel = `${value[0].axisValueLabel}`;
+          const info = value.map((item: any) => `${item.marker}${item.seriesName}: ${item.data.toFixed(2)}%`);
+          return `${axisLabel}<br/>${info.join("<br/>")}`;
+        },
+      },
+      toolbox: {
+        feature: {
+          dataZoom: {},
+          brush: {
+            type: ["rect", "clear"],
+          },
+        },
+      },
+      legend: {
+        data: legendData,
+        top: "bottom",
+      },
+      xAxis: {
+        type: "category",
+        data: xData,
+      },
+      yAxis: {
+        type: "value",
+        min: "dataMin",
+        axisLabel: {
+          formatter: (value: any) => {
+            return `${value.toFixed(2)}%`;
+          },
+        },
+      },
+      series: earningsRateData.value,
+    };
+    balanceChart.value.setOption(earningsRateOption);
+  }
+  if (type == 2) {
+    const earningsOption = {
+      tooltip: {
+        trigger: "axis",
+        formatter: (value: any) => {
+          const axisLabel = `${value[0].axisValueLabel}`;
+          const info = value.map((item: any) => `${item.marker}${item.seriesName}: ${item.data}`);
+          return `${axisLabel}<br/>${info.join("<br/>")}`;
+        },
+      },
+      toolbox: {
+        feature: {
+          dataZoom: {},
+          brush: {
+            type: ["rect", "clear"],
+          },
+        },
+      },
+      legend: {
+        data: legendData,
+        top: "bottom",
+      },
+      xAxis: {
+        type: "category",
+        data: xData,
+      },
+      yAxis: {
+        type: "value",
+        min: "dataMin",
+      },
+      series: earningsData.value,
+    };
+    balanceChart.value.setOption(earningsOption);
+  }
+  if (type == 3) {
+    const earningsOption = {
+      tooltip: {
+        trigger: "axis",
+        formatter: (value: any) => {
+          const axisLabel = `${value[0].axisValueLabel}`;
+          const info = value.map((item: any) => `${item.marker}${item.seriesName}: ${item.data}`);
+          return `${axisLabel}<br/>${info.join("<br/>")}`;
+        },
+      },
+      toolbox: {
+        feature: {
+          dataZoom: {},
+          brush: {
+            type: ["rect", "clear"],
+          },
+        },
+      },
+      legend: {
+        data: legendData,
+        top: "bottom",
+      },
+      xAxis: {
+        type: "category",
+        data: xData,
+      },
+      yAxis: {
+        type: "value",
+        min: "dataMin",
+      },
+      series: totalBalanceData.value,
+    };
+    balanceChart.value.setOption(earningsOption);
+  }
+};
+
+onUnmounted(() => {
+  window.removeEventListener("resize", () => balanceChart.value.resize());
+});
+</script>
+
+<style lang="scss" scoped>
+.custom-card {
+  min-width: 256px;
+  .form-button-wp {
+    margin-right: 60px;
+  }
+}
+.chart {
+  padding-top: 20px;
+  min-height: 500px;
+}
+.robot-info-header {
+  background-color: white;
+  padding: 16px 24px;
+  margin-bottom: 20px;
+  .robot-name {
+    font-size: 14px;
+    font-weight: bold;
+  }
+  .robot-status {
+    span {
+      font-size: 12px;
+      padding-left: 4px;
+    }
+  }
+}
+</style>

+ 34 - 19
src/views/statistic/balance_user/index.vue

@@ -95,32 +95,47 @@ const getUserBalanceData = () => {
 };
 getUserBalanceData();
 
+const handleEarningsData = (data: any) => {
+  let transferBalance = new Decimal(0);
+  let earningsList = data.map((item: any, index: any) => {
+    if (index == 0) return 0;
+    if (item.label !== 0) transferBalance = transferBalance.add(item.changeU);
+    return new Decimal(item.afterU || 0).minus(data[0].afterU).minus(transferBalance).toNumber();
+  });
+  return earningsList;
+};
+const handleEarningsRateData = (data: any) => {
+  let transferBalance = data[0].afterU;
+  let transferEarningsRate: any = 0;
+  let lastEarningsRate: any = 0;
+  let earningsList = data.map((item: any, index: any) => {
+    if (index == 0) return 0;
+    if (item.label !== 0) {
+      transferBalance = item.afterU;
+      transferEarningsRate = lastEarningsRate;
+      return transferEarningsRate
+    };
+    let earningsRate = new Decimal(item.afterU || 0).minus(transferBalance).div(transferBalance).mul(100).add(transferEarningsRate).toNumber();
+    lastEarningsRate = earningsRate
+    return earningsRate;
+  });
+  return earningsList;
+};
+const handleTotalBalanceData = (data: any) => {
+  return data.map((item: any) => item.afterU);
+};
+
 const handleBalanceData = (data: any) => {
   return new Promise((resolve: any) => {
     // 过滤无效数据
     balanceData.value = { ...data, userList: data.userList.filter((item: any) => item.balance.length > 0) };
     balanceData.value.userList.map((item: any) => {
-      let earningList: any = [];
-      let earningRateList: any = [];
-      let lastBalance = 0;
-      let transferBalance = 0;
-      item.balance.map((items: any) => {
-        const isTransfer = new Decimal(items || 0)
-          .minus(lastBalance || 0)
-          .mod(50)
-          .toNumber();
-        if (!isTransfer && items != lastBalance) transferBalance += items - lastBalance;
-        lastBalance = items;
-        let totalBalance = item.balance[0] + transferBalance;
-        let earning = new Decimal(items || 0).minus(totalBalance || 0);
-        let earningRate = earning.div(totalBalance || 1).mul(100);
-        earningList.push(earning.toNumber());
-        earningRateList.push(earningRate.toNumber());
-      });
-
+      let earningList: any = handleEarningsData(item.balance);
+      let earningRateList: any = handleEarningsRateData(item.balance);
+      let totalList: any = handleTotalBalanceData(item.balance);
       earningsData.value.push({ name: item.name, type: "line", stack: item.name, data: earningList });
       earningsRateData.value.push({ name: item.name, type: "line", stack: item.name, data: earningRateList });
-      totalBalanceData.value.push({ name: item.name, type: "line", stack: item.name, data: item.balance });
+      totalBalanceData.value.push({ name: item.name, type: "line", stack: item.name, data: totalList });
     });
     resolve(true);
   });