Ошибка получения данных с MOEX'); } $securities = []; foreach ($data['securities']['data'] as $row) { $sec = array_combine($data['securities']['columns'], $row); $securities[$sec['SECID']] = $sec; } if (isset($data['marketdata']['data'])) { foreach ($data['marketdata']['data'] as $row) { $md = array_combine($data['marketdata']['columns'], $row); if (isset($securities[$md['SECID']])) { $securities[$md['SECID']]['LAST'] = $md['LAST']; $securities[$md['SECID']]['YIELD'] = $md['YIELD']; } } } $today = new DateTime(); $targetDate = clone $today; $targetDate->modify('+365 days'); $allPayments = []; $bondsInfo = []; foreach ($securities as $sec) { if ($sec['CURRENCYID'] !== 'SUR') continue; $is26 = strpos($sec['SECID'], 'SU26') === 0; $is29 = strpos($sec['SECID'], 'SU29') === 0; if (!$is26 && !$is29) continue; $matDateStr = $sec['MATDATE'] ?? ''; if (empty($matDateStr) || empty($sec['NEXTCOUPON'])) continue; try { $matDate = new DateTime($matDateStr); if ($matDate <= $today) continue; } catch (Exception $e) { continue; } $price = !is_null($sec['LAST']) ? (float)$sec['LAST'] : (float)$sec['PREVPRICE']; $faceValue = (float)($sec['FACEVALUE'] ?? 1000); $bondPrice = ($price / 100) * $faceValue; $discount = $faceValue - $bondPrice; $couponValue = max(0.01, (float)$sec['COUPONVALUE']); $yield = (float)($sec['YIELD'] ?? 0); $couponPeriod = max(1, (int)($sec['COUPONPERIOD'] ?? 182)); $type = $is26 ? 'Фикс' : 'Флоат'; $colorClass = $is26 ? 'tag-fix' : 'tag-float'; $qtyFor1000 = 0; $payoutFor1000 = 0; if ($is29) { $qtyFor1000 = 25; $payoutFor1000 = 1000; } else { if ($yield > 0 && $couponValue > 0) { $qtyFor1000 = (int)ceil(1000 / $couponValue); $payoutFor1000 = $qtyFor1000 * $couponValue; } } $bondsInfo[$sec['SECID']] = [ 'secid' => $sec['SECID'], 'name' => $sec['SHORTNAME'], 'type' => $type, 'colorClass' => $colorClass, 'coupon' => $couponValue, 'yield' => $yield, 'isFloat' => $is29, 'qtyFor1000' => $qtyFor1000, 'payoutFor1000' => $payoutFor1000, 'bondPrice' => $bondPrice, 'discount' => $discount, 'costFor1000' => $qtyFor1000 * $bondPrice ]; $currentCouponDate = new DateTime($sec['NEXTCOUPON']); while ($currentCouponDate <= $targetDate) { $allPayments[] = [ 'date' => $currentCouponDate->format('Y-m-d'), 'secid' => $sec['SECID'], 'name' => $sec['SHORTNAME'], 'type' => $type, 'colorClass' => $colorClass, 'coupon' => $couponValue, 'yield' => $yield, 'isFloat' => $is29, 'qtyFor1000' => $qtyFor1000, 'payoutFor1000' => $payoutFor1000, 'discount' => $discount ]; $currentCouponDate->modify('+' . $couponPeriod . ' days'); } } usort($allPayments, function($a, $b) { return strcmp($a['date'], $b['date']); }); $groupedPayments = []; foreach ($allPayments as $payment) { $groupedPayments[$payment['date']][] = $payment; } $selectedSecids = []; foreach ($groupedPayments as $date => &$payments) { $bestIdx = -1; $fixPayments = array_filter($payments, fn($p) => !$p['isFloat'] && $p['yield'] > 0); if (!empty($fixPayments)) { $maxYield = -1; foreach ($payments as $idx => $p) { if (!$p['isFloat'] && $p['yield'] > $maxYield) { $maxYield = $p['yield']; $bestIdx = $idx; } } } else { $floatPayments = array_filter($payments, fn($p) => $p['isFloat']); if (!empty($floatPayments)) { $maxDiscount = -PHP_FLOAT_MAX; foreach ($payments as $idx => $p) { if ($p['isFloat'] && $p['discount'] > $maxDiscount) { $maxDiscount = $p['discount']; $bestIdx = $idx; } } } } if ($bestIdx >= 0) { $payments[$bestIdx]['inPortfolio'] = true; $payments[$bestIdx]['isBest'] = true; $selectedSecids[$payments[$bestIdx]['secid']] = true; } } unset($payments); $portfolioBonds = []; foreach ($bondsInfo as $secid => $bond) { if (isset($selectedSecids[$secid])) { $portfolioBonds[$secid] = $bond; } } $ladder = []; $prevDate = null; foreach ($groupedPayments as $date => $payments) { $currentDate = new DateTime($date); $daysDiff = $prevDate ? $currentDate->diff($prevDate)->days : 0; $totalPayout = 0; foreach ($payments as $p) { if (!empty($p['inPortfolio'])) { $totalPayout += $p['payoutFor1000']; } } $ladder[] = [ 'date' => $date, 'daysSinceLast' => $daysDiff, 'payments' => $payments, 'totalPayout' => $totalPayout ]; $prevDate = $currentDate; } $totalPortfolioCost = array_sum(array_column($portfolioBonds, 'costFor1000')); $totalAnnualPayout = array_sum(array_column($ladder, 'totalPayout')); $calendar = []; $currentMonth = clone $today; $monthNames = ['', 'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь']; $dayNames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; for ($i = 0; $i < 12; $i++) { $year = (int)$currentMonth->format('Y'); $month = (int)$currentMonth->format('m'); $monthName = $monthNames[$month] . ' ' . $year; $firstDay = new DateTime("$year-$month-01"); $lastDay = new DateTime("$year-$month-" . $firstDay->format('t')); $daysInMonth = (int)$lastDay->format('d'); $weeks = []; $week = array_fill(1, 7, null); for ($day = 1; $day <= $daysInMonth; $day++) { $dayOfWeek = (int)$firstDay->format('N'); $currentDateStr = sprintf("%04d-%02d-%02d", $year, $month, $day); $dayPayments = $groupedPayments[$currentDateStr] ?? []; $week[$dayOfWeek] = [ 'day' => $day, 'payments' => $dayPayments ]; if ($dayOfWeek == 7 || $day == $daysInMonth) { $weeks[] = $week; $week = array_fill(1, 7, null); } $firstDay->modify('+1 day'); } $calendar[] = [ 'monthName' => $monthName, 'weeks' => $weeks ]; $currentMonth->modify('+1 month'); } ?>
Стоимость портфеля
Для получения ~1 000 ₽ с каждой выплаты · ОФЗ 26 (фикс) и 29 (флоат)
Цель - собрать минимальный портфель из ОФЗ, чтобы получать ~1 000 ₽ с каждой купонной выплаты так часто, насколько это возможно.
💡 Итоговая сумма портфеля - это сколько нужно вложить прямо сейчас, чтобы весь год получать купоны примерно по 1 000 ₽ с каждой выплаты. Не учитывает НКД.
| Дата | Пауза | Облигации | Выплата |
|---|---|---|---|
|
= date('d.m.Y', strtotime($step['date'])) ?>
|
— = $step['daysSinceLast'] ?> дн. |
= !empty($p['inPortfolio']) ? '💼 ' : '' ?>
= htmlspecialchars($p['name']) ?>
= $p['qtyFor1000'] ?> шт.
|
0): ?> = number_format($step['totalPayout'], 0, '.', ' ') ?> ₽ — |
| Облигация | Тип | Купон | Доходность | Цена | Кол-во | Сумма | |
|---|---|---|---|---|---|---|---|
| Нет бумаг, удовлетворяющих критериям отбора | |||||||
| = htmlspecialchars($bond['name']) ?> | = $bond['type'] ?> | = number_format($bond['coupon'], 2, '.', ' ') ?> ₽ | н/д = number_format($bond['yield'], 2, '.', ' ') ?>% | = number_format($bond['bondPrice'], 2, '.', ' ') ?> ₽ | = $bond['qtyFor1000'] ?> | = number_format($bond['costFor1000'], 0, '.', ' ') ?> ₽ | |
| Итого в портфель | = number_format($totalPortfolioCost, 0, '.', ' ') ?> ₽ | ||||||