Selaa lähdekoodia

1.新增实时币对排行页面
2.添加套利机器人信息
3.修改机器人表格展示问题

DESKTOP-NE65RNK\Citrus_limon 1 vuosi sitten
vanhempi
commit
681c8ebe66

+ 33 - 0
src/api/index.ts

@@ -150,6 +150,26 @@ export const set_robot_auto = (params: any, callback: any) => {
   });
 };
 
+// 套利机器人
+// 机器人管理-套利列表
+export const get_arbitrage_robot_list = (params: any, callback: any) => {
+  return http.request("/api/robot/getBotPage", "get", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};
+// 机器人管理-套利详情
+export const get_arbitrage_robot_detail = (params: any, callback: any) => {
+  return http.request("/api/robot/getBotDetail", "get", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};
+// 机器人管理-套利详情
+export const get_arbitrage_robot_detail_balance = (params: any, callback: any) => {
+  return http.request("/api/robot/getBotBalance", "get", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};
+
 // 策略管理
 // 策略管理-策略列表
 export const get_strategy_list = (params: any, callback: any) => {
@@ -455,6 +475,12 @@ export const get_symbols_rank = (params: any, callback: any) => {
     if (data) callback && callback(data);
   });
 };
+// 情报中心-币对排行列表(实时)
+export const get_symbols_rank_real = (params: any, callback: any) => {
+  return http.request("/rk/get_symbols_rank_real", "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) => {
@@ -645,3 +671,10 @@ export const set_acquire_label = (params: any, callback: any) => {
     if (data) callback && callback(data);
   });
 };
+
+// as视图
+export const get_predictor_state = (params: any, callback: any) => {
+  return http.request("/predictor_state", "get", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};

+ 18 - 0
src/router/routes.ts

@@ -34,6 +34,12 @@ const routes: Array<RouteRecordRaw> = [
         component: () => import("@/views/bot/manage/index.vue"),
         meta: { title: "机器人管理", keepAlive: true },
       },
+      {
+        path: "/bot/arbitrage",
+        name: "BotArbitrage",
+        component: () => import("@/views/bot/arbitrage/index.vue"),
+        meta: { title: "套利机器人管理", keepAlive: true },
+      },
       {
         path: "/exchange/manage",
         name: "ExchangeManage",
@@ -82,6 +88,12 @@ const routes: Array<RouteRecordRaw> = [
         component: () => import("@/views/indicator/symbol_rank/index.vue"),
         meta: { title: "币对排行", keepAlive: true },
       },
+      {
+        path: "/indicator/symbol_rank_real",
+        name: "IndicatorSymbolRankReal",
+        component: () => import("@/views/indicator/symbol_rank_real/index.vue"),
+        meta: { title: "币对排行(实时)", keepAlive: true },
+      },
       {
         path: "/indicator/blacklists",
         name: "IndicatorBlacklists",
@@ -178,6 +190,12 @@ const routes: Array<RouteRecordRaw> = [
     component: () => import("@/views/bot/manage/detail.vue"),
     meta: { title: "机器人详情", keepAlive: false },
   },
+  {
+    path: "/bot/arbitrage/detail/:id",
+    name: "BotArbitrageDetail",
+    component: () => import("@/views/bot/arbitrage/detail.vue"),
+    meta: { title: "套利机器人详情", keepAlive: false },
+  },
 ];
 
 export default routes;

+ 261 - 0
src/views/bot/arbitrage/detail.vue

@@ -0,0 +1,261 @@
+<template>
+  <div class="container-wp">
+    <div class="robot-info-header">
+      <lay-space>
+        <span class="robot-name">{{ robotDetail.alias }}</span>
+        <lay-tag size="sm">
+          <span class="robot-status" v-if="robotDetail.status == '1'">
+            <lay-badge type="dot" theme="blue" ripple />
+            <span>运行中</span>
+          </span>
+          <span class="robot-status" v-else>
+            <lay-badge type="dot" theme="orange" ripple />
+            <span>已停止</span>
+          </span>
+        </lay-tag>
+      </lay-space>
+    </div>
+
+    <lay-card v-show="apiList?.includes('/remaining/list')" class="custom-card">
+      <template v-slot:title>
+        <span class="card-title">净值图</span>
+      </template>
+      <template v-slot:body>
+        <div class="profit-chart" ref="balanceChartRef"></div>
+      </template>
+    </lay-card>
+
+    <lay-card v-if="apiList?.includes('/robot/getRobotLog')" class="custom-card">
+      <template v-slot:title>
+        <span class="card-title">持仓信息</span>
+      </template>
+
+      <template v-slot:body>
+        <lay-table :columns="columns" size="sm" resize :data-source="positionList">
+          <template v-slot:spotBase="{ row }">
+            <span>{{ `${row.spot_base}/${row.spot_quote}` }}</span>
+          </template>
+          <template v-slot:swapBase="{ row }">
+            <span>{{ `${row.swap_base}/${row.swap_quote}` }}</span>
+          </template>
+          <template v-slot:openBaseDelta="{ row }">
+            <span>{{ row.config.config_payload.open_base_delta }}</span>
+          </template>
+          <template v-slot:closeDelta="{ row }">
+            <span>{{ row.config.config_payload.close_delta }}</span>
+          </template>
+          <template v-slot:openIncDelta="{ row }">
+            <span>{{ row.config.config_payload.open_inc_delta }}</span>
+          </template>
+          <template v-slot:openMaxValue="{ row }">
+            <span>{{ row.config.config_payload.open_max_value }}</span>
+          </template>
+          <template v-slot:positionMaxValue="{ row }">
+            <span>{{ row.config.config_payload.position_max_value }}</span>
+          </template>
+        </lay-table>
+      </template>
+    </lay-card>
+  </div>
+  <LogText ref="logtextRef" />
+</template>
+<script lang="ts" setup name="BotManageDetail">
+import { ref, reactive, onMounted, onUnmounted, shallowRef } from "vue";
+import { useRoute } from "vue-router";
+import * as echarts from "echarts";
+import { get_arbitrage_robot_detail, get_arbitrage_robot_detail_balance } from "@/api";
+
+const apiList = ref(window.sessionStorage.getItem("_4L_API_LIST"));
+
+const balanceChartRef = ref();
+const logtextRef = ref();
+
+const route = useRoute();
+
+interface PageConfig {
+  loading: boolean;
+}
+interface Logs {
+  time?: string;
+  text?: string;
+}
+
+let pageConfig: PageConfig = reactive({
+  loading: false,
+});
+
+const columns = ref([
+  { title: "现货交易对", key: "spot_base", customSlot: "spotBase", align: "center" },
+  { title: "合约交易对", key: "swap_base", customSlot: "swapBase", align: "center" },
+  { title: "现货数量", key: "spot_base_amount", align: "center" },
+  { title: "合约数量", key: "swap_base_amount", align: "center" },
+  { title: "开", key: "open_base_delta", customSlot: "openBaseDelta", align: "center" },
+  { title: "平", key: "close_delta", customSlot: "closeDelta", align: "center" },
+  { title: "阶梯", key: "open_inc_delta", customSlot: "openIncDelta", align: "center" },
+  { title: "阶梯容量", key: "open_max_value", customSlot: "openMaxValue", align: "center" },
+  { title: "最大持仓", key: "position_max_value", customSlot: "positionMaxValue", align: "center" },
+]);
+let positionList = ref<Array<Logs>>();
+let robotDetail = ref<any>({});
+let balanceList = ref([]);
+let balanceChart = shallowRef();
+// let timer = ref();
+
+// 获取机器人详情
+const getRobotDetail = () => {
+  const params = { id: route.params.id };
+  pageConfig.loading = true;
+  get_arbitrage_robot_detail(params, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      robotDetail.value = data.data;
+      document.title = data.data.alias;
+      positionList.value = data.data.position;
+      getBalanceInfo(data.data.bot_id);
+    }
+  });
+};
+const getBalanceInfo = (id: number) => {
+  const params = { botId: id, startTime: +new Date() - 7 * 24 * 60 * 60 * 1000, endTime: +new Date() };
+  pageConfig.loading = true;
+  get_arbitrage_robot_detail_balance(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.creationTime, item.afterU, item.changeU, item.pair, item.openNum, item.closeNum]);
+      const yMinData = Math.min(sData.map((item: any) => item[1]));
+
+      !balanceChart.value
+        ? initBalanceChart(data.data)
+        : balanceChart.value.setOption({
+            xAxis: {
+              type: "category",
+              boundaryGap: false,
+              data: xData,
+            },
+            yAxis: {
+              type: "value",
+              boundaryGap: [0, "100%"],
+              min: yMinData,
+              interval: 1,
+            },
+            series: {
+              name: "Balance",
+              type: "line",
+              areaStyle: {},
+              lineStyle: {
+                width: 1,
+              },
+              data: sData,
+            },
+          });
+    }
+  });
+};
+
+const initBalanceChart = (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.creationTime, item.afterU, item.changeU, item.pair, item.openNum, item.closeNum]);
+  const yMinData = Math.min(sData);
+
+  const balanceChartOption = {
+    tooltip: {
+      trigger: "axis",
+      axisPointer: {
+        type: "cross",
+      },
+      formatter: (params: any) => {
+        let info = params[0];
+        return `${info.marker}${info.seriesName}<br/>时间:${info.value[0]} <br/>余额:${info.value[1]}`;
+      },
+    },
+    dataZoom: [
+      {
+        start: 0,
+        end: 100,
+      },
+      {
+        type: "inside",
+        start: 0,
+        end: 100,
+      },
+    ],
+    xAxis: {
+      type: "category",
+      boundaryGap: false,
+      data: xData,
+    },
+    yAxis: {
+      type: "value",
+      boundaryGap: [0, "100%"],
+      min: yMinData,
+      interval: 1,
+    },
+    series: {
+      name: "Balance",
+      type: "line",
+      areaStyle: {},
+      data: sData,
+    },
+  };
+  balanceChart.value.setOption(balanceChartOption);
+};
+
+// timer.value = setInterval(() => {
+//   getBalanceInfo(robotDetail.value.bot_id);
+// }, 5000);
+
+onMounted(() => {
+  getRobotDetail();
+});
+
+onUnmounted(() => {
+  // window.removeEventListener("resize", () => balanceChart.value.resize());
+  // clearInterval(timer.value);
+});
+</script>
+
+<style lang="scss" scoped>
+.ellipsis-2 {
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+  line-break: anywhere;
+  -webkit-line-clamp: 2; /* 控制显示的行数 */
+}
+.container-wp {
+  min-height: 100%;
+  padding: 20px 40px;
+  background-color: rgb(244, 246, 247);
+}
+.profit-chart {
+  height: 300px;
+}
+.predictor-chart {
+  height: 1200px;
+}
+.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>

