startOfDay(); $endDate = Carbon::parse($date)->endOfDay(); // 获取当日所有成交记录 $transactions = MexTransaction::where('item_id', $itemId) ->where('currency_type', $currencyType) ->whereBetween('created_at', [$startDate, $endDate]) ->orderBy('created_at') ->get(); if ($transactions->isEmpty()) { // 没有成交记录时,使用前一日数据或价格配置数据生成趋势记录 return $this->generateTrendFromPreviousOrConfig($date, $itemId, $currencyType); } // 计算价格统计 $priceStats = $this->calculatePriceStatistics($transactions); // 计算买入卖出价格统计 $buyPriceStats = $this->calculateBuyPriceStatistics($transactions); $sellPriceStats = $this->calculateSellPriceStatistics($transactions); // 计算交易统计 $tradeStats = $this->calculateTradeStatistics($transactions); // 获取前一日收盘价用于计算价格变化 $previousClosePrice = $this->getPreviousClosePrice($date, $itemId, $currencyType); // 计算价格变化 $priceChange = $this->calculatePriceChange($priceStats['close_price'], $previousClosePrice); // 创建或更新趋势记录 $trend = MexDailyPriceTrend::updateOrCreate( [ 'item_id' => $itemId, 'currency_type' => $currencyType, 'trade_date' => $date, ], array_merge($priceStats, $buyPriceStats, $sellPriceStats, $tradeStats, $priceChange) ); return MexDailyPriceTrendDto::fromModel($trend); } /** * 批量生成多日价格趋势数据 */ public function generateMultipleDaysTrends( string $startDate, string $endDate, ?int $itemId = null, ?FUND_CURRENCY_TYPE $currencyType = null ): Collection { $results = collect(); $current = Carbon::parse($startDate); $end = Carbon::parse($endDate); while ($current <= $end) { $dateStr = $current->format('Y-m-d'); if ($itemId && $currencyType) { // 生成指定商品的趋势 $trend = $this->generateDailyTrend($dateStr, $itemId, $currencyType); if ($trend) { $results->push($trend); } } else { // 生成所有商品的趋势 $this->generateAllItemsTrendsForDate($dateStr, $results); } $current->addDay(); } return $results; } /** * 获取商品的价格趋势历史 */ public function getItemPriceTrends( int $itemId, FUND_CURRENCY_TYPE $currencyType, string $startDate, string $endDate, int $limit = 100 ): Collection { $trends = MexDailyPriceTrend::where('item_id', $itemId) ->where('currency_type', $currencyType) ->whereBetween('trade_date', [$startDate, $endDate]) ->orderBy('trade_date', 'desc') ->limit($limit) ->get(); return $trends->map(fn($trend) => MexDailyPriceTrendDto::fromModel($trend)); } /** * 获取价格趋势统计信息 */ public function getTrendStatistics( int $itemId, FUND_CURRENCY_TYPE $currencyType, string $startDate, string $endDate ): array { $trends = MexDailyPriceTrend::where('item_id', $itemId) ->where('currency_type', $currencyType) ->whereBetween('trade_date', [$startDate, $endDate]) ->get(); if ($trends->isEmpty()) { return []; } return [ 'period_start' => $startDate, 'period_end' => $endDate, 'trading_days' => $trends->count(), 'total_volume' => $trends->sum('total_volume'), 'total_amount' => $trends->sum('total_amount'), 'avg_daily_volume' => $trends->avg('total_volume'), 'avg_daily_amount' => $trends->avg('total_amount'), 'highest_price' => $trends->max('high_price'), 'lowest_price' => $trends->min('low_price'), 'period_start_price' => $trends->sortBy('trade_date')->first()->open_price, 'period_end_price' => $trends->sortByDesc('trade_date')->first()->close_price, 'avg_volatility' => $trends->avg('volatility'), 'max_volatility' => $trends->max('volatility'), ]; } /** * 计算价格统计 */ private function calculatePriceStatistics(Collection $transactions): array { $prices = $transactions->pluck('price'); // 计算加权平均价格 $totalAmount = $transactions->sum('total_amount'); $totalQuantity = $transactions->sum('quantity'); $avgPrice = $totalQuantity > 0 ? $totalAmount / $totalQuantity : 0; $openPrice = $transactions->first()->price; $closePrice = $transactions->last()->price; $highPrice = $prices->max(); $lowPrice = $prices->min(); // 计算波动率 $volatility = $openPrice > 0 ? (($highPrice - $lowPrice) / $openPrice * 100) : 0; return [ 'open_price' => $openPrice, 'close_price' => $closePrice, 'high_price' => $highPrice, 'low_price' => $lowPrice, 'avg_price' => $avgPrice, 'volatility' => $volatility, ]; } /** * 计算交易统计 */ private function calculateTradeStatistics(Collection $transactions): array { $totalVolume = $transactions->sum('quantity'); $totalAmount = $transactions->sum('total_amount'); $transactionCount = $transactions->count(); // 按交易类型分组统计 $userBuyTransactions = $transactions->where('transaction_type', 'USER_BUY'); $userSellTransactions = $transactions->where('transaction_type', 'USER_SELL'); $adminInjectTransactions = $transactions->where('transaction_type', 'ADMIN_INJECT'); $adminRecycleTransactions = $transactions->where('transaction_type', 'ADMIN_RECYCLE'); return [ 'total_volume' => $totalVolume, 'total_amount' => $totalAmount, 'transaction_count' => $transactionCount, 'buy_volume' => $userBuyTransactions->sum('quantity'), 'sell_volume' => $userSellTransactions->sum('quantity'), 'buy_amount' => $userBuyTransactions->sum('total_amount'), 'sell_amount' => $userSellTransactions->sum('total_amount'), 'admin_inject_volume' => $adminInjectTransactions->sum('quantity'), 'admin_recycle_volume' => $adminRecycleTransactions->sum('quantity'), 'admin_inject_amount' => $adminInjectTransactions->sum('total_amount'), 'admin_recycle_amount' => $adminRecycleTransactions->sum('total_amount'), ]; } /** * 获取前一日收盘价 */ private function getPreviousClosePrice(string $date, int $itemId, FUND_CURRENCY_TYPE $currencyType): ?float { $previousDate = Carbon::parse($date)->subDay()->format('Y-m-d'); $previousTrend = MexDailyPriceTrend::where('item_id', $itemId) ->where('currency_type', $currencyType) ->where('trade_date', $previousDate) ->first(); return $previousTrend?->close_price; } /** * 计算价格变化 */ private function calculatePriceChange(float $currentPrice, ?float $previousPrice): array { if ($previousPrice === null || $previousPrice == 0) { return [ 'price_change' => null, 'price_change_percent' => null, ]; } $priceChange = $currentPrice - $previousPrice; $priceChangePercent = ($priceChange / $previousPrice) * 100; return [ 'price_change' => $priceChange, 'price_change_percent' => $priceChangePercent, ]; } /** * 生成指定日期所有商品的趋势数据 */ private function generateAllItemsTrendsForDate(string $date, Collection &$results): void { // 获取当日有交易的所有商品和币种组合 $itemCurrencyPairs = MexTransaction::whereDate('created_at', $date) ->select('item_id', 'currency_type') ->distinct() ->get(); foreach ($itemCurrencyPairs as $pair) { $trend = $this->generateDailyTrend($date, $pair->item_id, $pair->currency_type); if ($trend) { $results->push($trend); } } } /** * 从前一日数据或价格配置生成趋势记录 */ private function generateTrendFromPreviousOrConfig(string $date, int $itemId, FUND_CURRENCY_TYPE $currencyType): ?MexDailyPriceTrendDto { // 获取前一日收盘价 $previousClosePrice = $this->getPreviousClosePrice($date, $itemId, $currencyType); if ($previousClosePrice !== null) { // 使用前一日收盘价作为当日所有价格 $priceData = [ 'open_price' => $previousClosePrice, 'close_price' => $previousClosePrice, 'high_price' => $previousClosePrice, 'low_price' => $previousClosePrice, 'avg_price' => $previousClosePrice, 'buy_open_price' => $previousClosePrice, 'buy_close_price' => $previousClosePrice, 'buy_high_price' => $previousClosePrice, 'buy_low_price' => $previousClosePrice, 'buy_avg_price' => $previousClosePrice, 'sell_open_price' => $previousClosePrice, 'sell_close_price' => $previousClosePrice, 'sell_high_price' => $previousClosePrice, 'sell_low_price' => $previousClosePrice, 'sell_avg_price' => $previousClosePrice, ]; } else { // 使用价格配置数据 $priceConfig = \App\Module\Mex\Logic\MexPriceConfigLogic::getItemPriceConfig($itemId); if (!$priceConfig) { return null; // 没有价格配置,无法生成趋势 } // 使用价格配置的中间价作为参考价格 $referencePrice = ($priceConfig['min_price'] + $priceConfig['max_price']) / 2; $priceData = [ 'open_price' => $referencePrice, 'close_price' => $referencePrice, 'high_price' => $referencePrice, 'low_price' => $referencePrice, 'avg_price' => $referencePrice, 'buy_open_price' => $referencePrice, 'buy_close_price' => $referencePrice, 'buy_high_price' => $referencePrice, 'buy_low_price' => $referencePrice, 'buy_avg_price' => $referencePrice, 'sell_open_price' => $referencePrice, 'sell_close_price' => $referencePrice, 'sell_high_price' => $referencePrice, 'sell_low_price' => $referencePrice, 'sell_avg_price' => $referencePrice, ]; } // 无成交时的统计数据 $statsData = [ 'total_volume' => 0, 'total_amount' => 0, 'transaction_count' => 0, 'buy_volume' => 0, 'sell_volume' => 0, 'buy_amount' => 0, 'sell_amount' => 0, 'admin_inject_volume' => 0, 'admin_recycle_volume' => 0, 'admin_inject_amount' => 0, 'admin_recycle_amount' => 0, 'volatility' => 0, ]; // 计算价格变化 $priceChange = $this->calculatePriceChange($priceData['close_price'], $previousClosePrice); // 创建或更新趋势记录 $trend = MexDailyPriceTrend::updateOrCreate( [ 'item_id' => $itemId, 'currency_type' => $currencyType, 'trade_date' => $date, ], array_merge($priceData, $statsData, $priceChange) ); return MexDailyPriceTrendDto::fromModel($trend); } /** * 计算买入价格统计 */ private function calculateBuyPriceStatistics(Collection $transactions): array { // 筛选买入交易 $buyTransactions = $transactions->where('transaction_type', 'USER_BUY'); if ($buyTransactions->isEmpty()) { return [ 'buy_open_price' => null, 'buy_close_price' => null, 'buy_high_price' => null, 'buy_low_price' => null, 'buy_avg_price' => null, ]; } $buyPrices = $buyTransactions->pluck('price'); // 计算买入加权平均价格 $buyTotalAmount = $buyTransactions->sum('total_amount'); $buyTotalQuantity = $buyTransactions->sum('quantity'); $buyAvgPrice = $buyTotalQuantity > 0 ? $buyTotalAmount / $buyTotalQuantity : 0; return [ 'buy_open_price' => $buyTransactions->first()->price, 'buy_close_price' => $buyTransactions->last()->price, 'buy_high_price' => $buyPrices->max(), 'buy_low_price' => $buyPrices->min(), 'buy_avg_price' => $buyAvgPrice, ]; } /** * 计算卖出价格统计 */ private function calculateSellPriceStatistics(Collection $transactions): array { // 筛选卖出交易 $sellTransactions = $transactions->where('transaction_type', 'USER_SELL'); if ($sellTransactions->isEmpty()) { return [ 'sell_open_price' => null, 'sell_close_price' => null, 'sell_high_price' => null, 'sell_low_price' => null, 'sell_avg_price' => null, ]; } $sellPrices = $sellTransactions->pluck('price'); // 计算卖出加权平均价格 $sellTotalAmount = $sellTransactions->sum('total_amount'); $sellTotalQuantity = $sellTransactions->sum('quantity'); $sellAvgPrice = $sellTotalQuantity > 0 ? $sellTotalAmount / $sellTotalQuantity : 0; return [ 'sell_open_price' => $sellTransactions->first()->price, 'sell_close_price' => $sellTransactions->last()->price, 'sell_high_price' => $sellPrices->max(), 'sell_low_price' => $sellPrices->min(), 'sell_avg_price' => $sellAvgPrice, ]; } }