using AssetManager.Data; using AssetManager.Models.DTOs; using AssetManager.Infrastructure.Services; using SqlSugar; namespace AssetManager.Services; public class PortfolioService : IPortfolioService { private readonly ISqlSugarClient _db; private readonly IMarketDataService _marketDataService; private readonly IExchangeRateService _exchangeRateService; public PortfolioService(ISqlSugarClient db, IMarketDataService marketDataService, IExchangeRateService exchangeRateService) { _db = db; _marketDataService = marketDataService; _exchangeRateService = exchangeRateService; } 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), ReturnRate = 0, Status = "运行中", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }; _db.Insertable(portfolio).ExecuteCommand(); // 创建初始持仓 foreach (var stock in request.stocks) { // 解析实际买入时间,如果解析失败则用当前时间 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 GetPortfolios(string userId) { var portfolios = _db.Queryable() .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(), 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 GetTotalAssetsAsync(string userId) { // 获取用户信息(包含默认本位币) var user = _db.Queryable() .Where(u => u.Id == userId) .First(); if (user == null) { throw new Exception("User not found"); } string targetCurrency = user.DefaultCurrency; decimal totalValueInTargetCurrency = 0; decimal totalCostInTargetCurrency = 0; decimal totalTodayProfitInTargetCurrency = 0; // 获取用户所有投资组合 var portfolios = _db.Queryable() .Where(p => p.UserId == userId) .ToList(); foreach (var portfolio in portfolios) { // 获取该组合的所有持仓 var positions = _db.Queryable() .Where(pos => pos.PortfolioId == portfolio.Id) .ToList(); foreach (var pos in positions) { // 获取实时价格 MarketPriceResponse priceResponse; if (pos.AssetType.Equals("Crypto", StringComparison.OrdinalIgnoreCase)) { priceResponse = await _marketDataService.GetCryptoPriceAsync(pos.StockCode); } else { priceResponse = await _marketDataService.GetStockPriceAsync(pos.StockCode); } decimal currentPrice = priceResponse.Price; decimal previousClose = priceResponse.PreviousClose; 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); 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 = totalReturnRate }; } // 保留同步方法作为兼容 public TotalAssetsResponse GetTotalAssets(string userId) { return GetTotalAssetsAsync(userId).GetAwaiter().GetResult(); } public async Task GetPortfolioByIdAsync(string id, string userId) { var portfolio = _db.Queryable() .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() .Where(pos => pos.PortfolioId == id) .ToList(); // 获取每个持仓的实时价格并计算 decimal totalPortfolioValue = 0; decimal totalCost = 0; decimal totalTodayProfit = 0; var positionItems = new List(); foreach (var pos in positions) { // 获取实时价格 MarketPriceResponse priceResponse; if (pos.AssetType.Equals("Crypto", StringComparison.OrdinalIgnoreCase)) { priceResponse = await _marketDataService.GetCryptoPriceAsync(pos.StockCode); } else { priceResponse = await _marketDataService.GetStockPriceAsync(pos.StockCode); } decimal currentPrice = priceResponse.Price; decimal previousClose = priceResponse.PreviousClose; 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; totalPortfolioValue += positionValue; totalCost += cost; totalTodayProfit += todayProfit; 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)positionValue, profit = (double)profit, profitRate = profitRate, changeAmount = (double)todayProfit, ratio = 0, // 后面统一计算比例 deviationRatio = 0, // 后续实现 currency = pos.Currency }); } // 计算每个持仓的比例 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 = portfolio.Currency, 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 = portfolio.Currency, 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() .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() .Where(t => t.PortfolioId == portfolioId) .OrderByDescending(t => t.TransactionTime) .Skip(offset) .Take(limit) .ToList(); var total = _db.Queryable() .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, amount = (double)t.TotalAmount, currency = t.Currency, status = t.Status, remark = t.Remark }).ToList(), total = total, page = offset / limit + 1, pageSize = limit }; } public CreateTransactionResponse CreateTransaction(CreateTransactionRequest request, string userId) { // 验证投资组合是否属于该用户 var portfolio = _db.Queryable() .Where(p => p.Id == request.portfolioId && p.UserId == userId) .First(); if (portfolio == null) { throw new Exception("Portfolio not found or access denied"); } // 解析实际交易时间,如果解析失败则用当前时间 DateTime transactionTime = DateTime.Now; 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, TransactionTime = transactionTime, CreatedAt = DateTime.Now }; _db.Insertable(transaction).ExecuteCommand(); // 更新持仓 var position = _db.Queryable() .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() .Where(pos => pos.PortfolioId == request.portfolioId) .ToList(); decimal totalPortfolioValue = 0; foreach (var pos in positions) { // 获取实时价格 MarketPriceResponse priceResponse; if (pos.AssetType.Equals("Crypto", StringComparison.OrdinalIgnoreCase)) { priceResponse = _marketDataService.GetCryptoPriceAsync(pos.StockCode).GetAwaiter().GetResult(); } else { priceResponse = _marketDataService.GetStockPriceAsync(pos.StockCode).GetAwaiter().GetResult(); } decimal currentPrice = priceResponse.Price; totalPortfolioValue += pos.Shares * currentPrice; } portfolio.TotalValue = totalPortfolioValue; portfolio.UpdatedAt = DateTime.Now; _db.Updateable(portfolio).ExecuteCommand(); return new CreateTransactionResponse { id = transaction.Id, totalAmount = (double)transaction.TotalAmount, status = transaction.Status, createdAt = transaction.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss") }; } }