+ 182 - 0
src/views/bot/arbitrage/index.vue

@@ -0,0 +1,182 @@
+<template>
+  <lay-card class="custom-card">
+    <template v-slot:title>
+      <span class="card-title">套利机器人管理</span>
+    </template>
+
+    <template v-slot:body>
+      <div>
+        <lay-table :page="tablePage" :columns="columns" resize id="id" :data-source="dataSource" :loading="pageConfig.loading" @change="handleCurrentChange">
+          <template v-slot:alias="{ row }">
+            <span v-if="apiList?.includes('/robot/findById')" class="normal-color" @click="jumpDetail(row)">{{ row.alias }}</span>
+            <span v-else>{{ row.alias }}</span>
+          </template>
+          <template v-slot:status="{ row }">
+            <lay-space v-if="row.status == 0">
+              <lay-badge type="dot" theme="blue" ripple />
+              <span>运行中</span>
+            </lay-space>
+            <lay-space v-else>
+              <lay-badge type="dot" theme="orange" />
+              <span>已停止</span>
+            </lay-space>
+          </template>
+          <template v-slot:totolFounding="{ row }">
+            <span>{{ new Decimal(row.spot_founding).add(row.swap_founding) }}</span>
+          </template>
+
+          <template v-slot:maintainRate="{ row }">
+            <span>{{ row.maintain_rate * 100 }}%</span>
+          </template>
+          <template v-slot:liquidationRate="{ row }">
+            <span>{{ row.liquidation_rate * 100 }}%</span>
+          </template>
+          <template v-slot:profit="{ row }">
+            <span>
+              <span>{{ row.since_last_0_profit * 1 }}</span>
+              <span>(</span>
+              <span v-if="row.since_last_0_profit * 1 == 0">0%</span>
+              <span v-else :class="row.since_last_0_profit * 1 > 0 ? 'primary-color' : 'danger-color'">{{ `${countProfitability(row)}%` }}</span>
+              <span>)</span>
+            </span>
+          </template>
+          <template v-slot:flow="{ row }">
+            <span>{{ `${row.since_last_0_spot_flow * 1}/${row.since_last_0_swap_flow * 1}` }}</span>
+          </template>
+          <template v-slot:last_ping="{ row }">
+            <span :class="{ 'danger-color': timeConverts(row.last_ping * 1000).indexOf('秒') == -1 }">{{ row.last_ping ? timeConverts(row.last_ping * 1000) : "未通讯" }}</span>
+          </template>
+        </lay-table>
+      </div>
+    </template>
+  </lay-card>
+</template>
+
+<script lang="ts" setup name="BotManage">
+import { ref, reactive, onBeforeUnmount } from "vue";
+import { get_arbitrage_robot_list } from "@/api";
+import { timeConverts } from "@/utils";
+import Decimal from "decimal.js";
+
+const apiList = ref(window.sessionStorage.getItem("_4L_API_LIST"));
+
+interface PageConfig {
+  loading: boolean;
+}
+
+let pageConfig: PageConfig = reactive({
+  loading: false,
+});
+
+interface FormItem {
+  pageNum?: Number;
+  pageSize?: Number;
+}
+const pageParams: FormItem = reactive({ pageNum: 1, pageSize: 100 });
+
+interface TablePage {
+  current: number;
+  limit: number;
+  total: number;
+}
+const tablePage: TablePage = reactive({ current: 1, limit: 100, total: 0, limits: [20, 50, 100, 200, 500] });
+const columns = ref([
+  { title: "ID", width: "60px", key: "bot_id" },
+  { title: "名称", width: "90px", key: "alias", customSlot: "alias", ellipsisTooltip: true },
+  { title: "运行状态", width: "100px", key: "status", customSlot: "status", align: "center" },
+  { title: "现货资金", width: "120px", key: "spot_founding", align: "center", ellipsisTooltip: true },
+  { title: "合约资金", width: "120px", key: "swap_founding", align: "center" },
+  { title: "总资金", width: "120px", key: "totol_founding", customSlot: "totolFounding", align: "center" },
+  { title: "维持保证金率", width: "120px", key: "maintain_rate", customSlot: "maintainRate", align: "center" },
+  { title: "保证金比率", width: "120px", key: "liquidation_rate", customSlot: "liquidationRate", align: "center" },
+  { title: "自0点盈利", key: "since_last_0_profit", customSlot: "profit", ellipsisTooltip: true },
+  { title: "自0点现货流水/合约流水", key: "since_last_0_spot_flow", customSlot: "flow", align: "center" },
+  { title: "最后一次通讯 ", width: "120px", key: "last_ping", customSlot: "last_ping", align: "center" },
+]);
+
+let dataSource = ref([]);
+
+// 请求机器人列表
+const getPageInfo = (isSearch?: boolean) => {
+  if (isSearch) pageParams.pageNum = 1;
+
+  pageConfig.loading = true;
+  get_arbitrage_robot_list(pageParams, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      dataSource.value = data.data.list;
+      tablePage.total = data.data.total;
+    }
+  });
+};
+getPageInfo();
+
+let refreshInterval = setInterval(() => {
+  get_arbitrage_robot_list(pageParams, (data: any) => {
+    if (data.code == 200) {
+      dataSource.value = data.data.list;
+      tablePage.total = data.data.total;
+    }
+  });
+}, 2000);
+
+const countProfitability = (info: any) => {
+  return new Decimal(info.since_last_0_profit * 1).div(new Decimal(info.spot_founding * 1).add(info.swap_founding * 1)).mul(100);
+};
+const jumpDetail = (info: any) => {
+  window.open(`/bot/arbitrage/detail/${info.bot_id}`);
+};
+
+// 分页设置
+const handleCurrentChange = (val: any) => {
+  pageParams.pageNum = val.current;
+  pageParams.pageSize = val.limit;
+  getPageInfo();
+};
+
+onBeforeUnmount(() => {
+  document.title = "4L CAPITAL";
+  clearInterval(refreshInterval);
+});
+</script>
+
+<style lang="scss" scoped>
+.custom-operator-wp {
+  .custom-group {
+    margin-right: 14px;
+    display: inline-block;
+    vertical-align: middle;
+  }
+  .collect-wp {
+    line-height: 24px;
+    padding: 0 4px;
+    border: 1px solid var(--normal-color);
+    color: var(--normal-color);
+    :deep(.primary-color) {
+      color: var(--primary-color);
+    }
+    :deep(.danger-color) {
+      color: var(--danger-color);
+    }
+  }
+}
+.ellipsis-2 {
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+  line-break: anywhere;
+  -webkit-line-clamp: 2; /* 控制显示的行数 */
+}
+.custom-form-layout {
+  font-size: 12px !important;
+}
+.primary-color {
+  color: var(--primary-color);
+}
+.normal-color {
+  color: var(--normal-color);
+}
+.danger-color {
+  color: var(--danger-color);
+}
+</style>

