Files
collector/internal/logic/storage.go

297 lines
10 KiB
Go
Raw Normal View History

2026-04-17 14:50:34 +08:00
package logic
2026-04-07 12:22:57 +08:00
import (
2026-04-08 11:21:40 +08:00
"encoding/json"
2026-04-07 12:22:57 +08:00
"fmt"
2026-04-17 19:56:22 +08:00
"log"
2026-04-07 12:22:57 +08:00
"time"
2026-04-17 14:50:34 +08:00
"git.apinb.com/quant/collector/internal/impl"
"git.apinb.com/quant/collector/internal/models"
"git.apinb.com/quant/collector/internal/types"
2026-04-07 12:22:57 +08:00
"gorm.io/gorm"
)
// SaveStatus 保存完整状态数据(使用事务)
2026-04-17 14:50:34 +08:00
func SaveData(status *types.StatusData) error {
2026-04-07 21:40:29 +08:00
// 验证必要字段 - AccountID是所有Upsert的共同条件
if status.Data.Assets.AccountID == "" {
return fmt.Errorf("账户ID不能为空")
}
2026-04-17 13:08:48 +08:00
// 计算Ymd (年月日数字格式,如20260407)
now := time.Now()
ymd := now.Year()*10000 + int(now.Month())*100 + now.Day()
// 保存资产快照 (先查询后更新/插入)
var existingAsset models.CollectorAssets
2026-04-17 14:50:34 +08:00
err := impl.DBService.Where("account_id = ? AND ymd = ?", status.Data.Assets.AccountID, ymd).First(&existingAsset).Error
2026-04-17 13:08:48 +08:00
asset := models.CollectorAssets{
AccountID: status.Data.Assets.AccountID,
Ymd: ymd,
Cash: status.Data.Assets.Cash,
FrozenCash: status.Data.Assets.FrozenCash,
MarketValue: status.Data.Assets.MarketValue,
Profit: status.Data.Assets.Profit,
TotalAsset: status.Data.Assets.TotalAsset,
CollectedAt: now,
}
2026-04-07 14:46:49 +08:00
2026-04-17 13:08:48 +08:00
if err == gorm.ErrRecordNotFound {
// 记录不存在,插入新记录
2026-04-17 14:50:34 +08:00
if err := impl.DBService.Create(&asset).Error; err != nil {
2026-04-17 13:08:48 +08:00
return fmt.Errorf("插入资产快照失败: %w", err)
}
} else if err != nil {
// 查询出错
return fmt.Errorf("查询资产快照失败: %w", err)
} else {
// 记录存在,更新现有记录
if asset.Cash != existingAsset.Cash || asset.FrozenCash != existingAsset.FrozenCash || asset.MarketValue != existingAsset.MarketValue || asset.Profit != existingAsset.Profit || asset.TotalAsset != existingAsset.TotalAsset {
2026-04-08 10:26:22 +08:00
asset.ID = existingAsset.ID
2026-04-17 14:50:34 +08:00
if err := impl.DBService.Save(&asset).Error; err != nil {
2026-04-08 10:26:22 +08:00
return fmt.Errorf("更新资产快照失败: %w", err)
}
2026-04-07 12:22:57 +08:00
}
2026-04-17 13:08:48 +08:00
}
2026-04-07 12:22:57 +08:00
2026-04-17 13:08:48 +08:00
// 批量保存订单 (先查询后更新/插入)
if len(status.Data.Orders) > 0 {
for _, order := range status.Data.Orders {
// 验证必要条件: OrderID和StockCode
if order.OrderID == 0 {
continue
}
if order.StockCode == "" {
continue
}
// 只存储成交的订单
if order.OrderStatus != 56 {
continue
}
var open_price float64
var info types.OrderInfo
err = json.Unmarshal([]byte(order.OrderRemark), &info)
if err == nil {
open_price = info.Op
}
2026-04-07 21:40:29 +08:00
2026-04-17 13:08:48 +08:00
// 查询是否存在
var cnt int64
2026-04-22 18:17:58 +08:00
impl.DBService.Model(&models.CollectorOrder{}).Where("account_id = ? AND order_id = ? AND ymd = ?", status.Data.Assets.AccountID, order.OrderID, ymd).Count(&cnt)
2026-04-17 13:08:48 +08:00
if cnt > 0 {
continue
}
2026-04-08 10:26:22 +08:00
2026-04-17 13:08:48 +08:00
orderRecord := models.CollectorOrder{
OrderID: order.OrderID,
AccountID: status.Data.Assets.AccountID,
StockCode: order.StockCode,
Ymd: ymd,
Price: order.Price,
Volume: order.Volume,
OpenPrice: open_price,
TradedPrice: order.TradedPrice,
TradedVolume: order.TradedVolume,
OrderStatus: order.OrderStatus,
OrderTime: order.OrderTime,
OrderRemark: order.OrderRemark,
OffsetFlag: order.OffsetFlag,
CollectedAt: now,
CreatedAt: now,
}
// 记录不存在,插入新记录
2026-04-17 14:50:34 +08:00
if err := impl.DBService.Create(&orderRecord).Error; err != nil {
2026-04-17 13:08:48 +08:00
return fmt.Errorf("插入订单失败: %w", err)
2026-04-07 12:22:57 +08:00
}
2026-04-17 14:50:34 +08:00
// 处理订单簿逻辑
2026-04-17 21:57:23 +08:00
// if err := processOrderBook(status.Data.Assets.AccountID, order, ymd, now, open_price); err != nil {
// fmt.Printf("处理订单簿失败: %v\n", err)
// // 不返回错误,避免影响主流程
// }
2026-04-07 12:22:57 +08:00
}
2026-04-17 13:08:48 +08:00
}
2026-04-07 12:22:57 +08:00
2026-04-17 13:08:48 +08:00
// 批量保存持仓 (先查询后更新/插入)
if len(status.Data.Positions) > 0 {
for _, pos := range status.Data.Positions {
// 验证必要条件: Code
if pos.Code == "" {
continue
}
2026-04-07 21:40:29 +08:00
2026-04-17 13:08:48 +08:00
// 查询是否存在
var existingPosition models.CollectorPosition
2026-04-17 14:50:34 +08:00
err := impl.DBService.Where("account_id = ? AND code = ? AND ymd = ?",
2026-04-17 13:08:48 +08:00
status.Data.Assets.AccountID, pos.Code, ymd).First(&existingPosition).Error
positionRecord := models.CollectorPosition{
AccountID: status.Data.Assets.AccountID,
Code: pos.Code,
Ymd: ymd,
Volume: pos.Volume,
CanUseVolume: pos.CanUseVolume,
FrozenVolume: pos.FrozenVolume,
AvgPrice: pos.AvgPrice,
OpenPrice: pos.OpenPrice,
CurrentPrice: pos.CurrentPrice,
MarketValue: pos.MarketValue,
Profit: pos.Profit,
ProfitRate: pos.ProfitRate,
MinProfitRate: pos.MinProfitRate,
CollectedAt: now,
}
2026-04-08 10:26:22 +08:00
2026-04-17 13:08:48 +08:00
if err == gorm.ErrRecordNotFound {
// 记录不存在,插入新记录
2026-04-17 14:50:34 +08:00
if err := impl.DBService.Create(&positionRecord).Error; err != nil {
2026-04-17 13:08:48 +08:00
return fmt.Errorf("插入持仓失败: %w", err)
}
} else if err != nil {
// 查询出错
return fmt.Errorf("查询持仓失败: %w", err)
} else {
if positionRecord.Volume != existingPosition.Volume ||
positionRecord.CanUseVolume != existingPosition.CanUseVolume ||
positionRecord.FrozenVolume != existingPosition.FrozenVolume ||
positionRecord.AvgPrice != existingPosition.AvgPrice ||
positionRecord.OpenPrice != existingPosition.OpenPrice ||
positionRecord.CurrentPrice != existingPosition.CurrentPrice ||
positionRecord.MarketValue != existingPosition.MarketValue ||
positionRecord.Profit != existingPosition.Profit ||
positionRecord.ProfitRate != existingPosition.ProfitRate {
2026-04-08 10:26:22 +08:00
// 记录存在,更新现有记录
positionRecord.ID = existingPosition.ID
2026-04-17 14:50:34 +08:00
if err := impl.DBService.Save(&positionRecord).Error; err != nil {
2026-04-08 10:26:22 +08:00
return fmt.Errorf("更新持仓失败: %w", err)
}
2026-04-07 21:40:29 +08:00
}
2026-04-08 10:26:22 +08:00
2026-04-07 12:22:57 +08:00
}
}
2026-04-17 13:08:48 +08:00
}
return nil
2026-04-07 12:22:57 +08:00
}
2026-04-17 14:50:34 +08:00
// processOrderBook 处理订单簿逻辑
func processOrderBook(accountID string, order types.Order, ymd int, now time.Time, openPrice float64) error {
// OffsetFlag: 通常 0=买入, 1=卖出 (需要根据实际系统确认)
// 这里假设: OffsetFlag == 0 表示买入, OffsetFlag == 1 表示卖出
2026-04-17 19:56:22 +08:00
if order.OffsetFlag == types.FLAG_BUY {
// 先检查有无未闭合的订单簿记录
var existingOrderBook models.OrderBook
err := impl.DBService.Where(
"account_id = ? AND stock_code = ? AND is_closed = ?",
accountID, order.StockCode, false,
).Order("buy_time DESC").First(&existingOrderBook).Error
2026-04-07 12:22:57 +08:00
2026-04-17 19:56:22 +08:00
if err == gorm.ErrRecordNotFound {
// 没有未闭合的订单簿记录,创建新的
orderBook := models.OrderBook{
AccountID: accountID,
StockCode: order.StockCode,
Ymd: ymd,
BuyOrderID: fmt.Sprintf("%d", order.OrderID),
BuyPrice: order.TradedPrice,
BuyVolume: order.TradedVolume,
BuyTime: order.OrderTime,
BuyCollectedAt: now,
IsClosed: false,
}
if err := impl.DBService.Create(&orderBook).Error; err != nil {
return fmt.Errorf("创建订单簿记录失败: %w", err)
}
log.Printf("新建订单簿: account=%s, stock=%s, orderID=%d, price=%.4f, volume=%d",
accountID, order.StockCode, order.OrderID, order.TradedPrice, order.TradedVolume)
} else if err != nil {
return fmt.Errorf("查询订单簿记录失败: %w", err)
} else {
// 存在未闭合的订单簿记录,更新数量和价格(加权平均)
totalVolume := existingOrderBook.BuyVolume + order.TradedVolume
totalAmount := existingOrderBook.BuyPrice*float64(existingOrderBook.BuyVolume) + order.TradedPrice*float64(order.TradedVolume)
newAvgPrice := totalAmount / float64(totalVolume)
existingOrderBook.BuyVolume = totalVolume
existingOrderBook.BuyPrice = newAvgPrice
// 追加订单ID到 BuyOrderID用逗号分隔
existingOrderBook.BuyOrderID = existingOrderBook.BuyOrderID + "," + fmt.Sprintf("%d", order.OrderID)
if err := impl.DBService.Save(&existingOrderBook).Error; err != nil {
return fmt.Errorf("更新订单簿记录失败: %w", err)
}
log.Printf("更新订单簿: account=%s, stock=%s, 新订单ID=%d, 总数量=%d, 平均价格=%.4f",
accountID, order.StockCode, order.OrderID, totalVolume, newAvgPrice)
2026-04-17 14:50:34 +08:00
}
2026-04-17 19:56:22 +08:00
} else if order.OffsetFlag == types.FLAG_SELL {
2026-04-17 14:50:34 +08:00
// 卖出订单 - 查找对应的买入记录并闭合
var orderBook models.OrderBook
// 查找同一账户、同一股票、未闭合的订单簿记录
err := impl.DBService.Where(
"account_id = ? AND stock_code = ? AND is_closed = ?",
accountID, order.StockCode, false,
).Order("buy_time ASC").First(&orderBook).Error
if err == gorm.ErrRecordNotFound {
// 没有找到对应的买入记录,可能是之前就已经持有的仓位
fmt.Printf("未找到对应的买入记录: account=%s, stock=%s\n", accountID, order.StockCode)
return nil
} else if err != nil {
return fmt.Errorf("查询订单簿记录失败: %w", err)
}
2026-04-17 19:56:22 +08:00
// 计算盈亏(支持部分卖出)
sellVolume := order.TradedVolume
sellPrice := order.TradedPrice
buyPrice := orderBook.BuyPrice
// 如果卖出数量小于买入数量,按比例计算盈亏
var profit float64
2026-04-17 14:50:34 +08:00
var profitRate float64
2026-04-17 19:56:22 +08:00
if sellVolume >= orderBook.BuyVolume {
// 全部卖出或超额卖出
buyAmount := float64(orderBook.BuyVolume) * buyPrice
sellAmount := float64(sellVolume) * sellPrice
profit = sellAmount - buyAmount
if buyAmount > 0 {
profitRate = (profit / buyAmount) * 100
}
} else {
// 部分卖出,按比例计算
sellRatio := float64(sellVolume) / float64(orderBook.BuyVolume)
buyAmount := float64(orderBook.BuyVolume) * buyPrice * sellRatio
sellAmount := float64(sellVolume) * sellPrice
profit = sellAmount - buyAmount
if buyAmount > 0 {
profitRate = (profit / buyAmount) * 100
}
2026-04-17 14:50:34 +08:00
}
// 更新订单簿记录
2026-04-17 19:56:22 +08:00
sellOrderID := fmt.Sprintf("%d", order.OrderID)
orderBook.SellOrderID = &sellOrderID
2026-04-17 14:50:34 +08:00
orderBook.SellPrice = &order.TradedPrice
orderBook.SellVolume = &order.TradedVolume
orderBook.SellTime = &order.OrderTime
orderBook.SellCollectedAt = &now
orderBook.IsClosed = true
orderBook.Profit = &profit
orderBook.ProfitRate = &profitRate
if err := impl.DBService.Save(&orderBook).Error; err != nil {
return fmt.Errorf("更新订单簿记录失败: %w", err)
}
2026-04-17 19:56:22 +08:00
log.Printf("闭合订单簿: account=%s, stock=%s, buyOrderID=%s, sellOrderID=%s, profit=%.2f",
accountID, order.StockCode, orderBook.BuyOrderID, sellOrderID, profit)
2026-04-07 12:22:57 +08:00
}
return nil
}