add binance sdk

This commit is contained in:
JiyangTang 2024-08-28 10:15:08 +08:00
parent 46da669271
commit cc8a2a6c3c
23 changed files with 933 additions and 126 deletions

View File

@ -114,6 +114,21 @@
<artifactId>xxl-job-core</artifactId> <artifactId>xxl-job-core</artifactId>
<version>2.2.0</version> <version>2.2.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>io.github.binance</groupId>
<artifactId>binance-connector-java</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>
<resources> <resources>

View File

@ -0,0 +1,34 @@
package top.baogutang.admin.config;
import com.binance.connector.client.SpotClient;
import com.binance.connector.client.WebSocketStreamClient;
import com.binance.connector.client.impl.SpotClientImpl;
import com.binance.connector.client.impl.WebSocketStreamClientImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.baogutang.common.properties.MarketCandlersProperties;
import javax.annotation.Resource;
/**
* @description:
* @author: nikooh
* @date: 2024/08/23 : 15:45
*/
@Configuration
public class SpotClientConfiguration {
@Resource
private MarketCandlersProperties marketCandlersProperties;
@Bean(name = "spotClient")
public SpotClient spotClient() {
return new SpotClientImpl(marketCandlersProperties.getApiKey(),
marketCandlersProperties.getApiSecret());
}
@Bean(name = "webSocketStreamClient")
public WebSocketStreamClient webSocketStreamClient() {
return new WebSocketStreamClientImpl();
}
}

View File

@ -0,0 +1,24 @@
package top.baogutang.admin.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import top.baogutang.admin.handlers.KlineWebSocketHandler;
import javax.annotation.Resource;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Resource
private KlineWebSocketHandler klineWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(klineWebSocketHandler, "/ws")
// 配置 WebSocket 端点并允许跨域
.setAllowedOrigins("*");
}
}

View File

@ -15,6 +15,6 @@ public class StaticController {
@RequestMapping @RequestMapping
public String viewJsonParseHtml() { public String viewJsonParseHtml() {
// 这里返回的字符串是HTML文件名不包括扩展名 // 这里返回的字符串是HTML文件名不包括扩展名
return "json-parse"; return "coin";
} }
} }

View File

@ -0,0 +1,48 @@
package top.baogutang.admin.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import top.baogutang.admin.domain.res.MarketCandlesRes;
import top.baogutang.admin.services.IVirtualCoinService;
import top.baogutang.common.domain.Results;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* @description:
* @author: nikooh
* @date: 2024/08/23 : 17:29
*/
@RestController
@RequestMapping("/api/v1/admin/virtualCoin")
public class VirtualCoinController {
@Resource
private IVirtualCoinService virtualCoinService;
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@GetMapping("/uiKline")
public Results<List<String[]>> uiKline(@RequestParam(name = "symbol") String symbol,
@RequestParam(name = "interval") String interval) {
return Results.ok(virtualCoinService.uiKline(symbol, interval));
}
@GetMapping("/uiKlineData")
public Results<List<MarketCandlesRes.KLineData>> uiKlineData(@RequestParam(name = "symbol") String symbol,
@RequestParam(name = "interval") String interval) {
List<MarketCandlesRes.KLineData> kLineDataList = virtualCoinService.uiKline(symbol, interval).stream()
.sorted(Collections.reverseOrder(Comparator.comparingLong(data -> Long.parseLong(data[0]))))
.limit(5)
.map(data -> MarketCandlesRes.KLineData.newInstance(data, DATE_FORMAT))
.collect(Collectors.toList());
return Results.ok(kLineDataList);
}
}

View File

@ -0,0 +1,68 @@
package top.baogutang.admin.domain.res;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
/**
* @description:
* @author: nikooh
* @date: 2024/08/22 : 10:06
*/
@Data
public class MarketCandlesRes implements Serializable {
private static final long serialVersionUID = -3375737475097909682L;
@JsonProperty("from")
private String from;
@JsonProperty("klines")
private List<KLineData> kLineDataList;
@Data
public static class KLineData implements Serializable {
private static final long serialVersionUID = 522558542163537197L;
// 开盘价格
@JsonProperty("开盘")
private BigDecimal openPrice;
// 最高价格
@JsonProperty("最高")
private BigDecimal highestPrice;
// 最低价格
@JsonProperty("最低")
private BigDecimal lowestPrice;
// 收盘价格
@JsonProperty("收盘")
private BigDecimal closePrice;
// K线
@JsonProperty("timestamp")
private Long timestamp;
@JsonProperty("时间")
private String date;
public static KLineData newInstance(String[] data, SimpleDateFormat dateFormat) {
KLineData kLineData = new KLineData();
kLineData.setOpenPrice(new BigDecimal(data[1]));
kLineData.setHighestPrice(new BigDecimal(data[2]));
kLineData.setLowestPrice(new BigDecimal(data[3]));
kLineData.setClosePrice(new BigDecimal(data[4]));
kLineData.setDate(dateFormat.format(new Date(Long.parseLong(data[0]))));
return kLineData;
}
}
}

