719 lines
28 KiB
C#
Executable File
719 lines
28 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)TodayProfit,
|
||
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")
|
||
};
|
||
}
|
||
|
||
// ===== 异步方法实现 =====
|
||
|
||
public Task<CreatePortfolioResponse> CreatePortfolioAsync(CreatePortfolioRequest request, string userId)
|
||
{
|
||
return Task.FromResult(CreatePortfolio(request, userId));
|
||
}
|
||
|
||
public Task<List<PortfolioListItem>> GetPortfolioListAsync(string userId)
|
||
{
|
||
return Task.FromResult(GetPortfolios(userId));
|
||
}
|
||
|
||
public Task<PortfolioDetailResponse> GetPortfolioDetailAsync(string portfolioId, string userId)
|
||
{
|
||
return GetPortfolioByIdAsync(portfolioId, userId);
|
||
}
|
||
|
||
public Task<List<TransactionItem>> GetTransactionsAsync(string portfolioId, GetTransactionsRequest request, string userId)
|
||
{
|
||
var response = GetTransactions(portfolioId, userId, request.Limit, request.Offset);
|
||
return Task.FromResult(response.Items ?? new List<TransactionItem>());
|
||
}
|
||
|
||
public async Task<TransactionItem> CreateTransactionAsync(string portfolioId, CreateTransactionRequest request, string userId)
|
||
{
|
||
request.PortfolioId = portfolioId;
|
||
var response = await CreateTransaction(request, userId);
|
||
|
||
return new TransactionItem
|
||
{
|
||
Id = response.Id,
|
||
PortfolioId = portfolioId,
|
||
Date = DateTime.Now.ToString("yyyy-MM-dd"),
|
||
Time = DateTime.Now.ToString("HH:mm:ss"),
|
||
Type = request.Type,
|
||
StockCode = request.StockCode,
|
||
Amount = response.TotalAmount,
|
||
Currency = request.Currency,
|
||
Status = response.Status,
|
||
Remark = request.Remark
|
||
};
|
||
}
|
||
|
||
public async Task<bool> DeletePortfolioAsync(string portfolioId, string userId)
|
||
{
|
||
var portfolio = _db.Queryable<Portfolio>()
|
||
.Where(p => p.Id == portfolioId && p.UserId == userId)
|
||
.First();
|
||
|
||
if (portfolio == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 删除相关持仓
|
||
var Positions = _db.Queryable<Position>()
|
||
.Where(pos => pos.PortfolioId == portfolioId)
|
||
.ToList();
|
||
if (Positions.Any())
|
||
{
|
||
_db.Deleteable(Positions).ExecuteCommand();
|
||
}
|
||
|
||
// 删除相关交易
|
||
var transactions = _db.Queryable<Transaction>()
|
||
.Where(t => t.PortfolioId == portfolioId)
|
||
.ToList();
|
||
if (transactions.Any())
|
||
{
|
||
_db.Deleteable(transactions).ExecuteCommand();
|
||
}
|
||
|
||
// 删除净值历史
|
||
var navHistory = _db.Queryable<PortfolioNavHistory>()
|
||
.Where(n => n.PortfolioId == portfolioId)
|
||
.ToList();
|
||
if (navHistory.Any())
|
||
{
|
||
_db.Deleteable(navHistory).ExecuteCommand();
|
||
}
|
||
|
||
// 删除组合
|
||
_db.Deleteable(portfolio).ExecuteCommand();
|
||
|
||
return true;
|
||
}
|
||
}
|