first(); if (! $last || (int) $last->price <= 30_000_000) { return; } $priceValue = (int) $last->price; } Price::create([ 'price' => $priceValue, 'date_at' => Carbon::now()->toDateString(), 'time_at' => Carbon::now()->toTimeString(), 'user_id' => null, ]); $this->touchCandle($priceValue); } private function touchCandle(int $priceValue): void { $candle = PriceCandle::whereDate('date_at', date('Y-m-d'))->first(); if ($candle) { $candle->end = $priceValue; if ($priceValue < $candle->low) { $candle->low = $priceValue; } elseif ($priceValue > $candle->high) { $candle->high = $priceValue; } $candle->save(); return; } PriceCandle::create([ 'start' => $priceValue, 'end' => $priceValue, 'high' => $priceValue, 'low' => $priceValue, 'date_at' => date('Y-m-d'), ]); } /** * پاک‌سازی شبانه: * - قبل از ۳۱ روز: حذف * - ۷ تا ۳۱ روز: هر ۱۲ ساعت یک رکورد * - ۲۴ ساعت تا ۷ روز: هر ۳۰ دقیقه یک رکورد * - ۲۴ ساعت اخیر: نگه‌داری همه (هر ۱۰ ثانیه) */ public function pruneHistory(): array { $deletedOld = Price::query() ->where('created_at', '<', now()->subDays(31)) ->delete(); $deleted7to31 = $this->downsampleRange(now()->subDays(31), now()->subDays(7), 12 * 3600); $deleted1to7 = $this->downsampleRange(now()->subDays(7), now()->subDay(), 30 * 60); return [ 'deleted_older_than_31d' => $deletedOld, 'downsampled_7_to_31d' => $deleted7to31, 'downsampled_1_to_7d' => $deleted1to7, ]; } private function downsampleRange(Carbon $from, Carbon $to, int $bucketSeconds): int { $rows = Price::query() ->where('created_at', '>=', $from) ->where('created_at', '<', $to) ->orderBy('created_at') ->get(['id', 'created_at']); if ($rows->isEmpty()) { return 0; } $keepIds = []; $lastBucket = null; foreach ($rows as $row) { $ts = $row->created_at?->timestamp ?? 0; $bucket = (int) floor($ts / $bucketSeconds); if ($bucket !== $lastBucket) { $keepIds[] = $row->id; $lastBucket = $bucket; } else { $keepIds[count($keepIds) - 1] = $row->id; } } return Price::query() ->where('created_at', '>=', $from) ->where('created_at', '<', $to) ->whereNotIn('id', $keepIds) ->delete(); } /** @return array{DAILY: array, WEEKLY: array, MONTHLY: array} */ public function chartFromDatabase(): array { $daily = Price::query() ->where('created_at', '>=', now()->subDay()) ->orderBy('created_at') ->get(['price', 'created_at']); $dailyPoints = $daily->map(function (Price $row) { $ts = ($row->created_at ?? now())->timestamp * 1000; return ['timestamp' => $ts, 'value' => (int) $row->price]; })->values()->all(); $weekRows = Price::query() ->where('created_at', '>=', now()->subDays(7)) ->orderBy('created_at') ->get(['id', 'price', 'created_at']); $weeklyPoints = $this->bucketRows($weekRows, 30 * 60); $monthRows = Price::query() ->where('created_at', '>=', now()->subDays(31)) ->orderBy('created_at') ->get(['id', 'price', 'created_at']); $monthlyPoints = $this->bucketRows($monthRows, 12 * 3600); return [ 'DAILY' => $dailyPoints, 'WEEKLY' => $weeklyPoints, 'MONTHLY' => $monthlyPoints, ]; } /** @param \Illuminate\Support\Collection $rows */ private function bucketRows($rows, int $bucketSeconds): array { $points = []; $lastBucket = null; foreach ($rows as $row) { $ts = $row->created_at?->timestamp ?? 0; $bucket = (int) floor($ts / $bucketSeconds); $point = [ 'timestamp' => $ts * 1000, 'value' => (int) $row->price, ]; if ($bucket !== $lastBucket) { $points[] = $point; $lastBucket = $bucket; } else { $points[count($points) - 1] = $point; } } return $points; } }