View File

@ -0,0 +1,98 @@
package top.baogutang.admin.handlers;
import com.binance.connector.client.WebSocketStreamClient;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.socket.*;
import top.baogutang.common.utils.JacksonUtil;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.Serializable;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class KlineWebSocketHandler implements WebSocketHandler {
@Resource
private WebSocketStreamClient webSocketStreamClient;
private final Map<String, WebSocketSession> webSocketMap = new ConcurrentHashMap<>();
private volatile Integer connectionId;
@Override
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
//连接成功时调用该方法
log.info(">>>>>>>>>>WebSocket connected:{}<<<<<<<<<<", webSocketSession.getId());
webSocketMap.put(webSocketSession.getId(), webSocketSession);
}
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
// 获取客户端发送的消息
log.info(">>>>>>>>>>客户端ID:{} 发送消息:{}<<<<<<<<<<", webSocketSession.getId(), webSocketMessage.getPayload());
KlineMessage message = JacksonUtil.fromJson(webSocketMessage.getPayload().toString(), KlineMessage.class);
if (Objects.nonNull(connectionId)) {
webSocketStreamClient.closeConnection(connectionId);
}
// 使用 Binance Connector 开启 WebSocket 连接
connectionId = webSocketStreamClient.klineStream(message.getSymbol(), message.getInterval(), event -> {
log.info(">>>>>>>>>>binance event:{}<<<<<<<<<<", event);
// 将从 Binance 接收到的数据转发给前端
TextMessage textMessage = new TextMessage(event);
// 推送消息给所有连接的客户端
webSocketMap.entrySet()
.stream()
.filter(entry -> entry.getValue().isOpen())
.forEach(entry -> {
try {
entry.getValue().sendMessage(textMessage);
} catch (IOException e) {
log.error(">>>>>>>>>>消息发送错误:{}<<<<<<<<<<", e.getMessage(), e);
}
});
});
log.info(">>>>>>>>>启动了 Binance WebSocket 连接: {} , 监控交易对: {} 时间间隔: {}<<<<<<<<", connectionId, message.getSymbol(), message.getInterval());
}
@Override
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
//发生错误时调用该方法
log.error(">>>>>>>>>>WebSocket error: {}<<<<<<<<<<", throwable.getMessage(), throwable);
webSocketSession.close(CloseStatus.SERVER_ERROR);
webSocketMap.remove(webSocketSession.getId());
}
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
//连接关闭时调用该方法
log.info(">>>>>>>>>>WebSocket closed:{}<<<<<<<<<<", webSocketSession.getId());
webSocketMap.remove(webSocketSession.getId());
if (CollectionUtils.isEmpty(webSocketMap)) {
log.info(">>>>>>>>>>client session all closed!<<<<<<<<<<");
webSocketStreamClient.closeAllConnections();
}
}
@Override
public boolean supportsPartialMessages() {
return false;
}
@Data
public static class KlineMessage implements Serializable {
private static final long serialVersionUID = -3288071423561766084L;
private String symbol;
private String interval;
}
}

View File