+ 38 - 29
src/views/bot/manage/detail.vue

@@ -19,32 +19,37 @@
         </lay-tag>
       </lay-space>
     </div>
-    <div v-show="apiList?.includes('/remaining/list')">
-      <lay-card class="custom-card">
-        <template v-slot:title>
-          <span class="card-title">净值图</span>
-        </template>
-        <template v-slot:body>
-          <div class="profit-chart" ref="balanceChartRef"></div>
-        </template>
-      </lay-card>
-    </div>
 
-    <div v-if="apiList?.includes('/robot/getRobotLog')">
-      <lay-card class="custom-card">
-        <template v-slot:title>
-          <span class="card-title">运行日志</span>
-        </template>
+    <lay-card v-show="apiList?.includes('/remaining/list')" class="custom-card">
+      <template v-slot:title>
+        <span class="card-title">净值图</span>
+      </template>
+      <template v-slot:body>
+        <div class="profit-chart" ref="balanceChartRef"></div>
+      </template>
+    </lay-card>
+    <!-- <lay-card class="custom-card">
+      <template v-slot:title>
+        <span class="card-title">净值图</span>
+      </template>
+      <template v-slot:body>
+        <div class="predictor-chart" ref="predictorChartRef" @mouseleave="startPredictorInterval" @mouseenter="stopPredictorInterval"></div>
+      </template>
+    </lay-card> -->
 
