AssetManager.API/AssetManager.Services/PortfolioService.cs

512 lines
19 KiB
C#

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) ?? 0),
ReturnRate = 0,
Status = "运行中",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
_db.Insertable(portfolio).ExecuteCommand();
// 创建初始持仓
foreach (var stock in request.stocks ?? new List<StockItem>())
{
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 = user.DefaultCurrency ?? "CNY";
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;
}
// 获取实时价格(自动路由数据源)
var priceResponse = await _marketDataService.GetPriceAsync(pos.StockCode, pos.AssetType ?? "Stock");
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<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;
}
// 获取实时价格(自动路由数据源)
var priceResponse = await _marketDataService.GetPriceAsync(pos.StockCode, pos.AssetType ?? "Stock");
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;
// 转换为组合本位币
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,
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<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;
}
// 获取实时价格(自动路由数据源)
var priceResponse = _marketDataService.GetPriceAsync(pos.StockCode, pos.AssetType ?? "Stock").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")
};
}
}