@ -0,0 +1,122 @@
package top.baogutang.admin.schedule;
import cn.hutool.core.bean.BeanUtil;
import com.binance.connector.client.SpotClient;
import com.fasterxml.jackson.core.type.TypeReference;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import top.baogutang.admin.utils.DingTalkMsgPushUtils;
import top.baogutang.admin.utils.OkCoinKLineUtil;
import top.baogutang.common.domain.BinanceResults;
import top.baogutang.common.utils.JacksonUtil;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
/**
* @description: OKCOIN K线数据
* @author: nikooh
* @date: 2024/08/22 : 09:52
*/
@Slf4j
@Component
@RefreshScope
public class OkCoinMarketCandlesHandler extends IJobHandler {
@Resource
private DingTalkMsgPushUtils dingTalkMsgPushUtils;
@Resource
private SpotClient spotClient;
public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
@Override
@XxlJob("marketCandlesHandler")
public ReturnT<String> execute(String params) throws Exception {
if (StringUtils.isBlank(params)) {
log.error(">>>>>>>>>>job params is null!<<<<<<<<<<");
return ReturnT.FAIL;
}
JobParams jobParams = JacksonUtil.fromJson(params, JobParams.class);
if (Objects.isNull(jobParams)) {
log.error(">>>>>>>>>>job params is null!<<<<<<<<<<");
return ReturnT.FAIL;
}
String interval = jobParams.getInterval();
Integer limit = jobParams.getLimit();
Arrays.stream(jobParams.getSymbolList().split(","))
.forEach(symbol -> {
KLinesRequestParameters kLinesRequestParameters = new KLinesRequestParameters(symbol, interval, limit);
Map<String, Object> parameters = BeanUtil.beanToMap(kLinesRequestParameters);
String resultStr = spotClient.createMarket().uiKlines(parameters);
if (StringUtils.isBlank(resultStr)) {
log.error(">>>>>>>>>>request result is null!<<<<<<<<<<");
return;
}
List<String[]> kLineDataList = JacksonUtil.fromJson(resultStr, new TypeReference<List<String[]>>() {
});
String[] kLineData = kLineDataList.get(0);
log.info(">>>>>>>>>>当前:【{}】 开盘价:【{}】 最高价:【{}】最低价:【{}】收盘价:【{}】<<<<<<<<<<", DATE_FORMAT.format(new Date(Long.parseLong(kLineData[0]))), kLineData[1], kLineData[2], kLineData[3], kLineData[4]);
kLineDataList = kLineDataList.stream()
.sorted(Comparator.comparing(data -> data[0], Comparator.reverseOrder()))
.limit(5)
.collect(Collectors.toList());
String markdownContent = OkCoinKLineUtil.getCoinMarkdownContent(symbol, kLineDataList);
dingTalkMsgPushUtils.robotMarkdownMsg("交易产品K线数据", markdownContent);
});
return ReturnT.SUCCESS;
}
@Data
public static class JobParams {
// BTCUSDT,ETHUSDT
private String symbolList;
// 时间粒度
// 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M
private String interval;
// 默认 500; 最大值 1000
private Integer limit;
}
@Data
public static class KLinesRequestParameters {
// BTCUSDT,ETHUSDT
private String symbol;
// 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M
private String interval;
// 默认 500; 最大值 1000
private Integer limit;
public KLinesRequestParameters(String symbol, String interval, Integer limit) {
this.symbol = symbol;
this.interval = interval;
this.limit = limit;
}
public KLinesRequestParameters(String symbol, String interval) {
this.symbol = symbol;
this.interval = interval;
}
}
}

View File

@ -0,0 +1,16 @@
package top.baogutang.admin.services;
import java.util.List;
/**
* @description:
* @author: nikooh
* @date: 2024/08/23 : 17:50
*/
public interface IVirtualCoinService {
List<String[]> uiKline(String symbol, String interval);
// void startWebSocket(String symbol, String interval);
}

View File

@ -0,0 +1,48 @@
package top.baogutang.admin.services.impl;
import cn.hutool.core.bean.BeanUtil;
import com.binance.connector.client.SpotClient;
import com.binance.connector.client.WebSocketStreamClient;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import top.baogutang.admin.schedule.OkCoinMarketCandlesHandler;
import top.baogutang.admin.services.IVirtualCoinService;
import top.baogutang.common.utils.JacksonUtil;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* @description:
* @author: nikooh
* @date: 2024/08/23 : 17:50
*/
@Slf4j
@Service
public class VirtualCoinServiceImpl implements IVirtualCoinService {
@Resource
private SpotClient spotClient;
@Resource
private WebSocketStreamClient webSocketStreamClient;
@Override
public List<String[]> uiKline(String symbol, String interval) {
OkCoinMarketCandlesHandler.KLinesRequestParameters kLinesRequestParameters = new OkCoinMarketCandlesHandler.KLinesRequestParameters(symbol, interval);
Map<String, Object> parameters = BeanUtil.beanToMap(kLinesRequestParameters, Boolean.FALSE, Boolean.TRUE);
String resultStr = spotClient.createMarket().uiKlines(parameters);
if (StringUtils.isBlank(resultStr)) {
log.error(">>>>>>>>>>request result is null!<<<<<<<<<<");
return Collections.emptyList();
}
return JacksonUtil.fromJson(resultStr, new TypeReference<List<String[]>>() {
});
}
}

View File