-        <template v-slot:body>
-          <lay-table :columns="columns" size="sm" resize :data-source="logsList">
-            <template v-slot:text="{ row }">
-              <span class="ellipsis-2" @click="showLog(row.text)">{{ row.text }}</span>
-            </template>
-          </lay-table>
-        </template>
-      </lay-card>
-    </div>
+    <lay-card v-if="apiList?.includes('/robot/getRobotLog')" class="custom-card">
+      <template v-slot:title>
+        <span class="card-title">运行日志</span>
+      </template>
+
+      <template v-slot:body>
+        <lay-table :columns="columns" size="sm" resize :data-source="logsList">
+          <template v-slot:text="{ row }">
+            <span class="ellipsis-2" @click="showLog(row.text)">{{ row.text }}</span>
+          </template>
+        </lay-table>
+      </template>
+    </lay-card>
   </div>
   <LogText ref="logtextRef" />
 </template>
@@ -91,8 +96,8 @@ const columns = ref([
 let logsList = ref<Array<Logs>>();
 let robotDetail = ref<any>({});
 let balanceList = ref([]);
-let timer = ref();
 let balanceChart = shallowRef();
+let timer = ref();
 
 // 获取机器人详情
 const getRobotDetail = () => {
@@ -107,8 +112,6 @@ const getRobotDetail = () => {
     }
   });
 };
