Pārlūkot izejas kodu

添加套利机器人信息

DESKTOP-NE65RNK\Citrus_limon 1 gadu atpakaļ
vecāks
revīzija
0139bacf55

+ 21 - 3
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) => {
@@ -652,11 +672,9 @@ export const set_acquire_label = (params: any, callback: any) => {
   });
 };
 
-
-
 // as视图
 export const get_predictor_state = (params: any, callback: any) => {
   return http.request("/predictor_state", "get", params).then((data) => {
     if (data) callback && callback(data);
   });
-};
+};

+ 12 - 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",
@@ -184,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>

+ 7 - 1
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>
@@ -232,6 +237,7 @@ const columns = ref([
   { 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: "100px", key: "posNum", customSlot: "posNum", align: "center", ellipsisTooltip: true },
   { title: "修改", width: "120px", key: "updateTime", customSlot: "updateTime", align: "center" },
   { title: "所属人", width: "80px", key: "userName", align: "center", ellipsisTooltip: true },
   {

+ 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;