@ -161,6 +161,30 @@ public class DingTalkMsgPushUtils {
log.info(">>>>>>>>>>robot msg send request:{}, response:{}<<<<<<<<<<", JacksonUtil.toJson(request), JacksonUtil.toJson(response)); log.info(">>>>>>>>>>robot msg send request:{}, response:{}<<<<<<<<<<", JacksonUtil.toJson(request), JacksonUtil.toJson(response));
} }
public void robotMarkdownMsg(String markdownTitle, String markdownContent) {
// 计算签名
long timestamp = System.currentTimeMillis();
String sign = this.sign(timestamp);
String url = robotWebhookUrl + "&timestamp=" + timestamp + "&sign=" + sign;
DingTalkClient client = new DefaultDingTalkClient(url);
OapiRobotSendRequest request = new OapiRobotSendRequest();
OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
at.setIsAtAll(false);
request.setAt(at);
request.setMsgtype(DingTalkMsgTypeEnum.MARKDOWN.getType());
OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
markdown.setTitle(markdownTitle);
markdown.setText(markdownContent);
request.setMarkdown(markdown);
OapiRobotSendResponse response = null;
try {
response = client.execute(request);
} catch (Exception e) {
log.error(">>>>>>>>>>robot msg send error:{}<<<<<<<<<<", e.getMessage(), e);
}
log.info(">>>>>>>>>>robot msg send request:{}, response:{}<<<<<<<<<<", JacksonUtil.toJson(request), JacksonUtil.toJson(response));
}
private String sign(Long timestamp) { private String sign(Long timestamp) {
try { try {
String stringToSign = timestamp + "\n" + secret; String stringToSign = timestamp + "\n" + secret;

View File

@ -0,0 +1,49 @@
package top.baogutang.admin.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
/**
* @description:
* @author: nikooh
* @date: 2024/08/22 : 10:42
*/
@Slf4j
public class OkCoinKLineUtil {
public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String getCoinMarkdownContent(String instId, List<String[]> candleDataList) {
if (CollectionUtils.isEmpty(candleDataList)) {
return null;
}
StringBuilder markdownBuilder = new StringBuilder();
// 添加标题
markdownBuilder.append("## ")
.append(instId)
.append("市场价格数据\n\n");
// 添加表头
markdownBuilder.append("| 时间 | 开盘价 | 最高价 | 最低价 | 收盘价 |\n");
markdownBuilder.append("|--------------------------------|----------------------------|---------------------------|---------------------------|---------------------------|\n");
// 添加每一行数据
for (String[] candle : candleDataList) {
markdownBuilder.append("| ")
.append(DATE_FORMAT.format(new Date(Long.parseLong(candle[0])))).append(" | ")
.append(String.format("%-30f", Double.parseDouble(candle[1]))).append(" | ")
.append(String.format("%-24f", Double.parseDouble(candle[2]))).append(" | ")
.append(String.format("%-24f", Double.parseDouble(candle[3]))).append(" | ")
.append(String.format("%-24f", Double.parseDouble(candle[4]))).append(" |\n");
}
return markdownBuilder.toString();
}
}

View File

@ -11,7 +11,7 @@
</encoder> </encoder>
</appender> </appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="localFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/app.log</file> <file>${log.path}/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/%d{yyyy-MM-dd,aux}/app-%d{yyyy-MM-dd}.%i.log <fileNamePattern>${log.path}/%d{yyyy-MM-dd,aux}/app-%d{yyyy-MM-dd}.%i.log
@ -27,11 +27,11 @@
</encoder> </encoder>
</appender> </appender>
<appender name="asyncFileAppender" class="ch.qos.logback.classic.AsyncAppender"> <!-- <appender name="asyncFileAppender" class="ch.qos.logback.classic.AsyncAppender">-->
<queueSize>10000</queueSize> <!-- <queueSize>10000</queueSize>-->
<discardingThreshold>0</discardingThreshold> <!-- <discardingThreshold>0</discardingThreshold>-->
<appender-ref ref="file"/> <!-- <appender-ref ref="file"/>-->
</appender> <!-- </appender>-->
<!-- 读取配置文件信息(交由各项目指定) --> <!-- 读取配置文件信息(交由各项目指定) -->
<property name="active" value="${spring.profiles.active:-test}" /> <property name="active" value="${spring.profiles.active:-test}" />
@ -43,42 +43,6 @@
<!--为了防止进程退出时,内存中的数据丢失,请加上此选项--> <!--为了防止进程退出时,内存中的数据丢失,请加上此选项-->
<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/> <shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
<!-- 外网 -->
<appender name="ONLINE-OUT" class="com.aliyun.openservices.log.logback.LoghubAppender">
<!--必选项 -->
<endpoint>cn-shanghai.log.aliyuncs.com</endpoint>
<accessKeyId>LTAI5tRN9T5Tz1QExcSpUaBc</accessKeyId>
<accessKey>LniIMK15XEOc6Nn5mOrtX399FkVfQd</accessKey>
<projectName>baogutang</projectName>
<logstore>${appEnv}</logstore>
<!-- 可选项 -->
<topic>${appId}</topic>
<!-- <source>source1</source> -->
<!-- 可选项 详见 '参数说明' -->
<packageTimeoutInMS>3000</packageTimeoutInMS>
<logsCountPerPackage>4096</logsCountPerPackage>
<logsBytesPerPackage>3145728</logsBytesPerPackage>
<memPoolSizeInByte>104857600</memPoolSizeInByte>
<retryTimes>3</retryTimes>
<maxIOThreadSizeInPool>8</maxIOThreadSizeInPool>
<!-- 可选项 设置时区 -->
<timeZone>Asia/Shanghai</timeZone>
<!-- 可选项 设置时间格式 -->
<timeFormat>yyyy-MM-dd HH:mm:ss.SSS</timeFormat>
<!-- 可选项 通过配置 encoder 的 pattern 自定义 log 的格式 -->
<encoder>
<!-- <pattern>[${appId}] %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{0}: %msg</pattern> -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{X-Request-Id}] [%thread] %logger{0}: %msg</pattern>
</encoder>
<!-- 指定级别的日志(INFO,WARN,ERROR) -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<logger name="org.hibernate" level="ERROR" /> <logger name="org.hibernate" level="ERROR" />
<logger name="org.apache" level="ERROR" /> <logger name="org.apache" level="ERROR" />
@ -100,14 +64,14 @@
<springProfile name="test,prod"> <springProfile name="test,prod">
<root level="DEBUG"> <root level="DEBUG">
<appender-ref ref="console"/> <appender-ref ref="console"/>
<appender-ref ref="ONLINE-OUT"/> <appender-ref ref="localFile"/>
</root> </root>
</springProfile> </springProfile>
<springProfile name="local"> <springProfile name="local">
<root level="DEBUG"> <root level="DEBUG">
<appender-ref ref="console"/> <appender-ref ref="console"/>
<appender-ref ref="ONLINE-OUT"/> <appender-ref ref="localFile"/>
</root> </root>
</springProfile> </springProfile>

View File

@ -0,0 +1,219 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>K-LINE</title>
<style>
/* 样式美化 */
body {
font-family: Arial, sans-serif;
background-color: #1b1b1b;
color: #fff;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
}
h2 {
margin: 20px 0;
color: #f0b90b;
}
select {
margin: 0 10px;
padding: 10px;
background-color: #2c2c2c;
color: #fff;
border: none;
border-radius: 5px;
}
#controls {
margin-bottom: 20px;
display: flex;
justify-content: center;
align-items: center;
}
#kLineChart {
width: 100%;
height: 600px;
}
</style>
<!-- 引入 Lightweight Charts 库 -->
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
</head>
<body>
<h2>K-LINE</h2>
<!-- 控制区 -->
<div id="controls">
<!-- 交易对选择 -->
<label for="symbol">选择交易对:</label>
<select id="symbol" onchange="updateChart()">
<option value="BTCUSDT">BTC/USDT</option>
<option value="ETHUSDT">ETH/USDT</option>
</select>
<!-- 时间间隔选择 -->
<label for="timeInterval">选择时间间隔:</label>
<select id="timeInterval" onchange="updateChart()">
<option value="1m">1 分钟</option>
<option value="3m">3 分钟</option>
<option value="5m">5 分钟</option>
<option value="15m">15 分钟</option>
<option value="1h">1 小时</option>
<option value="3h">3 小时</option>
<option value="1d">1 天</option>
<option value="3d">3 天</option>
</select>
</div>
<!-- K线图容器 -->
<div id="kLineChart"></div>
<script>
let chart, candlestickSeries, socket;
let lastKLineTime = 0; // 用于记录最后一个K线的时间
// 初始化 Lightweight Charts
function initChart() {
chart = LightweightCharts.createChart(document.getElementById('kLineChart'), {
width: document.getElementById('kLineChart').clientWidth,
height: 600,
layout: {
backgroundColor: '#131722',
textColor: '#d1d4dc',
},
grid: {
vertLines: {
color: '#2B2B43',
},
horzLines: {
color: '#363C4E',
},
},
crosshair: {
mode: LightweightCharts.CrosshairMode.Normal,
},
priceScale: {
borderColor: '#485c7b',
},
timeScale: {
borderColor: '#485c7b',
timeVisible: true,
secondsVisible: true,
localization: {
dateFormat: 'yyyy-MM-dd HH:mm:ss',
timeZone: 'Asia/Shanghai',
},
},
});
candlestickSeries = chart.addCandlestickSeries({
upColor: '#4CAF50',
downColor: '#F44336',
borderDownColor: '#F44336',
borderUpColor: '#4CAF50',
wickDownColor: '#F44336',
wickUpColor: '#4CAF50',
});
}
// 连接 WebSocket
function connectWebSocket() {
socket = new WebSocket("ws://localhost:8102/ws");
socket.onopen = function (event) {
console.log('WebSocket connection established.');
// Send initial data request to WebSocket
updateWebSocket();
};
socket.onmessage = function (event) {
const data = JSON.parse(event.data);
const kline = data.k;
if (kline.t > lastKLineTime) {
// 如果是新K线先更新 lastKLineTime
lastKLineTime = kline.t;
}
const formattedData = {
time: lastKLineTime / 1000, // 时间戳(秒)
open: parseFloat(kline.o), // 开盘价
high: parseFloat(kline.h), // 最高价
low: parseFloat(kline.l), // 最低价
close: parseFloat(kline.c) // 收盘价
};
// 更新图表
candlestickSeries.update(formattedData);
};
socket.onclose = function (event) {
console.log('WebSocket is closed now.');
};
socket.onerror = function (error) {
console.error('WebSocket error observed:', error);
};
}
// 更新 WebSocket 连接
function updateWebSocket() {
const symbol = document.getElementById('symbol').value;
const interval = document.getElementById('timeInterval').value;
const payload = JSON.stringify({symbol: symbol, interval: interval});
socket.send(payload);
}
// 更新图表
function updateChart() {
const symbol = document.getElementById('symbol').value;
const interval = document.getElementById('timeInterval').value;
// 获取接口数据并更新图表
fetch(`http://localhost:8102/api/v1/admin/virtualCoin/uiKline?symbol=${symbol}&interval=${interval}`)
.then(response => response.json())
.then(data => {
const formattedData = data.data.map(item => ({
time: item[0] / 1000, // 时间戳(秒)
open: parseFloat(item[1]), // 开盘价
high: parseFloat(item[2]), // 最高价
low: parseFloat(item[3]), // 最低价
close: parseFloat(item[4]) // 收盘价
}));
// 对数据进行排序(按时间升序)
formattedData.sort((a, b) => a.time - b.time);
// 设置初始的 lastKLineTime 为最后一条数据的时间戳(毫秒)
lastKLineTime = formattedData[formattedData.length - 1].time * 1000;
// 更新图表
candlestickSeries.setData(formattedData);
})
.catch(error => console.error('Error fetching data:', error));
// 更新 WebSocket 连接
if (socket && socket.readyState === WebSocket.OPEN) {
updateWebSocket();
}
}
// 初始化图表和 WebSocket 连接
initChart();
connectWebSocket();
// 监听窗口大小变化,调整图表尺寸
window.addEventListener('resize', () => {
chart.resize(document.getElementById('kLineChart').clientWidth, 600);
});
// 页面加载时更新图表
updateChart();
</script>
</body>
</html>

View File

@ -0,0 +1,28 @@
package top.baogutang.business.admin.job;
import com.xxl.job.core.biz.model.ReturnT;
import org.junit.Assert;
import org.junit.Test;
import top.baogutang.admin.schedule.OkCoinMarketCandlesHandler;
import top.baogutang.business.admin.BaoGuTangAdminAbstractTest;
import javax.annotation.Resource;
import static com.xxl.job.core.biz.model.ReturnT.SUCCESS_CODE;
/**
* @description:
* @author: nikooh
* @date: 2024/08/22 : 11:18
*/
public class MarketCandlesTest extends BaoGuTangAdminAbstractTest {
@Resource
private OkCoinMarketCandlesHandler okCoinMarketCandlesHandler;
@Test
public void testMarketCandles() throws Exception {
ReturnT<String> returnT = okCoinMarketCandlesHandler.execute("{\"onlyKey\":\"Bitcoin\",\"bar\":\"kline_1m\",\"sign\":\"marketCap\"}");
Assert.assertEquals(SUCCESS_CODE, returnT.getCode());
}
}

View File

@ -11,7 +11,7 @@
</encoder> </encoder>
</appender> </appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="localFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/app.log</file> <file>${log.path}/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/%d{yyyy-MM-dd,aux}/app-%d{yyyy-MM-dd}.%i.log <fileNamePattern>${log.path}/%d{yyyy-MM-dd,aux}/app-%d{yyyy-MM-dd}.%i.log
@ -43,43 +43,6 @@
<!--为了防止进程退出时,内存中的数据丢失,请加上此选项--> <!--为了防止进程退出时,内存中的数据丢失,请加上此选项-->
<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/> <shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
<!-- 外网 -->
<appender name="ONLINE-OUT" class="com.aliyun.openservices.log.logback.LoghubAppender">
<!--必选项 -->
<endpoint>cn-shanghai.log.aliyuncs.com</endpoint>
<accessKeyId>LTAI5tRN9T5Tz1QExcSpUaBc</accessKeyId>
<accessKey>LniIMK15XEOc6Nn5mOrtX399FkVfQd</accessKey>
<projectName>baogutang</projectName>
<logstore>${appEnv}</logstore>
<!-- 可选项 -->
<topic>${appId}</topic>
<!-- <source>source1</source> -->
<!-- 可选项 详见 '参数说明' -->
<packageTimeoutInMS>3000</packageTimeoutInMS>
<logsCountPerPackage>4096</logsCountPerPackage>
<logsBytesPerPackage>3145728</logsBytesPerPackage>
<memPoolSizeInByte>104857600</memPoolSizeInByte>
<retryTimes>3</retryTimes>
<maxIOThreadSizeInPool>8</maxIOThreadSizeInPool>
<!-- 可选项 设置时区 -->
<timeZone>Asia/Shanghai</timeZone>
<!-- 可选项 设置时间格式 -->
<timeFormat>yyyy-MM-dd HH:mm:ss.SSS</timeFormat>
<!-- 可选项 通过配置 encoder 的 pattern 自定义 log 的格式 -->
<encoder>
<!-- <pattern>[${appId}] %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{0}: %msg</pattern> -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{X-Request-Id}] [%thread] %logger{0}: %msg</pattern>
</encoder>
<!-- 指定级别的日志(INFO,WARN,ERROR) -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<logger name="org.hibernate" level="ERROR" /> <logger name="org.hibernate" level="ERROR" />
<logger name="org.apache" level="ERROR" /> <logger name="org.apache" level="ERROR" />
<logger name="ch.qos.logback" level="WARN" /> <logger name="ch.qos.logback" level="WARN" />
@ -100,14 +63,13 @@
<springProfile name="test,prod"> <springProfile name="test,prod">
<root level="DEBUG"> <root level="DEBUG">
<appender-ref ref="console"/> <appender-ref ref="console"/>
<appender-ref ref="ONLINE-OUT"/> <appender-ref ref="localFile"/>
</root> </root>
</springProfile> </springProfile>
<springProfile name="local"> <springProfile name="local">
<root level="DEBUG"> <root level="DEBUG">
<appender-ref ref="console"/> <appender-ref ref="console"/>
<appender-ref ref="ONLINE-OUT"/>
</root> </root>
</springProfile> </springProfile>

