diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 8028021..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Launch main.go", - "type": "go", - "request": "launch", - "mode": "auto", // 也可選 debug/test/remote - "program": "${workspaceFolder}/blockchain.go", // 或你的專案主程式 - "env": { - "GOPATH": "/home/wang/go" - }, - // "args": ["-arg1", "value"] - } - - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 58a6948..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "editor.fontFamily": "Fira Code, 'Fira Code', monospace" -} \ No newline at end of file diff --git a/client/blockchainservice/blockchain_service.go b/client/blockchainservice/blockchain_service.go index d269a8b..8ce2ec6 100644 --- a/client/blockchainservice/blockchain_service.go +++ b/client/blockchainservice/blockchain_service.go @@ -1,5 +1,5 @@ // Code generated by goctl. DO NOT EDIT. -// goctl 1.8.5 +// goctl 1.8.1 // Source: blockchain.proto package blockchainservice @@ -7,13 +7,14 @@ package blockchainservice import ( "context" - app_cloudep_blockchain "blockchain/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain" + "blockchain/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain" "github.com/zeromicro/go-zero/zrpc" "google.golang.org/grpc" ) type ( + HistoryReq = app_cloudep_blockchain.HistoryReq ListSymbolsRequest = app_cloudep_blockchain.ListSymbolsRequest ListSymbolsResponse = app_cloudep_blockchain.ListSymbolsResponse NoneReq = app_cloudep_blockchain.NoneReq @@ -23,6 +24,7 @@ type ( BlockchainService interface { // ListSymbols retrieves all available trading symbols. ListSymbols(ctx context.Context, in *ListSymbolsRequest, opts ...grpc.CallOption) (*ListSymbolsResponse, error) + GetHistoryKlineData(ctx context.Context, in *HistoryReq, opts ...grpc.CallOption) (*OKResp, error) // Ping is a health-check endpoint. Ping(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*OKResp, error) } @@ -44,6 +46,11 @@ func (m *defaultBlockchainService) ListSymbols(ctx context.Context, in *ListSymb return client.ListSymbols(ctx, in, opts...) } +func (m *defaultBlockchainService) GetHistoryKlineData(ctx context.Context, in *HistoryReq, opts ...grpc.CallOption) (*OKResp, error) { + client := app_cloudep_blockchain.NewBlockchainServiceClient(m.cli.Conn()) + return client.GetHistoryKlineData(ctx, in, opts...) +} + // Ping is a health-check endpoint. func (m *defaultBlockchainService) Ping(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*OKResp, error) { client := app_cloudep_blockchain.NewBlockchainServiceClient(m.cli.Conn()) diff --git a/etc/blockchain.yaml b/etc/blockchain.yaml index 0a400f4..9d6ba1c 100644 --- a/etc/blockchain.yaml +++ b/etc/blockchain.yaml @@ -12,12 +12,12 @@ Binance: WorkerSize: 20 RedisCluster: - Host: 127.0.0.1:6379 + Host: 10.0.0.13:6379 Type: node Cassandra: Hosts: - - 127.0.0.1 + - 10.0.0.13 Port: 9042 Keyspace: digimon UseAuth: true diff --git a/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain/blockchain.pb.go b/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain/blockchain.pb.go index 2c392ca..26c7afc 100644 --- a/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain/blockchain.pb.go +++ b/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain/blockchain.pb.go @@ -1,18 +1,16 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 -// protoc v3.21.12 +// protoc-gen-go v1.36.1 +// protoc v3.19.4 // source: generate/rpc/blockchain.proto package app_cloudep_blockchain import ( - reflect "reflect" - sync "sync" - unsafe "unsafe" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) const ( @@ -264,58 +262,159 @@ func (x *Symbol) GetQuoteAssetPrecision() int32 { return 0 } +type HistoryReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Symbol string `protobuf:"bytes,1,opt,name=symbol,proto3" json:"symbol,omitempty"` // 交易對名稱 (BTCUSDT) + Interval string `protobuf:"bytes,2,opt,name=interval,proto3" json:"interval,omitempty"` // 4h 1m 1d ... + StartTime string `protobuf:"bytes,3,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` + EndTime string `protobuf:"bytes,4,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HistoryReq) Reset() { + *x = HistoryReq{} + mi := &file_generate_rpc_blockchain_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HistoryReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HistoryReq) ProtoMessage() {} + +func (x *HistoryReq) ProtoReflect() protoreflect.Message { + mi := &file_generate_rpc_blockchain_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HistoryReq.ProtoReflect.Descriptor instead. +func (*HistoryReq) Descriptor() ([]byte, []int) { + return file_generate_rpc_blockchain_proto_rawDescGZIP(), []int{5} +} + +func (x *HistoryReq) GetSymbol() string { + if x != nil { + return x.Symbol + } + return "" +} + +func (x *HistoryReq) GetInterval() string { + if x != nil { + return x.Interval + } + return "" +} + +func (x *HistoryReq) GetStartTime() string { + if x != nil { + return x.StartTime + } + return "" +} + +func (x *HistoryReq) GetEndTime() string { + if x != nil { + return x.EndTime + } + return "" +} + var File_generate_rpc_blockchain_proto protoreflect.FileDescriptor -const file_generate_rpc_blockchain_proto_rawDesc = "" + - "\n" + - "\x1dgenerate/rpc/blockchain.proto\x12\n" + - "blockchain\"\b\n" + - "\x06OKResp\"\t\n" + - "\aNoneReq\"\x14\n" + - "\x12ListSymbolsRequest\"C\n" + - "\x13ListSymbolsResponse\x12,\n" + - "\asymbols\x18\x01 \x03(\v2\x12.blockchain.SymbolR\asymbols\"\xde\x01\n" + - "\x06Symbol\x12\x16\n" + - "\x06symbol\x18\x01 \x01(\tR\x06symbol\x12\x16\n" + - "\x06status\x18\x02 \x01(\tR\x06status\x12\x1d\n" + - "\n" + - "base_asset\x18\x03 \x01(\tR\tbaseAsset\x120\n" + - "\x14base_asset_precision\x18\x04 \x01(\x05R\x12baseAssetPrecision\x12\x1f\n" + - "\vquote_asset\x18\x05 \x01(\tR\n" + - "quoteAsset\x122\n" + - "\x15quote_asset_precision\x18\x06 \x01(\x05R\x13quoteAssetPrecision2\x94\x01\n" + - "\x11BlockchainService\x12N\n" + - "\vListSymbols\x12\x1e.blockchain.ListSymbolsRequest\x1a\x1f.blockchain.ListSymbolsResponse\x12/\n" + - "\x04Ping\x12\x13.blockchain.NoneReq\x1a\x12.blockchain.OKRespB.Z,code.30cm.net/digimon/app-cloudep-blockchainb\x06proto3" +var file_generate_rpc_blockchain_proto_rawDesc = []byte{ + 0x0a, 0x1d, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x62, + 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x22, 0x08, 0x0a, 0x06, 0x4f, + 0x4b, 0x52, 0x65, 0x73, 0x70, 0x22, 0x09, 0x0a, 0x07, 0x4e, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x71, + 0x22, 0x14, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x43, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x79, + 0x6d, 0x62, 0x6f, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, + 0x07, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, + 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2e, 0x53, 0x79, 0x6d, 0x62, + 0x6f, 0x6c, 0x52, 0x07, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x73, 0x22, 0xde, 0x01, 0x0a, 0x06, + 0x53, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x12, 0x16, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x61, 0x73, 0x65, + 0x41, 0x73, 0x73, 0x65, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x61, 0x73, + 0x73, 0x65, 0x74, 0x5f, 0x70, 0x72, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x12, 0x62, 0x61, 0x73, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x50, 0x72, + 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x71, 0x75, 0x6f, 0x74, 0x65, + 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x71, 0x75, + 0x6f, 0x74, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x71, 0x75, 0x6f, 0x74, + 0x65, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x70, 0x72, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x41, 0x73, + 0x73, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x7a, 0x0a, 0x0a, + 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x79, + 0x6d, 0x62, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x79, 0x6d, 0x62, + 0x6f, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x1d, + 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, + 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x32, 0xd7, 0x01, 0x0a, 0x11, 0x42, 0x6c, 0x6f, + 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4e, + 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x73, 0x12, 0x1e, 0x2e, + 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, + 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, + 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, + 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, + 0x0a, 0x13, 0x47, 0x65, 0x74, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x4b, 0x6c, 0x69, 0x6e, + 0x65, 0x44, 0x61, 0x74, 0x61, 0x12, 0x16, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, + 0x69, 0x6e, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, + 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2e, 0x4f, 0x4b, 0x52, 0x65, 0x73, + 0x70, 0x12, 0x2f, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x13, 0x2e, 0x62, 0x6c, 0x6f, 0x63, + 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2e, 0x4e, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x12, + 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2e, 0x4f, 0x4b, 0x52, 0x65, + 0x73, 0x70, 0x42, 0x2e, 0x5a, 0x2c, 0x63, 0x6f, 0x64, 0x65, 0x2e, 0x33, 0x30, 0x63, 0x6d, 0x2e, + 0x6e, 0x65, 0x74, 0x2f, 0x64, 0x69, 0x67, 0x69, 0x6d, 0x6f, 0x6e, 0x2f, 0x61, 0x70, 0x70, 0x2d, + 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x65, 0x70, 0x2d, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, + 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} var ( file_generate_rpc_blockchain_proto_rawDescOnce sync.Once - file_generate_rpc_blockchain_proto_rawDescData []byte + file_generate_rpc_blockchain_proto_rawDescData = file_generate_rpc_blockchain_proto_rawDesc ) func file_generate_rpc_blockchain_proto_rawDescGZIP() []byte { file_generate_rpc_blockchain_proto_rawDescOnce.Do(func() { - file_generate_rpc_blockchain_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_generate_rpc_blockchain_proto_rawDesc), len(file_generate_rpc_blockchain_proto_rawDesc))) + file_generate_rpc_blockchain_proto_rawDescData = protoimpl.X.CompressGZIP(file_generate_rpc_blockchain_proto_rawDescData) }) return file_generate_rpc_blockchain_proto_rawDescData } -var file_generate_rpc_blockchain_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_generate_rpc_blockchain_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_generate_rpc_blockchain_proto_goTypes = []any{ (*OKResp)(nil), // 0: blockchain.OKResp (*NoneReq)(nil), // 1: blockchain.NoneReq (*ListSymbolsRequest)(nil), // 2: blockchain.ListSymbolsRequest (*ListSymbolsResponse)(nil), // 3: blockchain.ListSymbolsResponse (*Symbol)(nil), // 4: blockchain.Symbol + (*HistoryReq)(nil), // 5: blockchain.HistoryReq } var file_generate_rpc_blockchain_proto_depIdxs = []int32{ 4, // 0: blockchain.ListSymbolsResponse.symbols:type_name -> blockchain.Symbol 2, // 1: blockchain.BlockchainService.ListSymbols:input_type -> blockchain.ListSymbolsRequest - 1, // 2: blockchain.BlockchainService.Ping:input_type -> blockchain.NoneReq - 3, // 3: blockchain.BlockchainService.ListSymbols:output_type -> blockchain.ListSymbolsResponse - 0, // 4: blockchain.BlockchainService.Ping:output_type -> blockchain.OKResp - 3, // [3:5] is the sub-list for method output_type - 1, // [1:3] is the sub-list for method input_type + 5, // 2: blockchain.BlockchainService.GetHistoryKlineData:input_type -> blockchain.HistoryReq + 1, // 3: blockchain.BlockchainService.Ping:input_type -> blockchain.NoneReq + 3, // 4: blockchain.BlockchainService.ListSymbols:output_type -> blockchain.ListSymbolsResponse + 0, // 5: blockchain.BlockchainService.GetHistoryKlineData:output_type -> blockchain.OKResp + 0, // 6: blockchain.BlockchainService.Ping:output_type -> blockchain.OKResp + 4, // [4:7] is the sub-list for method output_type + 1, // [1:4] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name 1, // [1:1] is the sub-list for extension extendee 0, // [0:1] is the sub-list for field type_name @@ -330,9 +429,9 @@ func file_generate_rpc_blockchain_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_generate_rpc_blockchain_proto_rawDesc), len(file_generate_rpc_blockchain_proto_rawDesc)), + RawDescriptor: file_generate_rpc_blockchain_proto_rawDesc, NumEnums: 0, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 1, }, @@ -341,6 +440,7 @@ func file_generate_rpc_blockchain_proto_init() { MessageInfos: file_generate_rpc_blockchain_proto_msgTypes, }.Build() File_generate_rpc_blockchain_proto = out.File + file_generate_rpc_blockchain_proto_rawDesc = nil file_generate_rpc_blockchain_proto_goTypes = nil file_generate_rpc_blockchain_proto_depIdxs = nil } diff --git a/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain/blockchain_grpc.pb.go b/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain/blockchain_grpc.pb.go index d0af9f0..5855f47 100644 --- a/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain/blockchain_grpc.pb.go +++ b/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain/blockchain_grpc.pb.go @@ -1,14 +1,13 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v3.21.12 +// - protoc v3.19.4 // source: generate/rpc/blockchain.proto package app_cloudep_blockchain import ( context "context" - grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" @@ -20,8 +19,9 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - BlockchainService_ListSymbols_FullMethodName = "/blockchain.BlockchainService/ListSymbols" - BlockchainService_Ping_FullMethodName = "/blockchain.BlockchainService/Ping" + BlockchainService_ListSymbols_FullMethodName = "/blockchain.BlockchainService/ListSymbols" + BlockchainService_GetHistoryKlineData_FullMethodName = "/blockchain.BlockchainService/GetHistoryKlineData" + BlockchainService_Ping_FullMethodName = "/blockchain.BlockchainService/Ping" ) // BlockchainServiceClient is the client API for BlockchainService service. @@ -30,6 +30,7 @@ const ( type BlockchainServiceClient interface { // ListSymbols retrieves all available trading symbols. ListSymbols(ctx context.Context, in *ListSymbolsRequest, opts ...grpc.CallOption) (*ListSymbolsResponse, error) + GetHistoryKlineData(ctx context.Context, in *HistoryReq, opts ...grpc.CallOption) (*OKResp, error) // Ping is a health-check endpoint. Ping(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*OKResp, error) } @@ -52,6 +53,16 @@ func (c *blockchainServiceClient) ListSymbols(ctx context.Context, in *ListSymbo return out, nil } +func (c *blockchainServiceClient) GetHistoryKlineData(ctx context.Context, in *HistoryReq, opts ...grpc.CallOption) (*OKResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OKResp) + err := c.cc.Invoke(ctx, BlockchainService_GetHistoryKlineData_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *blockchainServiceClient) Ping(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*OKResp, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(OKResp) @@ -68,6 +79,7 @@ func (c *blockchainServiceClient) Ping(ctx context.Context, in *NoneReq, opts .. type BlockchainServiceServer interface { // ListSymbols retrieves all available trading symbols. ListSymbols(context.Context, *ListSymbolsRequest) (*ListSymbolsResponse, error) + GetHistoryKlineData(context.Context, *HistoryReq) (*OKResp, error) // Ping is a health-check endpoint. Ping(context.Context, *NoneReq) (*OKResp, error) mustEmbedUnimplementedBlockchainServiceServer() @@ -83,6 +95,9 @@ type UnimplementedBlockchainServiceServer struct{} func (UnimplementedBlockchainServiceServer) ListSymbols(context.Context, *ListSymbolsRequest) (*ListSymbolsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListSymbols not implemented") } +func (UnimplementedBlockchainServiceServer) GetHistoryKlineData(context.Context, *HistoryReq) (*OKResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetHistoryKlineData not implemented") +} func (UnimplementedBlockchainServiceServer) Ping(context.Context, *NoneReq) (*OKResp, error) { return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented") } @@ -125,6 +140,24 @@ func _BlockchainService_ListSymbols_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } +func _BlockchainService_GetHistoryKlineData_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HistoryReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(BlockchainServiceServer).GetHistoryKlineData(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: BlockchainService_GetHistoryKlineData_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(BlockchainServiceServer).GetHistoryKlineData(ctx, req.(*HistoryReq)) + } + return interceptor(ctx, in, info, handler) +} + func _BlockchainService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(NoneReq) if err := dec(in); err != nil { @@ -154,6 +187,10 @@ var BlockchainService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListSymbols", Handler: _BlockchainService_ListSymbols_Handler, }, + { + MethodName: "GetHistoryKlineData", + Handler: _BlockchainService_GetHistoryKlineData_Handler, + }, { MethodName: "Ping", Handler: _BlockchainService_Ping_Handler, diff --git a/generate/rpc/blockchain.proto b/generate/rpc/blockchain.proto index 333e7e5..8de42f1 100644 --- a/generate/rpc/blockchain.proto +++ b/generate/rpc/blockchain.proto @@ -26,9 +26,17 @@ message Symbol { int32 quote_asset_precision = 6; // 報價資產顯示的小數位數 } +message HistoryReq { + string symbol = 1; // 交易對名稱 (BTCUSDT) + string interval =2; // 4h 1m 1d ... + string start_time=3; + string end_time=4; +} + service BlockchainService{ // ListSymbols retrieves all available trading symbols. rpc ListSymbols(ListSymbolsRequest) returns(ListSymbolsResponse); + rpc GetHistoryKlineData(HistoryReq)returns(OKResp); // Ping is a health-check endpoint. rpc Ping(NoneReq) returns(OKResp); } diff --git a/internal/domain/blockchain/errors.go b/internal/domain/blockchain/errors.go index 4e2c601..760760f 100644 --- a/internal/domain/blockchain/errors.go +++ b/internal/domain/blockchain/errors.go @@ -7,4 +7,5 @@ const CodeBlockchain uint32 = 10 const ( FailedToGetSymbolFormBinanceErrorCode errs.ErrorCode = 1 FailedToUpsertBinanceErrorCode errs.ErrorCode = 2 + FailedToListBinanceKlineErrorCode errs.ErrorCode = 3 ) diff --git a/internal/domain/repository/data_source.go b/internal/domain/repository/data_source.go index 1a98be4..b695de7 100644 --- a/internal/domain/repository/data_source.go +++ b/internal/domain/repository/data_source.go @@ -14,11 +14,12 @@ type Downloader interface { // FetchHistoryKline 抓歷史 K 線資料 FetchHistoryKline(ctx context.Context, param QueryKline) ([]*entity.Kline, error) SaveHistoryKline(ctx context.Context, data []*entity.Kline) error + GetKline(ctx context.Context, param QueryKline) ([]entity.Kline, error) } type QueryKline struct { - Symbol string - Interval string - StartUnixNano int64 - EndUnixNano int64 + Symbol string + Interval string + StartTime int64 + EndTime int64 } diff --git a/internal/domain/usecase/data_source.go b/internal/domain/usecase/data_source.go index 50cba57..4601f7c 100644 --- a/internal/domain/usecase/data_source.go +++ b/internal/domain/usecase/data_source.go @@ -7,6 +7,7 @@ import ( type DataSourceUseCase interface { GetSymbols(ctx context.Context) ([]*Symbol, error) UpsertKline(ctx context.Context, data QueryKline) error + ListKline(ctx context.Context, param QueryKline) ([]Candle, error) } // Symbol 代表交易對資訊 @@ -20,8 +21,25 @@ type Symbol struct { } type QueryKline struct { - Symbol string - Interval string - StartUnixNano int64 - EndUnixNano int64 + Symbol string + Interval string + StartTime int64 + EndTime int64 +} + +type Candle struct { + OpenTime int64 `json:"open_time"` // 開盤時間(毫秒),clustering key,用於時序查詢 + Open string `json:"open"` // 開盤價 + High string `json:"high"` // 最高價 + Low string `json:"low"` // 最低價 + Close string `json:"close"` // 收盤價 + Volume string `json:"volume" ` // 成交量 + CloseTime int64 `json:"close_time" ` // 收盤時間(毫秒) + QuoteAssetVolume string `json:"quote_asset_volume" ` // 成交額(以報價資產計) + NumberOfTrades int `json:"number_of_trades"` // 交易筆數 + TakerBuyBaseAssetVolume string `json:"taker_buy_base_asset_volume"` // 主動買入成交量 + TakerBuyQuoteAssetVolume string `json:"taker_buy_quote_asset_volume"` // 主動買入成交額 + Symbol string `json:"symbol"` // 交易對,partition key + // K 線時間區間,partition key // 12h,15m,1d,1h,1m,1s,2h,30m,3m,4h,5m,6h,8h + Interval string `json:"interval"` } diff --git a/internal/lib/strategy/ema.go b/internal/lib/strategy/ema.go new file mode 100644 index 0000000..e2c704c --- /dev/null +++ b/internal/lib/strategy/ema.go @@ -0,0 +1,27 @@ +package strategy + +import "github.com/shopspring/decimal" + +/************** EMA 指數移動平均 **************/ +type EMA struct { + n int + alp decimal.Decimal // 平滑係數 α = 2 / (n + 1) + val decimal.Decimal // 當前EMA值 + ok bool +} + +func NewEMA(n int) *EMA { + return &EMA{n: n, alp: decimal.NewFromFloat(2.0).Div(decimal.NewFromInt(int64(n + 1)))} +} + +func (e *EMA) Push(close decimal.Decimal) (decimal.Decimal, bool) { + if !e.ok { + // 第一筆資料直接當作EMA初始值 + e.val = close + e.ok = true + return e.val, false + } + // EMA計算公式 + e.val = e.alp.Mul(close).Add(decimal.NewFromInt(1).Sub(e.alp).Mul(e.val)) + return e.val, true +} diff --git a/internal/lib/strategy/entity.go b/internal/lib/strategy/entity.go new file mode 100644 index 0000000..0118a4d --- /dev/null +++ b/internal/lib/strategy/entity.go @@ -0,0 +1,17 @@ +package strategy + +import ( + "github.com/shopspring/decimal" + "time" +) + +// CandleForStrategy 表示一根K線資料 +type CandleForStrategy struct { + T time.Time // T:時間 + // O:開盤價 + // H:最高價 + // L:最低價 + // C:收盤價 + O, H, L, C decimal.Decimal + V decimal.Decimal // V:成交量 +} diff --git a/internal/lib/strategy/ring_queue.go b/internal/lib/strategy/ring_queue.go new file mode 100644 index 0000000..b7b8a02 --- /dev/null +++ b/internal/lib/strategy/ring_queue.go @@ -0,0 +1,50 @@ +package strategy + +import ( + "container/list" + "github.com/shopspring/decimal" +) + +/************** 基礎的固定長度隊列,用來計算移動平均 **************/ +type ringQD struct { + N int // 窗口大小(需要保留的資料數量) + l *list.List // 用於儲存資料的雙向鏈表 + sum decimal.Decimal // 當前窗口的總和,方便快速計算平均值 +} + +// 建立一個固定長度的隊列 +func newRingQD(n int) *ringQD { + return &ringQD{N: n, l: list.New(), sum: decimal.Zero} +} + +// push:將新的數值放入隊列,並維護總和 +func (q *ringQD) push(x decimal.Decimal) { + q.l.PushBack(x) + q.sum = q.sum.Add(x) + // 如果超出最大長度,移除最舊的數值 + if q.l.Len() > q.N { + f := q.l.Front() + q.sum = q.sum.Sub(f.Value.(decimal.Decimal)) + q.l.Remove(f) + } +} + +// ready:判斷隊列是否已經填滿 +func (q *ringQD) ready() bool { return q.l.Len() == q.N } + +// mean:計算平均值 +func (q *ringQD) mean() decimal.Decimal { + if q.l.Len() == 0 { + return decimal.Zero + } + return q.sum.Div(decimal.NewFromInt(int64(q.l.Len()))) +} + +// values:返回隊列中所有的值(複製一份,不影響原資料) +func (q *ringQD) values() []decimal.Decimal { + out := make([]decimal.Decimal, 0, q.l.Len()) + for e := q.l.Front(); e != nil; e = e.Next() { + out = append(out, e.Value.(decimal.Decimal)) + } + return out +} diff --git a/internal/lib/strategy/ring_queue_test.go b/internal/lib/strategy/ring_queue_test.go new file mode 100644 index 0000000..f1f1094 --- /dev/null +++ b/internal/lib/strategy/ring_queue_test.go @@ -0,0 +1,129 @@ +package strategy + +import ( + "github.com/shopspring/decimal" + "reflect" + "testing" +) + +// --- 表格式驅動測試 (Table-Driven Test) --- + +func TestRingQD(t *testing.T) { + // 為了方便測試,預先建立幾個 decimal 數值 + d10 := decimal.NewFromInt(10) + d20 := decimal.NewFromInt(20) + d30 := decimal.NewFromInt(30) + d40 := decimal.NewFromInt(40) + d50 := decimal.NewFromInt(50) + d_neg5 := decimal.NewFromInt(-5) + + // 定義測試案例的結構 + testCases := []struct { + name string // 測試案例的名稱 + n int // ringQD 的大小 + inputs []decimal.Decimal // 輸入的數值序列 + wantSum decimal.Decimal // 預期的總和 + wantMean decimal.Decimal // 預期的平均值 + wantValues []decimal.Decimal // 預期隊列中最後的值 + wantReady bool // 預期的就緒狀態 + }{ + { + name: "未滿載的情況", + n: 5, + inputs: []decimal.Decimal{d10, d20}, + wantSum: decimal.NewFromInt(30), + wantMean: decimal.NewFromInt(15), + wantValues: []decimal.Decimal{d10, d20}, + wantReady: false, + }, + { + name: "剛好滿載的情況", + n: 3, + inputs: []decimal.Decimal{d10, d20, d30}, + wantSum: decimal.NewFromInt(60), + wantMean: decimal.NewFromInt(20), + wantValues: []decimal.Decimal{d10, d20, d30}, + wantReady: true, + }, + { + name: "超出容量,舊資料被移除", + n: 3, + inputs: []decimal.Decimal{d10, d20, d30, d40, d50}, + wantSum: decimal.NewFromInt(120), // 30 + 40 + 50 + wantMean: decimal.NewFromInt(40), + wantValues: []decimal.Decimal{d30, d40, d50}, + wantReady: true, + }, + { + name: "包含零與負數", + n: 4, + inputs: []decimal.Decimal{d10, d_neg5, decimal.Zero, d30, d_neg5}, + wantSum: decimal.NewFromInt(20), // -5 + 0 + 30 + (-5) + wantMean: decimal.NewFromInt(5), + wantValues: []decimal.Decimal{d_neg5, decimal.Zero, d30, d_neg5}, + wantReady: true, + }, + { + name: "初始為空", + n: 5, + inputs: []decimal.Decimal{}, + wantSum: decimal.Zero, + wantMean: decimal.Zero, + wantValues: []decimal.Decimal{}, + wantReady: false, + }, + { + name: "N 為 1 的邊界情況", + n: 1, + inputs: []decimal.Decimal{d10, d20, d30}, + wantSum: d30, + wantMean: d30, + wantValues: []decimal.Decimal{d30}, + wantReady: true, + }, + { + name: "N 為 0 的無效情況", + n: 0, + inputs: []decimal.Decimal{d10, d20, d30}, + wantSum: decimal.Zero, + wantMean: decimal.Zero, + wantValues: []decimal.Decimal{}, + wantReady: true, + }, + } + + // 遍歷所有測試案例 + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // 針對每個案例建立一個新的 ringQD + q := newRingQD(tc.n) + + // 依序推入資料 + for _, val := range tc.inputs { + q.push(val) + } + + // 驗證總和 + if !q.sum.Equals(tc.wantSum) { + t.Errorf("Sum 錯誤:got %v, want %v", q.sum, tc.wantSum) + } + + // 驗證平均值 + gotMean := q.mean() + if !gotMean.Equals(tc.wantMean) { + t.Errorf("Mean 錯誤:got %v, want %v", gotMean, tc.wantMean) + } + + // 驗證就緒狀態 + if q.ready() != tc.wantReady { + t.Errorf("Ready 狀態錯誤:got %v, want %v", q.ready(), tc.wantReady) + } + + // 驗證隊列中的值 + gotValues := q.values() + if !reflect.DeepEqual(gotValues, tc.wantValues) { + t.Errorf("Values 錯誤:got %v, want %v", gotValues, tc.wantValues) + } + }) + } +} diff --git a/internal/lib/strategy/sma.go b/internal/lib/strategy/sma.go new file mode 100644 index 0000000..9aafdbb --- /dev/null +++ b/internal/lib/strategy/sma.go @@ -0,0 +1,45 @@ +package strategy + +import "github.com/shopspring/decimal" + +/* SMA (Simple Moving Average) 簡單移動平均線。 它透過計算一段時間內股價的平均值,來判斷趨勢和提供交易信號。 + +SMA 的計算方式: + SMA 簡單地將一段時間內的收盤價加總,再除以該期間的交易日數。 例如,5 日SMA 就是將過去 5 個交易日的收盤價加總後除以 5。 +SMA 的應用: + 1. 趨勢判斷: + SMA 可以用來判斷價格的趨勢。 當股價在 SMA 上方,且 SMA 向上移動時,表示股價處於上升趨勢; + 反之,當股價在 SMA 下方,且SMA 向下移動時,表示股價處於下降趨勢。 + 2. 支撐與阻力: + SMA 也可以被視為支撐位和阻力位。 當股價下跌到SMA 附近時,SMA 可能會提供支撐;而當股價上漲到SMA 附近時,SMA 可能會提供阻力。 + 3. 交易信號: + 移動平均線的交叉也可以產生交易信號。 例如,當短期SMA 線向上穿越長期SMA 線時,被稱為黃金交叉,可能是一個買入信號; + 反之,當短期SMA 線向下穿越長期SMA 線時,被稱為死亡交叉,可能是一個賣出信號。 +SMA 的優缺點: + 優點: SMA 計算簡單、易於理解,適合新手使用。 + 缺點: SMA 對於價格變動的反應較為遲鈍,可能會落後於市場,特別是短期波動時,可能不如其他移動平均線指標準確。 +*/ + +type SMA struct { + q *ringQD +} + +// NewSMA 建立SMA計算器 +func NewSMA(n int) *SMA { return &SMA{q: newRingQD(n)} } + +// Push 輸入收盤價,返回當前SMA值 +func (s *SMA) Push(close decimal.Decimal) (decimal.Decimal, bool) { + s.q.push(close) + if !s.q.ready() { + return decimal.Zero, false // 尚未湊滿資料 + } + return s.q.mean(), true +} + +// GetSMA 取得目前 SMA 值 +func (s *SMA) GetSMA() (decimal.Decimal, bool) { + if !s.q.ready() { + return decimal.Zero, false // 尚未湊滿資料 + } + return s.q.mean(), true +} diff --git a/internal/logic/blockchainservice/get_history_kline_data_logic.go b/internal/logic/blockchainservice/get_history_kline_data_logic.go new file mode 100644 index 0000000..f158fd0 --- /dev/null +++ b/internal/logic/blockchainservice/get_history_kline_data_logic.go @@ -0,0 +1,42 @@ +package blockchainservicelogic + +import ( + "blockchain/internal/domain/usecase" + "context" + "time" + + "blockchain/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain" + "blockchain/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetHistoryKlineDataLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetHistoryKlineDataLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetHistoryKlineDataLogic { + return &GetHistoryKlineDataLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +func (l *GetHistoryKlineDataLogic) GetHistoryKlineData(in *app_cloudep_blockchain.HistoryReq) (*app_cloudep_blockchain.OKResp, error) { + go func() { + st, _ := time.Parse(time.RFC3339, in.GetStartTime()) + et, _ := time.Parse(time.RFC3339, in.GetEndTime()) + _ = l.svcCtx.BinanceDataSource.UpsertKline(context.Background(), usecase.QueryKline{ + Symbol: in.GetSymbol(), + Interval: in.GetInterval(), + StartTime: st.UnixNano(), + EndTime: et.UnixNano(), + }) + return + }() + + return &app_cloudep_blockchain.OKResp{}, nil +} diff --git a/internal/logic/blockchainservice/ping_logic.go b/internal/logic/blockchainservice/ping_logic.go index 27dbe49..35abdfa 100644 --- a/internal/logic/blockchainservice/ping_logic.go +++ b/internal/logic/blockchainservice/ping_logic.go @@ -1,12 +1,11 @@ package blockchainservicelogic import ( - "blockchain/internal/domain/usecase" - "context" - "time" - app_cloudep_blockchain "blockchain/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain" + "blockchain/internal/domain/usecase" "blockchain/internal/svc" + "context" + "fmt" "github.com/zeromicro/go-zero/core/logx" ) @@ -26,11 +25,12 @@ func NewPingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PingLogic { } func (l *PingLogic) Ping(_ *app_cloudep_blockchain.NoneReq) (*app_cloudep_blockchain.OKResp, error) { - err := l.svcCtx.BinanceDataSource.UpsertKline(l.ctx, usecase.QueryKline{ - Symbol: "BTCUSDT", - Interval: "1m", - StartUnixNano: time.Date(2024, 8, 1, 0, 0, 0, 0, time.UTC).UnixNano(), - EndUnixNano: time.Date(2025, 8, 2, 0, 0, 0, 0, time.UTC).UnixNano(), + + kline, err := l.svcCtx.BinanceDataSource.ListKline(l.ctx, usecase.QueryKline{ + Symbol: "ETHUSDT", + Interval: "1d", + StartTime: 1502928000000, + EndTime: 1503964800000, }) if err != nil { return nil, err diff --git a/internal/repository/data_source_binance.go b/internal/repository/data_source_binance.go index bca47bf..c1724e3 100644 --- a/internal/repository/data_source_binance.go +++ b/internal/repository/data_source_binance.go @@ -12,6 +12,7 @@ import ( "encoding/csv" "fmt" "github.com/goccy/go-json" + "github.com/scylladb/gocqlx/v3/qb" "io" "net/http" "os" @@ -140,38 +141,24 @@ func (repo *BinanceRepository) GetSymbols(ctx context.Context) ([]*entity.Symbol } func (repo *BinanceRepository) FetchHistoryKline(ctx context.Context, param repository.QueryKline) ([]*entity.Kline, error) { - ch := make(chan []*entity.Kline, repo.workerSize) - var wg sync.WaitGroup + start := time.Unix(0, param.StartTime) + end := time.Unix(0, param.EndTime) + + var allKLines []*entity.Kline - start := time.Unix(0, param.StartUnixNano) - end := time.Unix(0, param.EndUnixNano) // 產生所有天的任務 for d := start; !d.After(end); d = d.AddDate(0, 0, 1) { day := d - wg.Add(1) - _ = repo.workers.Submit(func() { - defer wg.Done() - klines, err := repo.fetchHistoryKline(ctx, param.Symbol, param.Interval, day.Format(time.DateOnly)) - if err == nil && len(klines) > 0 { - ch <- klines // 只要拿到資料就丟進 channel - } - // 沒資料不用丟,避免 nil append - }) + a, err := repo.fetchHistoryKline(ctx, param.Symbol, param.Interval, day.Format(time.DateOnly)) + if err != nil { + logx.Errorf("failed to get history of kline : %v", err) + + continue + } + allKLines = append(allKLines, a...) } - // 等全部任務完成再關閉 channel - go func() { - wg.Wait() - close(ch) - }() - - // 收集所有 K 線 - var allKlines []*entity.Kline - for klines := range ch { - allKlines = append(allKlines, klines...) - } - - return allKlines, nil + return allKLines, nil } func (repo *BinanceRepository) SaveHistoryKline(ctx context.Context, data []*entity.Kline) error { @@ -187,7 +174,6 @@ func (repo *BinanceRepository) SaveHistoryKline(ctx context.Context, data []*ent go func(k *entity.Kline) { defer wg.Done() defer func() { <-ch }() - if err := repo.db.Insert(ctx, k, repo.KeySpace); err != nil { mu.Lock() errList = append(errList, err) @@ -206,6 +192,35 @@ func (repo *BinanceRepository) SaveHistoryKline(ctx context.Context, data []*ent return nil } +func (repo *BinanceRepository) GetKline(ctx context.Context, param repository.QueryKline) ([]entity.Kline, error) { + e := entity.Kline{} + stmt, names := qb.Select(fmt.Sprintf("%s.%s", repo.KeySpace, e.TableName())). + Columns( + "symbol", "interval", "open_time", "open", "high", "low", "close", + "volume", "close_time", "quote_asset_volume", "number_of_trades", + "taker_buy_base_asset_volume", "taker_buy_quote_asset_volume", + ). + Where( + qb.Eq("symbol"), + qb.Eq("interval"), + qb.GtOrEqNamed("open_time", "start"), + qb.LtOrEqNamed("open_time", "end"), + ).ToCql() + + var result []entity.Kline + err := repo.db.GetSession().Query(stmt, names).BindMap(qb.M{ + "symbol": param.Symbol, + "interval": param.Interval, + "start": param.StartTime, + "end": param.EndTime, + }).SelectRelease(&result) + if err != nil { + return nil, err + } + + return result, nil +} + // ============= func (repo *BinanceRepository) getSymbolsFromSource(ctx context.Context) ([]binance.Symbol, error) { if repo.Client == nil { @@ -229,11 +244,7 @@ func (repo *BinanceRepository) fetchHistoryKline(ctx context.Context, symbol str if err := check(ctx, url); err != nil { return nil, err } - // 這個 URL 只可能指向 binance.vision 官方站,已限定字串組合,不可能被用戶控制。 - // #nosec G107 - // 下載 zip - // 這個 URL 只可能指向 binance.vision 官方站,已限定字串組合,不可能被用戶控制。 - // #nosec G107 + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err @@ -274,7 +285,6 @@ func (repo *BinanceRepository) fetchHistoryKline(ctx context.Context, symbol str "quote_asset_volume", "number_of_trades", "taker_buy_base_asset_volume", "taker_buy_quote_asset_volume", "ignore", } - for _, f := range r.File { rc, err := f.Open() if err != nil { @@ -293,11 +303,11 @@ func (repo *BinanceRepository) fetchHistoryKline(ctx context.Context, symbol str if err != nil || len(record) < 12 { continue } + logx.Infof("record: %s", record) _ = writer.Write(record) } writer.Flush() rc.Close() - // csvutil parse var klines []*entity.Kline if err := csvutil.Unmarshal(buf.Bytes(), &klines); err != nil { diff --git a/internal/repository/data_source_binance_test.go b/internal/repository/data_source_binance_test.go index a4eb74f..25dc106 100644 --- a/internal/repository/data_source_binance_test.go +++ b/internal/repository/data_source_binance_test.go @@ -100,10 +100,10 @@ func TestSymbol(t *testing.T) { }) k, err := repo.FetchHistoryKline(context.Background(), repository.QueryKline{ - Symbol: "BTCUSDT", - Interval: "1m", - StartUnixNano: time.Date(2025, 8, 3, 0, 0, 0, 0, time.UTC).UnixNano(), - EndUnixNano: time.Date(2025, 8, 4, 0, 0, 0, 0, time.UTC).UnixNano(), + Symbol: "BTCUSDT", + Interval: "1m", + StartTime: time.Date(2025, 8, 3, 0, 0, 0, 0, time.UTC).UnixNano(), + EndTime: time.Date(2025, 8, 4, 0, 0, 0, 0, time.UTC).UnixNano(), }) assert.NoError(t, err) for _, item := range k { diff --git a/internal/server/blockchainservice/blockchain_service_server.go b/internal/server/blockchainservice/blockchain_service_server.go index 5ea0248..18536d8 100644 --- a/internal/server/blockchainservice/blockchain_service_server.go +++ b/internal/server/blockchainservice/blockchain_service_server.go @@ -1,5 +1,5 @@ // Code generated by goctl. DO NOT EDIT. -// goctl 1.8.5 +// goctl 1.8.1 // Source: blockchain.proto package server @@ -7,8 +7,8 @@ package server import ( "context" - app_cloudep_blockchain "blockchain/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain" - blockchainservicelogic "blockchain/internal/logic/blockchainservice" + "blockchain/gen_result/pb/code.30cm.net/digimon/app-cloudep-blockchain" + "blockchain/internal/logic/blockchainservice" "blockchain/internal/svc" ) @@ -29,6 +29,11 @@ func (s *BlockchainServiceServer) ListSymbols(ctx context.Context, in *app_cloud return l.ListSymbols(in) } +func (s *BlockchainServiceServer) GetHistoryKlineData(ctx context.Context, in *app_cloudep_blockchain.HistoryReq) (*app_cloudep_blockchain.OKResp, error) { + l := blockchainservicelogic.NewGetHistoryKlineDataLogic(ctx, s.svcCtx) + return l.GetHistoryKlineData(in) +} + // Ping is a health-check endpoint. func (s *BlockchainServiceServer) Ping(ctx context.Context, in *app_cloudep_blockchain.NoneReq) (*app_cloudep_blockchain.OKResp, error) { l := blockchainservicelogic.NewPingLogic(ctx, s.svcCtx) diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 19d9233..21a90f9 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -32,7 +32,7 @@ func NewServiceContext(c config.Config) *ServiceContext { DB: cassandra, KeySpace: c.Cassandra.Keyspace, }) - InitBinanceKLineWebsocket() + // InitBinanceKLineWebsocket() return &ServiceContext{ Config: c, BinanceRepo: binanceRepo, diff --git a/internal/usecase/binance.go b/internal/usecase/binance.go index 1cdea0b..e49ab2c 100644 --- a/internal/usecase/binance.go +++ b/internal/usecase/binance.go @@ -4,9 +4,8 @@ import ( "blockchain/internal/domain/blockchain" "blockchain/internal/domain/repository" "blockchain/internal/domain/usecase" - "context" - "code.30cm.net/digimon/library-go/errs" + "context" "github.com/zeromicro/go-zero/core/logx" ) @@ -58,10 +57,10 @@ func (use *BinanceUseCase) GetSymbols(ctx context.Context) ([]*usecase.Symbol, e func (use *BinanceUseCase) UpsertKline(ctx context.Context, data usecase.QueryKline) error { origianData, err := use.BinanceRepo.FetchHistoryKline(ctx, repository.QueryKline{ - Symbol: data.Symbol, - Interval: data.Interval, - StartUnixNano: data.StartUnixNano, - EndUnixNano: data.EndUnixNano, + Symbol: data.Symbol, + Interval: data.Interval, + StartTime: data.StartTime, + EndTime: data.EndTime, }) if err != nil { e := errs.ThirdPartyErrorL( @@ -94,3 +93,47 @@ func (use *BinanceUseCase) UpsertKline(ctx context.Context, data usecase.QueryKl return nil } + +func (use *BinanceUseCase) ListKline(ctx context.Context, param usecase.QueryKline) ([]usecase.Candle, error) { + kline, err := use.BinanceRepo.GetKline(ctx, repository.QueryKline{ + Symbol: param.Symbol, + Interval: param.Interval, + StartTime: param.StartTime, + EndTime: param.EndTime, + }) + + if err != nil { + e := errs.DatabaseErrorWithScopeL( + blockchain.CodeBlockchain, + blockchain.FailedToListBinanceKlineErrorCode, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "func", Value: "BinanceRepo.GetKline"}, + {Key: "err", Value: err.Error()}, + }, + "failed get kline data from db").Wrap(err) + + return nil, e + } + + candle := make([]usecase.Candle, 0, len(kline)) + for _, item := range kline { + candle = append(candle, usecase.Candle{ + OpenTime: item.OpenTime, + Open: item.Open, + High: item.High, + Low: item.Low, + Close: item.Close, + Volume: item.Volume, + CloseTime: item.CloseTime, + QuoteAssetVolume: item.QuoteAssetVolume, + NumberOfTrades: item.NumberOfTrades, + TakerBuyBaseAssetVolume: item.TakerBuyBaseAssetVolume, + TakerBuyQuoteAssetVolume: item.TakerBuyQuoteAssetVolume, + Symbol: item.Symbol, + Interval: item.Interval, + }) + } + + return candle, nil +}