- CreateTransaction完成后删除该交易日期之后的净值记录 - 下次请求收益曲线时自动重新计算 - 修改CreateTransaction为async方法 - 注入IPortfolioNavService到PortfolioService 流程: 1. 用户买入/卖出 → 创建交易记录 2. 删除交易日期之后的净值历史 3. 下次请求收益曲线 → 自动回填缺失数据
632 lines
25 KiB
C#
Executable File
632 lines
25 KiB
C#
Executable File
using AssetManager.Data;
|
||
using AssetManager.Models.DTOs;
|
||
using AssetManager.Infrastructure.Services;
|
||
using SqlSugar;
|
||
using Microsoft.Extensions.Logging;
|
||
|
||
namespace AssetManager.Services;
|
||
|
||
public class PortfolioService : IPortfolioService
|
||
{
|
||
private readonly ISqlSugarClient _db;
|
||
private readonly IMarketDataService _marketDataService;
|
||
private readonly IExchangeRateService _exchangeRateService;
|
||
private readonly IPortfolioNavService _navService;
|
||
private readonly ILogger<PortfolioService> _logger;
|
||
|
||
public PortfolioService(
|
||
ISqlSugarClient db,
|
||
IMarketDataService marketDataService,
|
||
IExchangeRateService exchangeRateService,
|
||
IPortfolioNavService navService,
|
||
ILogger<PortfolioService> logger)
|
||
{
|
||
_db = db;
|
||
_marketDataService = marketDataService;
|
||
_exchangeRateService = exchangeRateService;
|
||
_navService = navService;
|
||
_logger = logger;
|
||
}
|
||
|
||
public CreatePortfolioResponse CreatePortfolio(CreatePortfolioRequest request, string userId)
|
||
{
|
||
var portfolio = new Portfolio
|
||
{
|
||
Id = "port-" + Guid.NewGuid().ToString().Substring(0, 8),
|
||
UserId = userId,
|
||
StrategyId = request.strategyId,
|
||
Name = request.name,
|
||
Currency = request.currency,
|
||
TotalValue = (decimal)(request.stocks?.Sum(s => s.price * s.amount) ?? 0),
|
||
ReturnRate = 0,
|
||
Status = "运行中",
|
||
CreatedAt = DateTime.Now,
|
||
UpdatedAt = DateTime.Now
|
||
};
|
||
|
||
_db.Insertable(portfolio).ExecuteCommand();
|
||
|
||
// 如果选择了策略,自动加载策略配置的标的作为初始持仓
|
||
var strategyStocks = new List<StockItem>();
|
||
if (!string.IsNullOrEmpty(request.strategyId))
|
||
{
|
||
var strategy = _db.Queryable<Strategy>()
|
||
.Where(s => s.Id == request.strategyId && s.UserId == userId)
|
||
.First();
|
||
|
||
if (strategy != null && !string.IsNullOrEmpty(strategy.Config))
|
||
{
|
||
try
|
||
{
|
||
// 风险平价策略
|
||
if (strategy.Type?.Equals("risk_parity", StringComparison.OrdinalIgnoreCase) == true)
|
||
{
|
||
// 处理可能的双层转义
|
||
string configJson = strategy.Config;
|
||
if (configJson.StartsWith("\"") && configJson.EndsWith("\""))
|
||
{
|
||
// 去掉外层的引号和转义
|
||
configJson = System.Text.Json.JsonSerializer.Deserialize<string>(configJson);
|
||
}
|
||
var config = System.Text.Json.JsonSerializer.Deserialize<RiskParityConfig>(configJson);
|
||
if (config?.Assets != null)
|
||
{
|
||
foreach (var asset in config.Assets)
|
||
{
|
||
if (!string.IsNullOrEmpty(asset.Symbol))
|
||
{
|
||
strategyStocks.Add(new StockItem
|
||
{
|
||
code = asset.Symbol,
|
||
name = asset.Symbol,
|
||
price = 0, // 价格留空,用户后续填写
|
||
amount = 0, // 数量留空,用户后续填写
|
||
currency = request.currency,
|
||
assetType = "Stock"
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 其他策略类型可以在这里扩展
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "解析策略配置失败,策略ID: {StrategyId}", request.strategyId);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 合并用户传入的持仓和策略自动生成的持仓
|
||
var allStocks = (request.stocks ?? new List<StockItem>()).Concat(strategyStocks).DistinctBy(s => s.code).ToList();
|
||
|
||
// 创建初始持仓
|
||
foreach (var stock in allStocks)
|
||
{
|
||
if (stock.code == null || stock.name == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 解析实际买入时间,如果解析失败则用当前时间
|
||
DateTime buyTime = DateTime.Now;
|
||
if (!string.IsNullOrEmpty(stock.date))
|
||
{
|
||
if (DateTime.TryParse(stock.date, out var parsedDate))
|
||
{
|
||
buyTime = parsedDate;
|
||
}
|
||
}
|
||
|
||
var position = new Position
|
||
{
|
||
Id = "pos-" + Guid.NewGuid().ToString().Substring(0, 8),
|
||
PortfolioId = portfolio.Id,
|
||
StockCode = stock.code,
|
||
StockName = stock.name,
|
||
AssetType = string.IsNullOrEmpty(stock.assetType) ? "Stock" : stock.assetType,
|
||
Shares = (decimal)stock.amount,
|
||
AvgPrice = (decimal)stock.price,
|
||
Currency = request.currency,
|
||
CreatedAt = buyTime,
|
||
UpdatedAt = DateTime.Now
|
||
};
|
||
|
||
_db.Insertable(position).ExecuteCommand();
|
||
|
||
// 创建交易记录
|
||
var transaction = new Transaction
|
||
{
|
||
Id = "trans-" + Guid.NewGuid().ToString().Substring(0, 8),
|
||
PortfolioId = portfolio.Id,
|
||
Type = "buy",
|
||
StockCode = stock.code,
|
||
AssetType = string.IsNullOrEmpty(stock.assetType) ? "Stock" : stock.assetType,
|
||
Title = "初始建仓",
|
||
Amount = (decimal)stock.amount,
|
||
Price = (decimal)stock.price,
|
||
TotalAmount = (decimal)(stock.price * stock.amount),
|
||
Currency = request.currency,
|
||
Status = "completed",
|
||
Remark = "初始建仓",
|
||
TransactionTime = buyTime,
|
||
CreatedAt = DateTime.Now
|
||
};
|
||
|
||
_db.Insertable(transaction).ExecuteCommand();
|
||
}
|
||
|
||
return new CreatePortfolioResponse
|
||
{
|
||
id = portfolio.Id,
|
||
totalValue = (double)portfolio.TotalValue,
|
||
returnRate = 0,
|
||
currency = portfolio.Currency,
|
||
createdAt = portfolio.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")
|
||
};
|
||
}
|
||
|
||
public List<PortfolioListItem> GetPortfolios(string userId)
|
||
{
|
||
var portfolios = _db.Queryable<Portfolio>()
|
||
.Where(p => p.UserId == userId)
|
||
.ToList();
|
||
|
||
return portfolios.Select(p => new PortfolioListItem
|
||
{
|
||
id = p.Id,
|
||
name = p.Name,
|
||
tags = $"{p.Status} · {p.Currency}",
|
||
status = p.Status,
|
||
statusType = p.Status == "运行中" ? "green" : "gray",
|
||
iconChar = p.Name?.Substring(0, 1).ToUpper() ?? "P",
|
||
iconBgClass = "bg-blue-100",
|
||
iconTextClass = "text-blue-700",
|
||
value = (double)p.TotalValue,
|
||
currency = p.Currency,
|
||
returnRate = (double)p.ReturnRate,
|
||
returnType = p.ReturnRate >= 0 ? "positive" : "negative"
|
||
}).ToList();
|
||
}
|
||
|
||
public async Task<TotalAssetsResponse> GetTotalAssetsAsync(string userId)
|
||
{
|
||
// 获取用户信息(包含默认本位币)
|
||
var user = _db.Queryable<User>()
|
||
.Where(u => u.Id == userId)
|
||
.First();
|
||
|
||
if (user == null)
|
||
{
|
||
throw new Exception("User not found");
|
||
}
|
||
|
||
string targetCurrency = !string.IsNullOrEmpty(user.DefaultCurrency) ? user.DefaultCurrency : "CNY";
|
||
_logger.LogInformation("用户 {UserId} 默认本位币: {Currency}", userId, targetCurrency);
|
||
decimal totalValueInTargetCurrency = 0;
|
||
decimal totalCostInTargetCurrency = 0;
|
||
decimal totalTodayProfitInTargetCurrency = 0;
|
||
|
||
// 获取用户所有投资组合
|
||
var portfolios = _db.Queryable<Portfolio>()
|
||
.Where(p => p.UserId == userId)
|
||
.ToList();
|
||
|
||
foreach (var portfolio in portfolios)
|
||
{
|
||
// 获取该组合的所有持仓
|
||
var positions = _db.Queryable<Position>()
|
||
.Where(pos => pos.PortfolioId == portfolio.Id)
|
||
.ToList();
|
||
|
||
foreach (var pos in positions)
|
||
{
|
||
if (pos.StockCode == null || pos.Currency == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 获取实时价格(自动路由数据源),失败则降级使用成本价
|
||
decimal currentPrice = pos.AvgPrice;
|
||
decimal previousClose = pos.AvgPrice;
|
||
try
|
||
{
|
||
var priceResponse = await _marketDataService.GetPriceAsync(pos.StockCode, pos.AssetType ?? "Stock");
|
||
if (priceResponse.Price > 0)
|
||
{
|
||
currentPrice = priceResponse.Price;
|
||
previousClose = priceResponse.PreviousClose > 0 ? priceResponse.PreviousClose : currentPrice;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "获取标的 {StockCode} 实时价格失败,使用成本价作为当前价", pos.StockCode);
|
||
}
|
||
|
||
decimal currentPositionValue = pos.Shares * currentPrice;
|
||
decimal costPositionValue = pos.Shares * pos.AvgPrice;
|
||
decimal todayProfit = previousClose > 0 ? pos.Shares * (currentPrice - previousClose) : 0;
|
||
|
||
// 换算到目标本位币
|
||
decimal currentInTarget = await _exchangeRateService.ConvertAmountAsync(currentPositionValue, pos.Currency, targetCurrency);
|
||
decimal costInTarget = await _exchangeRateService.ConvertAmountAsync(costPositionValue, pos.Currency, targetCurrency);
|
||
decimal todayProfitInTarget = await _exchangeRateService.ConvertAmountAsync(todayProfit, pos.Currency, targetCurrency);
|
||
|
||
_logger.LogInformation("标的 {StockCode} 换算: {Amount} {From} → {Converted} {To},汇率: {Rate}",
|
||
pos.StockCode, currentPositionValue, pos.Currency, currentInTarget, targetCurrency,
|
||
currentPositionValue > 0 ? currentInTarget / currentPositionValue : 0);
|
||
|
||
totalValueInTargetCurrency += currentInTarget;
|
||
totalCostInTargetCurrency += costInTarget;
|
||
totalTodayProfitInTargetCurrency += todayProfitInTarget;
|
||
}
|
||
}
|
||
|
||
double totalReturnRate = totalCostInTargetCurrency > 0 ? (double)((totalValueInTargetCurrency - totalCostInTargetCurrency) / totalCostInTargetCurrency * 100) : 0;
|
||
|
||
return new TotalAssetsResponse
|
||
{
|
||
totalValue = (double)totalValueInTargetCurrency,
|
||
currency = targetCurrency,
|
||
todayProfit = (double)totalTodayProfitInTargetCurrency,
|
||
todayProfitCurrency = targetCurrency,
|
||
totalReturnRate = Math.Round(totalReturnRate, 2)
|
||
};
|
||
}
|
||
|
||
// 保留同步方法作为兼容
|
||
public TotalAssetsResponse GetTotalAssets(string userId)
|
||
{
|
||
return GetTotalAssetsAsync(userId).GetAwaiter().GetResult();
|
||
}
|
||
|
||
public async Task<PortfolioDetailResponse> GetPortfolioByIdAsync(string id, string userId)
|
||
{
|
||
var portfolio = _db.Queryable<Portfolio>()
|
||
.Where(p => p.Id == id && p.UserId == userId)
|
||
.First();
|
||
|
||
if (portfolio == null)
|
||
{
|
||
throw new Exception("Portfolio not found or access denied");
|
||
}
|
||
|
||
var positions = _db.Queryable<Position>()
|
||
.Where(pos => pos.PortfolioId == id)
|
||
.ToList();
|
||
|
||
// 获取每个持仓的实时价格并转换为组合本位币
|
||
string targetCurrency = portfolio.Currency ?? "CNY";
|
||
decimal totalPortfolioValue = 0;
|
||
decimal totalCost = 0;
|
||
decimal totalTodayProfit = 0;
|
||
var positionItems = new List<PositionItem>();
|
||
|
||
foreach (var pos in positions)
|
||
{
|
||
if (pos.StockCode == null || pos.Currency == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 获取实时价格(自动路由数据源),失败则降级使用成本价
|
||
decimal currentPrice = pos.AvgPrice;
|
||
decimal previousClose = pos.AvgPrice;
|
||
try
|
||
{
|
||
var priceResponse = await _marketDataService.GetPriceAsync(pos.StockCode, pos.AssetType ?? "Stock");
|
||
if (priceResponse.Price > 0)
|
||
{
|
||
currentPrice = priceResponse.Price;
|
||
previousClose = priceResponse.PreviousClose > 0 ? priceResponse.PreviousClose : currentPrice;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "获取标的 {StockCode} 实时价格失败,使用成本价作为当前价", pos.StockCode);
|
||
}
|
||
|
||
decimal positionValue = pos.Shares * currentPrice;
|
||
decimal cost = pos.Shares * pos.AvgPrice;
|
||
decimal profit = positionValue - cost;
|
||
double profitRate = cost > 0 ? (double)(profit / cost * 100) : 0;
|
||
decimal todayProfit = previousClose > 0 ? pos.Shares * (currentPrice - previousClose) : 0;
|
||
|
||
// 转换为组合本位币
|
||
decimal positionValueInTarget = await _exchangeRateService.ConvertAmountAsync(positionValue, pos.Currency, targetCurrency);
|
||
decimal costInTarget = await _exchangeRateService.ConvertAmountAsync(cost, pos.Currency, targetCurrency);
|
||
decimal todayProfitInTarget = await _exchangeRateService.ConvertAmountAsync(todayProfit, pos.Currency, targetCurrency);
|
||
|
||
totalPortfolioValue += positionValueInTarget;
|
||
totalCost += costInTarget;
|
||
totalTodayProfit += todayProfitInTarget;
|
||
|
||
positionItems.Add(new PositionItem
|
||
{
|
||
id = pos.Id,
|
||
stockCode = pos.StockCode,
|
||
stockName = pos.StockName,
|
||
symbol = pos.StockCode,
|
||
amount = (int)pos.Shares,
|
||
averagePrice = (double)pos.AvgPrice,
|
||
currentPrice = (double)currentPrice,
|
||
totalValue = (double)positionValueInTarget,
|
||
profit = (double)(positionValueInTarget - costInTarget),
|
||
profitRate = profitRate,
|
||
changeAmount = (double)todayProfitInTarget,
|
||
ratio = 0, // 后面统一计算比例
|
||
deviationRatio = 0, // 后续实现
|
||
currency = targetCurrency
|
||
});
|
||
}
|
||
|
||
// 计算每个持仓的比例
|
||
foreach (var item in positionItems)
|
||
{
|
||
item.ratio = totalPortfolioValue > 0 ? (item.totalValue / (double)totalPortfolioValue) * 100 : 0;
|
||
}
|
||
|
||
decimal totalReturn = totalPortfolioValue - totalCost;
|
||
double totalReturnRate = totalCost > 0 ? (double)(totalReturn / totalCost * 100) : 0;
|
||
|
||
return new PortfolioDetailResponse
|
||
{
|
||
id = portfolio.Id,
|
||
name = portfolio.Name,
|
||
currency = targetCurrency,
|
||
status = portfolio.Status,
|
||
strategy = new StrategyInfo
|
||
{
|
||
id = portfolio.StrategyId,
|
||
name = "策略名称",
|
||
description = "策略描述"
|
||
},
|
||
portfolioValue = (double)totalPortfolioValue,
|
||
totalReturn = (double)totalReturn,
|
||
todayProfit = (double)totalTodayProfit,
|
||
historicalChange = totalReturnRate,
|
||
dailyVolatility = 0, // 后续实现
|
||
todayProfitCurrency = targetCurrency,
|
||
logicModel = "HFEA 风险平价逻辑",
|
||
logicModelStatus = "监控中",
|
||
logicModelDescription = "目标权重 季度调仓",
|
||
totalItems = positions.Count,
|
||
totalRatio = 100.0,
|
||
positions = positionItems
|
||
};
|
||
}
|
||
|
||
// 保留同步方法作为兼容(内部调用异步)
|
||
public PortfolioDetailResponse GetPortfolioById(string id, string userId)
|
||
{
|
||
return GetPortfolioByIdAsync(id, userId).GetAwaiter().GetResult();
|
||
}
|
||
|
||
public GetTransactionsResponse GetTransactions(string portfolioId, string userId, int limit, int offset)
|
||
{
|
||
// 验证投资组合是否属于该用户
|
||
var portfolio = _db.Queryable<Portfolio>()
|
||
.Where(p => p.Id == portfolioId && p.UserId == userId)
|
||
.First();
|
||
|
||
if (portfolio == null)
|
||
{
|
||
throw new Exception("Portfolio not found or access denied");
|
||
}
|
||
|
||
var transactions = _db.Queryable<Transaction>()
|
||
.Where(t => t.PortfolioId == portfolioId)
|
||
.OrderByDescending(t => t.TransactionTime)
|
||
.Skip(offset)
|
||
.Take(limit)
|
||
.ToList();
|
||
|
||
var total = _db.Queryable<Transaction>()
|
||
.Where(t => t.PortfolioId == portfolioId)
|
||
.Count();
|
||
|
||
return new GetTransactionsResponse
|
||
{
|
||
items = transactions.Select(t => new TransactionItem
|
||
{
|
||
id = t.Id,
|
||
portfolioId = t.PortfolioId,
|
||
date = t.TransactionTime.ToString("yyyy-MM-dd"),
|
||
time = t.TransactionTime.ToString("HH:mm:ss"),
|
||
type = t.Type,
|
||
title = t.Title,
|
||
stockCode = t.StockCode,
|
||
amount = (double)t.TotalAmount,
|
||
currency = t.Currency,
|
||
status = t.Status,
|
||
remark = t.Remark
|
||
}).ToList(),
|
||
total = total,
|
||
page = offset / limit + 1,
|
||
pageSize = limit
|
||
};
|
||
}
|
||
|
||
public async Task<CreateTransactionResponse> CreateTransaction(CreateTransactionRequest request, string userId)
|
||
{
|
||
// 验证投资组合是否属于该用户
|
||
var portfolio = _db.Queryable<Portfolio>()
|
||
.Where(p => p.Id == request.portfolioId && p.UserId == userId)
|
||
.First();
|
||
|
||
if (portfolio == null)
|
||
{
|
||
throw new Exception("Portfolio not found or access denied");
|
||
}
|
||
|
||
// 校验交易币种必须和组合本位币一致(双重校验)
|
||
if (!string.IsNullOrEmpty(request.currency) && !request.currency.Equals(portfolio.Currency, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
throw new Exception($"该组合本位币为 {portfolio.Currency},只能添加相同币种的标的");
|
||
}
|
||
|
||
// 卖出操作校验
|
||
if (request.type?.ToLower() == "sell")
|
||
{
|
||
// 校验是否有该持仓
|
||
var existingPosition = _db.Queryable<Position>()
|
||
.Where(pos => pos.PortfolioId == request.portfolioId && pos.StockCode == request.stockCode)
|
||
.First();
|
||
|
||
if (existingPosition == null)
|
||
{
|
||
throw new Exception($"该组合中不存在标的 {request.stockCode},无法卖出");
|
||
}
|
||
|
||
// 校验卖出数量不超过持仓数量
|
||
if (request.amount > (double)existingPosition.Shares)
|
||
{
|
||
throw new Exception($"卖出数量不能超过持仓数量,当前持仓 {existingPosition.Shares} 份");
|
||
}
|
||
}
|
||
|
||
// 解析实际交易时间,如果解析失败则用当前时间
|
||
DateTime transactionTime = DateTime.Now;
|
||
if (!string.IsNullOrEmpty(request.transactionDate))
|
||
{
|
||
if (DateTime.TryParse(request.transactionDate, out var parsedDate))
|
||
{
|
||
// 如果只传了日期,时间部分默认用当前时间
|
||
transactionTime = parsedDate.Date + DateTime.Now.TimeOfDay;
|
||
}
|
||
}
|
||
else if (!string.IsNullOrEmpty(request.transactionTime))
|
||
{
|
||
if (DateTime.TryParse(request.transactionTime, out var parsedTime))
|
||
{
|
||
transactionTime = parsedTime;
|
||
}
|
||
}
|
||
|
||
var transaction = new Transaction
|
||
{
|
||
Id = "trans-" + Guid.NewGuid().ToString().Substring(0, 8),
|
||
PortfolioId = request.portfolioId,
|
||
Type = request.type,
|
||
StockCode = request.stockCode,
|
||
AssetType = string.IsNullOrEmpty(request.assetType) ? "Stock" : request.assetType,
|
||
Title = request.remark ?? "交易",
|
||
Amount = (decimal)request.amount,
|
||
Price = (decimal)request.price,
|
||
TotalAmount = (decimal)(request.price * request.amount),
|
||
Currency = request.currency,
|
||
Status = "completed",
|
||
Remark = request.remark ?? string.Empty,
|
||
TransactionTime = transactionTime,
|
||
CreatedAt = DateTime.Now
|
||
};
|
||
|
||
_db.Insertable(transaction).ExecuteCommand();
|
||
|
||
// 更新持仓
|
||
var position = _db.Queryable<Position>()
|
||
.Where(pos => pos.PortfolioId == request.portfolioId && pos.StockCode == request.stockCode)
|
||
.First();
|
||
|
||
if (position != null)
|
||
{
|
||
if (request.type == "buy")
|
||
{
|
||
// 计算新的平均价格
|
||
var newTotalShares = position.Shares + (decimal)request.amount;
|
||
var newTotalCost = (position.Shares * position.AvgPrice) + ((decimal)request.amount * (decimal)request.price);
|
||
position.AvgPrice = newTotalCost / newTotalShares;
|
||
position.Shares = newTotalShares;
|
||
}
|
||
else if (request.type == "sell")
|
||
{
|
||
position.Shares -= (decimal)request.amount;
|
||
if (position.Shares <= 0)
|
||
{
|
||
_db.Deleteable(position).ExecuteCommand();
|
||
}
|
||
else
|
||
{
|
||
_db.Updateable(position).ExecuteCommand();
|
||
}
|
||
}
|
||
}
|
||
else if (request.type == "buy")
|
||
{
|
||
// 创建新持仓
|
||
position = new Position
|
||
{
|
||
Id = "pos-" + Guid.NewGuid().ToString().Substring(0, 8),
|
||
PortfolioId = request.portfolioId,
|
||
StockCode = request.stockCode,
|
||
StockName = request.remark ?? request.stockCode,
|
||
AssetType = string.IsNullOrEmpty(request.assetType) ? "Stock" : request.assetType,
|
||
Shares = (decimal)request.amount,
|
||
AvgPrice = (decimal)request.price,
|
||
Currency = request.currency,
|
||
CreatedAt = DateTime.Now,
|
||
UpdatedAt = DateTime.Now
|
||
};
|
||
_db.Insertable(position).ExecuteCommand();
|
||
}
|
||
|
||
// 更新投资组合总价值(使用实时市值而不是成本价)
|
||
var positions = _db.Queryable<Position>()
|
||
.Where(pos => pos.PortfolioId == request.portfolioId)
|
||
.ToList();
|
||
|
||
decimal totalPortfolioValue = 0;
|
||
foreach (var pos in positions)
|
||
{
|
||
if (pos.StockCode == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 获取实时价格(自动路由数据源),失败则降级使用成本价
|
||
decimal currentPrice = pos.AvgPrice;
|
||
try
|
||
{
|
||
var priceResponse = _marketDataService.GetPriceAsync(pos.StockCode, pos.AssetType ?? "Stock").GetAwaiter().GetResult();
|
||
if (priceResponse.Price > 0)
|
||
{
|
||
currentPrice = priceResponse.Price;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "获取标的 {StockCode} 实时价格失败,使用成本价计算组合总价值", pos.StockCode);
|
||
}
|
||
|
||
totalPortfolioValue += pos.Shares * currentPrice;
|
||
}
|
||
|
||
portfolio.TotalValue = totalPortfolioValue;
|
||
portfolio.UpdatedAt = DateTime.Now;
|
||
_db.Updateable(portfolio).ExecuteCommand();
|
||
|
||
// 删除该交易日期之后的净值历史记录,下次请求收益曲线时会自动重新计算
|
||
try
|
||
{
|
||
var deletedCount = await _navService.DeleteNavHistoryAfterDateAsync(request.portfolioId, transactionTime.Date);
|
||
if (deletedCount > 0)
|
||
{
|
||
_logger.LogInformation("交易创建后删除净值历史: PortfolioId={PortfolioId}, Date={Date}, Count={Count}",
|
||
request.portfolioId, transactionTime.Date, deletedCount);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "删除净值历史失败,将在下次请求时重新计算");
|
||
}
|
||
|
||
return new CreateTransactionResponse
|
||
{
|
||
id = transaction.Id,
|
||
totalAmount = (double)transaction.TotalAmount,
|
||
status = transaction.Status,
|
||
createdAt = transaction.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")
|
||
};
|
||
}
|
||
}
|