View File

@ -170,6 +170,9 @@
<version>1.4.2</version> <version>1.4.2</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -20,7 +20,7 @@ public class GlobalCorsConfig {
//重写父类提供的跨域请求处理的接口 //重写父类提供的跨域请求处理的接口
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
//添加映射路径 //添加映射路径
registry.addMapping("/api") registry.addMapping("/api/**")
//放行哪些原始域 //放行哪些原始域
.allowedOrigins("*") .allowedOrigins("*")
//是否发送Cookie信息 //是否发送Cookie信息

View File

@ -0,0 +1,24 @@
package top.baogutang.common.domain;
import lombok.Data;
import java.io.Serializable;
/**
* @description:
* @author: nikooh
* @date: 2024/08/23 : 16:00
*/
@Data
public class BinanceResults<T> implements Serializable {
private static final long serialVersionUID = 4314677378175006552L;
public static final int SUCCESS_CODE = 200;
private String id;
private Integer status;
private T result;
}

View File

@ -1,8 +1,12 @@
package top.baogutang.common.domain; package top.baogutang.common.domain;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/** /**
* @description: * @description:
@ -18,6 +22,23 @@ public class PageParam implements Serializable {
private Integer pageSize; private Integer pageSize;
@ApiModelProperty("排序条件")
private List<OrderBy> orders = new ArrayList<>();
@ApiModel
@Data
public static class OrderBy implements Serializable {
private static final long serialVersionUID = -2936335557980068706L;
@ApiModelProperty("排序列名")
private String columnName;
@ApiModelProperty("是否降序")
private boolean desc;
}
public Integer getPageNum() { public Integer getPageNum() {
pageNum = pageNum == null ? 1 : pageNum; pageNum = pageNum == null ? 1 : pageNum;
return pageNum; return pageNum;

View File

@ -0,0 +1,25 @@
package top.baogutang.common.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
/**
* @description:
* @author: nikooh
* @date: 2024/08/22 : 09:57
*/
@Data
@Component
@RefreshScope
@ConfigurationProperties(prefix = "baogutang.coin")
public class MarketCandlersProperties {
private String okCoinMarketCandlesUrl = "https://i.bicoin.com.cn/data/getKlineByOnlyKey";
private String apiKey = "xQIw2QFYG817KN9QiRQ76E1ThrBS2vAKJZTyGE5O3acX47Dr21I6QFHNMZbHWzE9";
private String apiSecret = "mJvcuTz57Qu4hHjZQLwMJhXASInosPXM4181x6UnofCdLlS3ROrFCRw9GLNucSTt";
}

View File

@ -1,5 +1,9 @@
package top.baogutang.common.utils; package top.baogutang.common.utils;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import top.baogutang.common.domain.PageParam; import top.baogutang.common.domain.PageParam;
@ -42,4 +46,53 @@ public class MyBatisPlusPageUtil {
return new PageUtil<>(pageRes.getCurrent(), pageRes.getSize(), pageRes.getTotal(), pageRes.getPages(), rPageRes); return new PageUtil<>(pageRes.getCurrent(), pageRes.getSize(), pageRes.getTotal(), pageRes.getPages(), rPageRes);
} }
/**
* 默认排序
*
* @param orders 排序条件
* @param queryWrapper 查询条件
* @param <T> T
*/
public static <T> void defaultOrders(List<PageParam.OrderBy> orders, QueryWrapper<T> queryWrapper) {
defaultOrder(orders, queryWrapper, null);
}
/**
* 默认排序,可以指定默认排序字段
*
* @param orders 排序条件
* @param queryWrapper 查询条件
* @param defaultColumnName 指定默认排序字段列
* @param <T> T
*/
public static <T, R> void defaultOrder(List<PageParam.OrderBy> orders, QueryWrapper<T> queryWrapper, SFunction<T, R> defaultColumnName) {
if (CollUtil.isEmpty(orders)) {
defaultOrderProcess(queryWrapper, defaultColumnName);
return;
}
orders.forEach(orderBy -> {
//默认时间倒序
if (StringUtils.isEmpty(orderBy.getColumnName())) {
defaultOrderProcess(queryWrapper, defaultColumnName);
} else {
if (orderBy.isDesc()) {
queryWrapper.orderByDesc(orderBy.getColumnName());
} else {
queryWrapper.orderByAsc(orderBy.getColumnName());
}
}
});
}
/**
* @param queryWrapper 查询条件
* @param defaultColumnName 默认排序列名
* @param <T> t
* @param <R> r
*/
private static <T, R> void defaultOrderProcess(QueryWrapper<T> queryWrapper, SFunction<T, R> defaultColumnName) {
if (defaultColumnName != null) {
queryWrapper.lambda().orderByDesc(defaultColumnName);
}
}
} }