-
-// 获取账户余额
 const getBalanceInfo = (id: number) => {
   const params = { id: id };
   pageConfig.loading = true;
@@ -122,7 +125,7 @@ const getBalanceInfo = (id: number) => {
       const yMinData = Math.min(sData.map((item: any) => item[1]));
 
       !balanceChart.value
-        ? initChart(data.data)
+        ? initBalanceChart(data.data)
         : balanceChart.value.setOption({
             xAxis: {
               type: "category",
@@ -175,7 +178,7 @@ const handlePageInfo = (data: any) => {
   return result;
 };
 
-const initChart = (data: any) => {
+const initBalanceChart = (data: any) => {
   if (balanceChart.value != null && !balanceChart.value.isDisposed()) echarts.dispose(balanceChart.value);
 
   balanceChart.value = echarts.init(balanceChartRef.value);
@@ -229,11 +232,14 @@ const initChart = (data: any) => {
   };
   balanceChart.value.setOption(balanceChartOption);
 };
+
 timer.value = setInterval(() => {
   getBalanceInfo(robotDetail.value.accId);
   getLogsInfo();
 }, 5000);
 
+getLogsInfo();
+
 onMounted(() => {
   getRobotDetail();
 });
@@ -260,6 +266,9 @@ onUnmounted(() => {
 .profit-chart {
   height: 300px;
 }
+.predictor-chart {
+  height: 1200px;
+}
 .robot-info-header {
   background-color: white;
   padding: 16px 24px;

+ 11 - 5
src/views/bot/manage/index.vue

@@ -99,12 +99,17 @@
           </template>
           <template v-slot:configs="{ row }">
             <lay-tooltip :content="row.configs || ''">
-              <div class="ellipsis-2" @click="handleUpdate(row)"><span class="danger-color" v-if="apiList?.includes('/robot/setAuto') && row.isAuto == 1">[自动调参]</span>{{ `${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 }">
             <span :class="{ 'danger-color': timeConverts(row.lastReportTime).indexOf('秒') == -1 }">{{ row.lastReportTime ? timeConverts(row.lastReportTime) : "未通讯" }}</span>
           </template>
+          <template v-slot:posNum="{ row }">
+            <span :class="{ 'danger-color': row.posNum > 0 }">{{ row.posNum > 0 ? row.posNum : 0 }}</span>
+          </template>
           <template v-slot:updateTime="{ row }">
             <span>{{ timeConverts(row.updateTime) }}</span>
           </template>
@@ -222,17 +227,18 @@ interface TablePage {
 const tablePage: TablePage = reactive({ current: 1, limit: 50, total: 0, limits: [20, 50, 100, 200, 500] });
 const columns = ref([
   { title: "选项", width: "44px", type: "checkbox" },
-  { title: "ID", width: "60px", key: "id" },
+  { title: "ID", width: "50px", key: "id" },
   { title: "名称", width: "90px", key: "name", customSlot: "name", ellipsisTooltip: true },
   { title: "账户", width: "90px", key: "account", customSlot: "account", ellipsisTooltip: true },
   { title: "币对", width: "90px", key: "pair", customSlot: "pair", align: "center", ellipsisTooltip: true },
   { title: "起始", width: "90px", key: "startAmount", align: "center" },
-  { title: "收益", width: "90px", key: "earningRate", customSlot: "earningRate", align: "center" },
+  { title: "收益", width: "70px", key: "earningRate", customSlot: "earningRate", align: "center" },
   { title: "状态", width: "90px", key: "status", customSlot: "status", align: "center" },
   { title: "参数", key: "configs", customSlot: "configs" },
   { title: "IP:编号", width: "130px", key: "ip", customSlot: "ip", ellipsisTooltip: true },
-  { title: "通讯", width: "120px", key: "lastReportTime", customSlot: "lastReportTime", align: "center" },
-  { title: "修改", width: "120px", key: "updateTime", customSlot: "updateTime", align: "center" },
+  { title: "通讯", width: "70px", key: "lastReportTime", customSlot: "lastReportTime", align: "center" },
+  { title: "持仓信息", width: "80px", key: "posNum", customSlot: "posNum", align: "center", ellipsisTooltip: true },
+  { title: "修改", width: "70px", key: "updateTime", customSlot: "updateTime", align: "center" },
   { title: "所属人", width: "80px", key: "userName", align: "center", ellipsisTooltip: true },
   {
     title: "操作",

+ 226 - 0
src/views/indicator/symbol_rank_real/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="IndicatorSymbolRankReal">
+import { ref, reactive } from "vue";
+import { useRouter } from "vue-router";
+import TableButton from "@/components/TableButton.vue";
+import { get_exchange_rank, get_symbols_rank_real } 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_real(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>

+ 33 - 8
src/views/statistic/balance_arbitrage/index.vue

@@ -28,6 +28,9 @@
                 <lay-button type="default" @click="initChart(balanceData, 3)">按资金</lay-button>
               </lay-button-group>
             </lay-form-item>
+            <lay-form-item label="是否展示全部">
+              <lay-switch class="sm-switch" v-model="pageParams.isAll" @change="handleChangeLegendSelected" />
+            </lay-form-item>
           </lay-form>
         </div>
         <div>
@@ -55,19 +58,22 @@ interface PageConfig {
 
 interface FormItem {
   rangeTime: Array<string>;
+  isAll?: boolean;
 }
 
 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")] });
+const pageParams: FormItem = reactive({ rangeTime: [dayjs().subtract(24, "hour").format("YYYY-MM-DD HH:mm:ss"), dayjs().format("YYYY-MM-DD HH:mm:ss")], isAll: true });
 
 let balanceChart = shallowRef();
 let balanceData = ref();
 let earningsRateData = ref<Array<any>>([]);
 let earningsData = ref<Array<any>>([]);
 let totalBalanceData = ref<Array<any>>([]);
+let legendData = ref<any>([]);
+let legendSelected = ref<any>({});
 
 const getUserBalanceData = () => {
   balanceData.value = [];
@@ -108,10 +114,10 @@ const handleEarningsRateData = (data: any) => {
     if (item.label !== 0) {
       transferBalance = item.afterU;
       transferEarningsRate = lastEarningsRate;
-      return transferEarningsRate
-    };
+      return transferEarningsRate;
+    }
     let earningsRate = new Decimal(item.afterU || 0).minus(transferBalance).div(transferBalance).mul(100).add(transferEarningsRate).toNumber();
-    lastEarningsRate = earningsRate
+    lastEarningsRate = earningsRate;
     return earningsRate;
   });
   return earningsList;
@@ -167,7 +173,8 @@ const initChart = (data: any, type: number) => {
   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);
+  legendData.value = data.userList.map((item: any) => item.name);
+
   if (type == 1) {
     const earningsRateOption = {
       tooltip: {
@@ -187,8 +194,9 @@ const initChart = (data: any, type: number) => {
         },
       },
       legend: {
-        data: legendData,
+        data: legendData.value,
         top: "bottom",
+        selected: legendSelected.value,
       },
       xAxis: {
         type: "category",
@@ -226,8 +234,9 @@ const initChart = (data: any, type: number) => {
         },
       },
       legend: {
-        data: legendData,
+        data: legendData.value,
         top: "bottom",
+        selected: legendSelected.value,
       },
       xAxis: {
         type: "category",
@@ -260,8 +269,9 @@ const initChart = (data: any, type: number) => {
         },
       },
       legend: {
-        data: legendData,
+        data: legendData.value,
         top: "bottom",
+        selected: legendSelected.value,
       },
       xAxis: {
         type: "category",
@@ -277,6 +287,18 @@ const initChart = (data: any, type: number) => {
   }
 };
 
+const handleChangeLegendSelected = (value: any) => {
+  legendData.value.map((item: any) => {
+    legendSelected.value[item] = value;
+  });
+  balanceChart.value.setOption({
+    legend: {
+      data: legendData.value,
+      top: "bottom",
+      selected: legendSelected.value,
+    },
+  });
+};
 onUnmounted(() => {
   window.removeEventListener("resize", () => balanceChart.value.resize());
 });
@@ -288,6 +310,9 @@ onUnmounted(() => {
   .form-button-wp {
     margin-right: 60px;
   }
+  .sm-switch {
+    margin-top: 2px;
+  }
 }
 .chart {
   padding-top: 20px;