MexDailyPriceTrendLogic.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. <?php
  2. namespace App\Module\Mex\Logic;
  3. use App\Module\Fund\Enums\FUND_CURRENCY_TYPE;
  4. use App\Module\Mex\Dto\MexDailyPriceTrendDto;
  5. use App\Module\Mex\Models\MexDailyPriceTrend;
  6. use App\Module\Mex\Models\MexTransaction;
  7. use Carbon\Carbon;
  8. use Illuminate\Support\Collection;
  9. /**
  10. * 农贸市场每日价格趋势逻辑层
  11. */
  12. class MexDailyPriceTrendLogic
  13. {
  14. /**
  15. * 生成指定日期的价格趋势数据
  16. */
  17. public function generateDailyTrend(string $date, int $itemId, FUND_CURRENCY_TYPE $currencyType): ?MexDailyPriceTrendDto
  18. {
  19. $startDate = Carbon::parse($date)->startOfDay();
  20. $endDate = Carbon::parse($date)->endOfDay();
  21. // 获取当日所有成交记录
  22. $transactions = MexTransaction::where('item_id', $itemId)
  23. ->where('currency_type', $currencyType)
  24. ->whereBetween('created_at', [$startDate, $endDate])
  25. ->orderBy('created_at')
  26. ->get();
  27. if ($transactions->isEmpty()) {
  28. // 没有成交记录时,使用前一日数据或价格配置数据生成趋势记录
  29. return $this->generateTrendFromPreviousOrConfig($date, $itemId, $currencyType);
  30. }
  31. // 计算价格统计
  32. $priceStats = $this->calculatePriceStatistics($transactions);
  33. // 计算买入卖出价格统计
  34. $buyPriceStats = $this->calculateBuyPriceStatistics($transactions);
  35. $sellPriceStats = $this->calculateSellPriceStatistics($transactions);
  36. // 计算交易统计
  37. $tradeStats = $this->calculateTradeStatistics($transactions);
  38. // 获取前一日收盘价用于计算价格变化
  39. $previousClosePrice = $this->getPreviousClosePrice($date, $itemId, $currencyType);
  40. // 计算价格变化
  41. $priceChange = $this->calculatePriceChange($priceStats['close_price'], $previousClosePrice);
  42. // 创建或更新趋势记录
  43. $trend = MexDailyPriceTrend::updateOrCreate(
  44. [
  45. 'item_id' => $itemId,
  46. 'currency_type' => $currencyType,
  47. 'trade_date' => $date,
  48. ],
  49. array_merge($priceStats, $buyPriceStats, $sellPriceStats, $tradeStats, $priceChange)
  50. );
  51. return MexDailyPriceTrendDto::fromModel($trend);
  52. }
  53. /**
  54. * 批量生成多日价格趋势数据
  55. */
  56. public function generateMultipleDaysTrends(
  57. string $startDate,
  58. string $endDate,
  59. ?int $itemId = null,
  60. ?FUND_CURRENCY_TYPE $currencyType = null
  61. ): Collection {
  62. $results = collect();
  63. $current = Carbon::parse($startDate);
  64. $end = Carbon::parse($endDate);
  65. while ($current <= $end) {
  66. $dateStr = $current->format('Y-m-d');
  67. if ($itemId && $currencyType) {
  68. // 生成指定商品的趋势
  69. $trend = $this->generateDailyTrend($dateStr, $itemId, $currencyType);
  70. if ($trend) {
  71. $results->push($trend);
  72. }
  73. } else {
  74. // 生成所有商品的趋势
  75. $this->generateAllItemsTrendsForDate($dateStr, $results);
  76. }
  77. $current->addDay();
  78. }
  79. return $results;
  80. }
  81. /**
  82. * 获取商品的价格趋势历史
  83. */
  84. public function getItemPriceTrends(
  85. int $itemId,
  86. FUND_CURRENCY_TYPE $currencyType,
  87. string $startDate,
  88. string $endDate,
  89. int $limit = 100
  90. ): Collection {
  91. $trends = MexDailyPriceTrend::where('item_id', $itemId)
  92. ->where('currency_type', $currencyType)
  93. ->whereBetween('trade_date', [$startDate, $endDate])
  94. ->orderBy('trade_date', 'desc')
  95. ->limit($limit)
  96. ->get();
  97. return $trends->map(fn($trend) => MexDailyPriceTrendDto::fromModel($trend));
  98. }
  99. /**
  100. * 获取价格趋势统计信息
  101. */
  102. public function getTrendStatistics(
  103. int $itemId,
  104. FUND_CURRENCY_TYPE $currencyType,
  105. string $startDate,
  106. string $endDate
  107. ): array {
  108. $trends = MexDailyPriceTrend::where('item_id', $itemId)
  109. ->where('currency_type', $currencyType)
  110. ->whereBetween('trade_date', [$startDate, $endDate])
  111. ->get();
  112. if ($trends->isEmpty()) {
  113. return [];
  114. }
  115. return [
  116. 'period_start' => $startDate,
  117. 'period_end' => $endDate,
  118. 'trading_days' => $trends->count(),
  119. 'total_volume' => $trends->sum('total_volume'),
  120. 'total_amount' => $trends->sum('total_amount'),
  121. 'avg_daily_volume' => $trends->avg('total_volume'),
  122. 'avg_daily_amount' => $trends->avg('total_amount'),
  123. 'highest_price' => $trends->max('high_price'),
  124. 'lowest_price' => $trends->min('low_price'),
  125. 'period_start_price' => $trends->sortBy('trade_date')->first()->open_price,
  126. 'period_end_price' => $trends->sortByDesc('trade_date')->first()->close_price,
  127. 'avg_volatility' => $trends->avg('volatility'),
  128. 'max_volatility' => $trends->max('volatility'),
  129. ];
  130. }
  131. /**
  132. * 计算价格统计
  133. */
  134. private function calculatePriceStatistics(Collection $transactions): array
  135. {
  136. $prices = $transactions->pluck('price');
  137. // 计算加权平均价格
  138. $totalAmount = $transactions->sum('total_amount');
  139. $totalQuantity = $transactions->sum('quantity');
  140. $avgPrice = $totalQuantity > 0 ? $totalAmount / $totalQuantity : 0;
  141. $openPrice = $transactions->first()->price;
  142. $closePrice = $transactions->last()->price;
  143. $highPrice = $prices->max();
  144. $lowPrice = $prices->min();
  145. // 计算波动率
  146. $volatility = $openPrice > 0 ? (($highPrice - $lowPrice) / $openPrice * 100) : 0;
  147. return [
  148. 'open_price' => $openPrice,
  149. 'close_price' => $closePrice,
  150. 'high_price' => $highPrice,
  151. 'low_price' => $lowPrice,
  152. 'avg_price' => $avgPrice,
  153. 'volatility' => $volatility,
  154. ];
  155. }
  156. /**
  157. * 计算交易统计
  158. */
  159. private function calculateTradeStatistics(Collection $transactions): array
  160. {
  161. $totalVolume = $transactions->sum('quantity');
  162. $totalAmount = $transactions->sum('total_amount');
  163. $transactionCount = $transactions->count();
  164. // 按交易类型分组统计
  165. $userBuyTransactions = $transactions->where('transaction_type', 'USER_BUY');
  166. $userSellTransactions = $transactions->where('transaction_type', 'USER_SELL');
  167. $adminInjectTransactions = $transactions->where('transaction_type', 'ADMIN_INJECT');
  168. $adminRecycleTransactions = $transactions->where('transaction_type', 'ADMIN_RECYCLE');
  169. return [
  170. 'total_volume' => $totalVolume,
  171. 'total_amount' => $totalAmount,
  172. 'transaction_count' => $transactionCount,
  173. 'buy_volume' => $userBuyTransactions->sum('quantity'),
  174. 'sell_volume' => $userSellTransactions->sum('quantity'),
  175. 'buy_amount' => $userBuyTransactions->sum('total_amount'),
  176. 'sell_amount' => $userSellTransactions->sum('total_amount'),
  177. 'admin_inject_volume' => $adminInjectTransactions->sum('quantity'),
  178. 'admin_recycle_volume' => $adminRecycleTransactions->sum('quantity'),
  179. 'admin_inject_amount' => $adminInjectTransactions->sum('total_amount'),
  180. 'admin_recycle_amount' => $adminRecycleTransactions->sum('total_amount'),
  181. ];
  182. }
  183. /**
  184. * 获取前一日收盘价
  185. */
  186. private function getPreviousClosePrice(string $date, int $itemId, FUND_CURRENCY_TYPE $currencyType): ?float
  187. {
  188. $previousDate = Carbon::parse($date)->subDay()->format('Y-m-d');
  189. $previousTrend = MexDailyPriceTrend::where('item_id', $itemId)
  190. ->where('currency_type', $currencyType)
  191. ->where('trade_date', $previousDate)
  192. ->first();
  193. return $previousTrend?->close_price;
  194. }
  195. /**
  196. * 计算价格变化
  197. */
  198. private function calculatePriceChange(float $currentPrice, ?float $previousPrice): array
  199. {
  200. if ($previousPrice === null || $previousPrice == 0) {
  201. return [
  202. 'price_change' => null,
  203. 'price_change_percent' => null,
  204. ];
  205. }
  206. $priceChange = $currentPrice - $previousPrice;
  207. $priceChangePercent = ($priceChange / $previousPrice) * 100;
  208. return [
  209. 'price_change' => $priceChange,
  210. 'price_change_percent' => $priceChangePercent,
  211. ];
  212. }
  213. /**
  214. * 生成指定日期所有商品的趋势数据
  215. */
  216. private function generateAllItemsTrendsForDate(string $date, Collection &$results): void
  217. {
  218. // 获取当日有交易的所有商品和币种组合
  219. $itemCurrencyPairs = MexTransaction::whereDate('created_at', $date)
  220. ->select('item_id', 'currency_type')
  221. ->distinct()
  222. ->get();
  223. foreach ($itemCurrencyPairs as $pair) {
  224. $trend = $this->generateDailyTrend($date, $pair->item_id, $pair->currency_type);
  225. if ($trend) {
  226. $results->push($trend);
  227. }
  228. }
  229. }
  230. /**
  231. * 从前一日数据或价格配置生成趋势记录
  232. */
  233. private function generateTrendFromPreviousOrConfig(string $date, int $itemId, FUND_CURRENCY_TYPE $currencyType): ?MexDailyPriceTrendDto
  234. {
  235. // 获取前一日收盘价
  236. $previousClosePrice = $this->getPreviousClosePrice($date, $itemId, $currencyType);
  237. if ($previousClosePrice !== null) {
  238. // 使用前一日收盘价作为当日所有价格
  239. $priceData = [
  240. 'open_price' => $previousClosePrice,
  241. 'close_price' => $previousClosePrice,
  242. 'high_price' => $previousClosePrice,
  243. 'low_price' => $previousClosePrice,
  244. 'avg_price' => $previousClosePrice,
  245. 'buy_open_price' => $previousClosePrice,
  246. 'buy_close_price' => $previousClosePrice,
  247. 'buy_high_price' => $previousClosePrice,
  248. 'buy_low_price' => $previousClosePrice,
  249. 'buy_avg_price' => $previousClosePrice,
  250. 'sell_open_price' => $previousClosePrice,
  251. 'sell_close_price' => $previousClosePrice,
  252. 'sell_high_price' => $previousClosePrice,
  253. 'sell_low_price' => $previousClosePrice,
  254. 'sell_avg_price' => $previousClosePrice,
  255. ];
  256. } else {
  257. // 使用价格配置数据
  258. $priceConfig = \App\Module\Mex\Logic\MexPriceConfigLogic::getItemPriceConfig($itemId);
  259. if (!$priceConfig) {
  260. return null; // 没有价格配置,无法生成趋势
  261. }
  262. // 使用价格配置的中间价作为参考价格
  263. $referencePrice = ($priceConfig['min_price'] + $priceConfig['max_price']) / 2;
  264. $priceData = [
  265. 'open_price' => $referencePrice,
  266. 'close_price' => $referencePrice,
  267. 'high_price' => $referencePrice,
  268. 'low_price' => $referencePrice,
  269. 'avg_price' => $referencePrice,
  270. 'buy_open_price' => $referencePrice,
  271. 'buy_close_price' => $referencePrice,
  272. 'buy_high_price' => $referencePrice,
  273. 'buy_low_price' => $referencePrice,
  274. 'buy_avg_price' => $referencePrice,
  275. 'sell_open_price' => $referencePrice,
  276. 'sell_close_price' => $referencePrice,
  277. 'sell_high_price' => $referencePrice,
  278. 'sell_low_price' => $referencePrice,
  279. 'sell_avg_price' => $referencePrice,
  280. ];
  281. }
  282. // 无成交时的统计数据
  283. $statsData = [
  284. 'total_volume' => 0,
  285. 'total_amount' => 0,
  286. 'transaction_count' => 0,
  287. 'buy_volume' => 0,
  288. 'sell_volume' => 0,
  289. 'buy_amount' => 0,
  290. 'sell_amount' => 0,
  291. 'admin_inject_volume' => 0,
  292. 'admin_recycle_volume' => 0,
  293. 'admin_inject_amount' => 0,
  294. 'admin_recycle_amount' => 0,
  295. 'volatility' => 0,
  296. ];
  297. // 计算价格变化
  298. $priceChange = $this->calculatePriceChange($priceData['close_price'], $previousClosePrice);
  299. // 创建或更新趋势记录
  300. $trend = MexDailyPriceTrend::updateOrCreate(
  301. [
  302. 'item_id' => $itemId,
  303. 'currency_type' => $currencyType,
  304. 'trade_date' => $date,
  305. ],
  306. array_merge($priceData, $statsData, $priceChange)
  307. );
  308. return MexDailyPriceTrendDto::fromModel($trend);
  309. }
  310. /**
  311. * 计算买入价格统计
  312. */
  313. private function calculateBuyPriceStatistics(Collection $transactions): array
  314. {
  315. // 筛选买入交易
  316. $buyTransactions = $transactions->where('transaction_type', 'USER_BUY');
  317. if ($buyTransactions->isEmpty()) {
  318. return [
  319. 'buy_open_price' => null,
  320. 'buy_close_price' => null,
  321. 'buy_high_price' => null,
  322. 'buy_low_price' => null,
  323. 'buy_avg_price' => null,
  324. ];
  325. }
  326. $buyPrices = $buyTransactions->pluck('price');
  327. // 计算买入加权平均价格
  328. $buyTotalAmount = $buyTransactions->sum('total_amount');
  329. $buyTotalQuantity = $buyTransactions->sum('quantity');
  330. $buyAvgPrice = $buyTotalQuantity > 0 ? $buyTotalAmount / $buyTotalQuantity : 0;
  331. return [
  332. 'buy_open_price' => $buyTransactions->first()->price,
  333. 'buy_close_price' => $buyTransactions->last()->price,
  334. 'buy_high_price' => $buyPrices->max(),
  335. 'buy_low_price' => $buyPrices->min(),
  336. 'buy_avg_price' => $buyAvgPrice,
  337. ];
  338. }
  339. /**
  340. * 计算卖出价格统计
  341. */
  342. private function calculateSellPriceStatistics(Collection $transactions): array
  343. {
  344. // 筛选卖出交易
  345. $sellTransactions = $transactions->where('transaction_type', 'USER_SELL');
  346. if ($sellTransactions->isEmpty()) {
  347. return [
  348. 'sell_open_price' => null,
  349. 'sell_close_price' => null,
  350. 'sell_high_price' => null,
  351. 'sell_low_price' => null,
  352. 'sell_avg_price' => null,
  353. ];
  354. }
  355. $sellPrices = $sellTransactions->pluck('price');
  356. // 计算卖出加权平均价格
  357. $sellTotalAmount = $sellTransactions->sum('total_amount');
  358. $sellTotalQuantity = $sellTransactions->sum('quantity');
  359. $sellAvgPrice = $sellTotalQuantity > 0 ? $sellTotalAmount / $sellTotalQuantity : 0;
  360. return [
  361. 'sell_open_price' => $sellTransactions->first()->price,
  362. 'sell_close_price' => $sellTransactions->last()->price,
  363. 'sell_high_price' => $sellPrices->max(),
  364. 'sell_low_price' => $sellPrices->min(),
  365. 'sell_avg_price' => $sellAvgPrice,
  366. ];
  367. }
  368. }