View File

@ -11,7 +11,7 @@
</encoder> </encoder>
</appender> </appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="localFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/app.log</file> <file>${log.path}/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/%d{yyyy-MM-dd,aux}/app-%d{yyyy-MM-dd}.%i.log <fileNamePattern>${log.path}/%d{yyyy-MM-dd,aux}/app-%d{yyyy-MM-dd}.%i.log
@ -43,43 +43,6 @@
<!--为了防止进程退出时,内存中的数据丢失,请加上此选项--> <!--为了防止进程退出时,内存中的数据丢失,请加上此选项-->
<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/> <shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
<!-- 外网 -->
<appender name="ONLINE-OUT" class="com.aliyun.openservices.log.logback.LoghubAppender">
<!--必选项 -->
<endpoint>cn-shanghai.log.aliyuncs.com</endpoint>
<accessKeyId>LTAI5tRN9T5Tz1QExcSpUaBc</accessKeyId>
<accessKey>LniIMK15XEOc6Nn5mOrtX399FkVfQd</accessKey>
<projectName>baogutang</projectName>
<logstore>${appEnv}</logstore>
<!-- 可选项 -->
<topic>${appId}</topic>
<!-- <source>source1</source> -->
<!-- 可选项 详见 '参数说明' -->
<packageTimeoutInMS>3000</packageTimeoutInMS>
<logsCountPerPackage>4096</logsCountPerPackage>
<logsBytesPerPackage>3145728</logsBytesPerPackage>
<memPoolSizeInByte>104857600</memPoolSizeInByte>
<retryTimes>3</retryTimes>
<maxIOThreadSizeInPool>8</maxIOThreadSizeInPool>
<!-- 可选项 设置时区 -->
<timeZone>Asia/Shanghai</timeZone>
<!-- 可选项 设置时间格式 -->
<timeFormat>yyyy-MM-dd HH:mm:ss.SSS</timeFormat>
<!-- 可选项 通过配置 encoder 的 pattern 自定义 log 的格式 -->
<encoder>
<!-- <pattern>[${appId}] %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{0}: %msg</pattern> -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{X-Request-Id}] [%thread] %logger{0}: %msg</pattern>
</encoder>
<!-- 指定级别的日志(INFO,WARN,ERROR) -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<logger name="org.hibernate" level="ERROR" /> <logger name="org.hibernate" level="ERROR" />
<logger name="org.apache" level="ERROR" /> <logger name="org.apache" level="ERROR" />
<logger name="ch.qos.logback" level="WARN" /> <logger name="ch.qos.logback" level="WARN" />
@ -100,14 +63,13 @@
<springProfile name="test,prod"> <springProfile name="test,prod">
<root level="DEBUG"> <root level="DEBUG">
<appender-ref ref="console"/> <appender-ref ref="console"/>
<appender-ref ref="ONLINE-OUT"/> <appender-ref ref="localFile"/>
</root> </root>
</springProfile> </springProfile>
<springProfile name="local"> <springProfile name="local">
<root level="DEBUG"> <root level="DEBUG">
<appender-ref ref="console"/> <appender-ref ref="console"/>
<appender-ref ref="ONLINE-OUT"/>
</root> </root>
</springProfile> </springProfile>