- Введение
- Что такое Laravel?
- Архитектура проекта
- Структура проекта
- Детальное описание компонентов
- Поток данных
- База данных
- Как запустить проект
- Как добавить новую функциональность
- Часто задаваемые вопросы
Crypto Spread - это Laravel-приложение для автоматического обнаружения арбитражных возможностей на криптовалютных биржах. Проект мониторит цены на разных биржах (Binance, OKX, Bybit) и отправляет уведомления в Telegram, когда обнаруживает возможность заработать на разнице цен.
Арбитраж - это покупка актива на одной бирже по низкой цене и одновременная продажа на другой бирже по более высокой цене. Разница в ценах (минус комиссии) - это прибыль.
Пример:
- BTC стоит $50,000 на Binance
- BTC стоит $50,500 на OKX
- Потенциальная прибыль: $500 (минус комиссии бирж)
Если вы знаете PHP, но не знакомы с Laravel, вот краткое объяснение:
Laravel - это PHP-фреймворк, который упрощает разработку веб-приложений. Основные концепции:
- Model (
app/Models/) - работа с базой данных - View (
resources/views/) - HTML шаблоны - Controller (
app/Http/Controllers/) - обработка запросов
В нашем проекте мы используем в основном Models и Services (бизнес-логика).
Вместо SQL запросов вы пишете PHP код:
// Вместо: SELECT * FROM users WHERE telegram_id = '123'
$user = User::where('telegram_id', '123')->first();
// Вместо: INSERT INTO prices ...
Price::create([
'exchange_id' => 1,
'symbol' => 'BTC/USDT',
'price' => 50000
]);Laravel автоматически передает зависимости в конструкторы:
// Laravel автоматически создаст TelegramBotService и передаст его
public function __construct(TelegramBotService $telegramService) {
$this->telegramService = $telegramService;
}Асинхронное выполнение задач через очереди (Redis):
// Задача будет выполнена в фоне
FetchPricesJob::dispatch('BTC/USDT');Консольные команды для выполнения задач:
php artisan telegram:bot # Запустить Telegram бота
php artisan arbitrage:fetch-prices # Запустить фетчер ценПроект состоит из нескольких независимых компонентов, которые работают вместе:
┌─────────────────┐
│ Price Fetcher │ → Получает цены с бирж
│ (Command) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ FetchPricesJob │ → Сохраняет цены в БД
└────────┬────────┘
│
▼
┌─────────────────┐
│CheckArbitrageJob│ → Анализирует цены
└────────┬────────┘
│
▼
┌─────────────────┐
│ ArbitrageService│ → Находит арбитраж
└────────┬────────┘
│
▼
┌──────────────────────┐
│SendTelegramNotifJob │ → Отправляет уведомления
└────────┬─────────────┘
│
▼
┌─────────────────┐
│ Telegram Bot │ → Пользователь получает сообщение
│ (Command) │
└─────────────────┘
Что такое Model? Model - это класс, который представляет таблицу в базе данных. Каждая модель соответствует одной таблице.
User.php- Пользователи Telegram ботаExchange.php- Криптовалютные биржиPrice.php- История цен с биржAlert.php- Обнаруженные арбитражные возможности
Что такое Service? Service - это класс, который содержит бизнес-логику (не связанную напрямую с БД или HTTP).
ArbitrageService.php- Логика обнаружения арбитражаTelegramBotService.php- Работа с Telegram APIExchange/- Сервисы для работы с биржамиExchangeServiceInterface.php- Интерфейс (контракт)BinanceService.php- Интеграция с BinanceOKXService.php- Интеграция с OKXBybitService.php- Интеграция с BybitExchangeServiceFactory.php- Фабрика для создания сервисов
Что такое Job? Job - это задача, которая выполняется асинхронно через очередь (Redis). Полезно для долгих операций.
FetchPricesJob.php- Получение цен с биржCheckArbitrageJob.php- Проверка арбитражных возможностейSendTelegramNotificationJob.php- Отправка уведомлений
Что такое Command?
Command - это команда, которую можно запустить из терминала через php artisan.
TelegramBotCommand.php- Запуск Telegram ботаRunPriceFetcherCommand.php- Запуск фетчера цен
migrations/- Миграции (схема БД)seeders/- Сидеры (начальные данные)
services.php- Настройки внешних сервисов (Telegram)database.php- Настройки БДqueue.php- Настройки очередей
web.php- HTTP маршрутыconsole.php- Консольные команды
class User extends Model {
protected $fillable = [
'telegram_id', // ID пользователя в Telegram
'password_hash', // Хеш пароля (bcrypt)
'is_active', // Активен ли пользователь
'alerts_enabled', // Включены ли уведомления
];
}Что делает:
- Хранит информацию о пользователях Telegram бота
- Метод
verifyPassword()- проверяет пароль
Пример использования:
// Найти пользователя по Telegram ID
$user = User::where('telegram_id', '123456789')->first();
// Создать нового пользователя
$user = User::create([
'telegram_id' => '123456789',
'password_hash' => Hash::make('my_password'),
'is_active' => true,
'alerts_enabled' => true,
]);
// Обновить настройки
$user->update(['alerts_enabled' => false]);class Exchange extends Model {
protected $fillable = [
'name', // Название биржи (Binance, OKX, Bybit)
'api_url', // URL API биржи
'priority', // Приоритет (1 = самый важный)
'is_active', // Активна ли биржа
];
}Что делает:
- Хранит информацию о биржах
- Метод
getLatestPrice($symbol)- получает последнюю цену для символа - Связь
prices()- все цены этой биржи
Пример использования:
// Получить все активные биржи
$exchanges = Exchange::where('is_active', true)->get();
// Получить последнюю цену BTC/USDT на Binance
$exchange = Exchange::where('name', 'Binance')->first();
$price = $exchange->getLatestPrice('BTC/USDT');class Price extends Model {
protected $fillable = [
'exchange_id', // ID биржи
'symbol', // Пара (BTC/USDT)
'price', // Цена
];
}Что делает:
- Хранит историю цен с бирж
- Связь
exchange()- биржа, которой принадлежит цена
Пример использования:
// Сохранить новую цену
Price::create([
'exchange_id' => 1,
'symbol' => 'BTC/USDT',
'price' => 50000.50,
]);
// Получить все цены для BTC/USDT
$prices = Price::where('symbol', 'BTC/USDT')
->orderBy('created_at', 'desc')
->get();class Alert extends Model {
protected $fillable = [
'symbol', // Пара (BTC/USDT)
'buy_exchange', // Биржа для покупки
'sell_exchange', // Биржа для продажи
'buy_price', // Цена покупки
'sell_price', // Цена продажи
'profit_percent', // Процент прибыли
'notified', // Отправлено ли уведомление
];
}Что делает:
- Хранит обнаруженные арбитражные возможности
Пример использования:
// Создать новый сигнал
$alert = Alert::create([
'symbol' => 'BTC/USDT',
'buy_exchange' => 'Binance',
'sell_exchange' => 'OKX',
'buy_price' => 50000,
'sell_price' => 50500,
'profit_percent' => 0.8,
'notified' => false,
]);Что делает: Этот сервис анализирует цены с разных бирж и находит арбитражные возможности.
Основной метод: checkArbitrage($symbol)
public function checkArbitrage(string $symbol): ?AlertКак работает:
- Получает все активные биржи из БД
- Для каждой биржи получает последнюю цену (не старше 1 минуты)
- Находит самую низкую цену (где покупать) и самую высокую (где продавать)
- Вычисляет процент прибыли:
((sell_price - buy_price) / buy_price) * 100 - Вычитает комиссии бирж (0.2% = 0.1% на покупку + 0.1% на продажу)
- Если прибыль >= 0.5%, создает Alert
Константы:
MIN_PROFIT_PERCENT = 0.5- минимальный процент прибылиEXCHANGE_FEE_PERCENT = 0.1- комиссия одной биржи
Пример использования:
$arbitrageService = new ArbitrageService();
$alert = $arbitrageService->checkArbitrage('BTC/USDT');
if ($alert) {
echo "Найдена возможность! Прибыль: {$alert->profit_percent}%";
}Что делает: Этот сервис отправляет сообщения в Telegram и получает обновления от бота.
Основные методы:
sendMessage($chatId, $message)- Отправить сообщениеgetUpdates($offset)- Получить новые сообщенияformatArbitrageAlert($alert)- Форматировать сообщение об арбитраже
Как работает:
Использует HTTP клиент (Guzzle) для отправки запросов к Telegram API:
https://api.telegram.org/bot{TOKEN}/sendMessage- отправка сообщенияhttps://api.telegram.org/bot{TOKEN}/getUpdates- получение обновлений
Пример использования:
$telegramService = new TelegramBotService();
// Отправить сообщение
$telegramService->sendMessage('123456789', 'Привет!');
// Получить новые сообщения
$updates = $telegramService->getUpdates();Что такое Interface? Интерфейс - это "контракт", который определяет, какие методы должен иметь класс. Все сервисы бирж должны реализовывать этот интерфейс.
interface ExchangeServiceInterface {
public function getTickerPrice(string $symbol): ?float;
public function getName(): string;
}Зачем это нужно? Это позволяет использовать любой сервис биржи одинаково, не зная конкретной реализации:
// Работает с любой биржей одинаково
$service = ExchangeServiceFactory::create('Binance');
$price = $service->getTickerPrice('BTC/USDT');Что делает: Получает цены с API Binance.
Как работает:
- Преобразует символ
BTC/USDT→BTCUSDT(формат Binance) - Отправляет GET запрос:
https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT - Парсит ответ и возвращает цену
Пример ответа от API:
{
"symbol": "BTCUSDT",
"price": "50000.50"
}Аналогично работают:
OKXService.php- для OKXBybitService.php- для Bybit
Что такое Factory? Фабрика - это паттерн проектирования, который создает объекты.
Что делает: Создает нужный сервис биржи по имени:
// Создать сервис Binance
$service = ExchangeServiceFactory::create('Binance');
// Создать сервис из модели Exchange
$exchange = Exchange::find(1);
$service = ExchangeServiceFactory::createFromModel($exchange);Зачем это нужно? Вместо того чтобы писать:
if ($exchange->name === 'Binance') {
$service = new BinanceService();
} elseif ($exchange->name === 'OKX') {
$service = new OKXService();
}Мы просто пишем:
$service = ExchangeServiceFactory::createFromModel($exchange);Что делает: Получает цены со всех активных бирж и сохраняет их в БД.
Как работает:
- Получает все активные биржи из БД
- Для каждой биржи:
- Создает сервис через Factory
- Получает цену через API
- Сохраняет цену в БД
- Делает паузу 200ms (чтобы не превысить лимиты API)
Когда выполняется:
Вызывается из команды RunPriceFetcherCommand каждые N секунд.
Пример использования:
// Добавить задачу в очередь
FetchPricesJob::dispatch('BTC/USDT');
// Выполнить сразу (синхронно)
FetchPricesJob::dispatchSync('BTC/USDT');Что делает: Проверяет наличие арбитражных возможностей и отправляет уведомления.
Как работает:
- Вызывает
ArbitrageService::checkArbitrage() - Если найдена возможность:
- Получает всех активных пользователей с включенными уведомлениями
- Для каждого пользователя создает задачу
SendTelegramNotificationJob
Когда выполняется:
Вызывается из команды RunPriceFetcherCommand после получения цен.
Что делает: Отправляет уведомление об арбитраже конкретному пользователю.
Как работает:
- Получает Alert из БД
- Проверяет, не отправлено ли уже уведомление
- Форматирует сообщение через
TelegramBotService::formatArbitrageAlert() - Отправляет сообщение через
TelegramBotService::sendMessage() - Помечает Alert как
notified = true
Повторы:
Если отправка не удалась, задача повторится до 3 раз ($tries = 3).
Что делает: Запускает Telegram бота, который обрабатывает команды пользователей.
Как работает:
- Запускается бесконечный цикл (
while (true)) - Каждую секунду получает новые сообщения через
getUpdates() - Обрабатывает команды:
/start- Приветствие/set_password YOUR_PASSWORD- Аутентификация/alerts_on- Включить уведомления/alerts_off- Выключить уведомления/history- Показать последние 10 сигналов
Запуск:
php artisan telegram:botКак обрабатываются команды:
switch ($command) {
case '/start':
$this->handleStart($chatId);
break;
case '/set_password':
$this->handleSetPassword($chatId, $text);
break;
// ...
}Что делает: Запускает процесс получения цен и проверки арбитража.
Как работает:
- Запускается бесконечный цикл
- Каждые N секунд (по умолчанию 5):
- Добавляет задачу
FetchPricesJobв очередь - Ждет 1 секунду (чтобы цены успели сохраниться)
- Добавляет задачу
CheckArbitrageJobв очередь
- Добавляет задачу
Запуск:
# Мониторить BTC/USDT каждые 5 секунд
php artisan arbitrage:fetch-prices BTC/USDT --interval=5
# Мониторить ETH/USDT каждые 10 секунд
php artisan arbitrage:fetch-prices ETH/USDT --interval=101. Запуск команды: php artisan arbitrage:fetch-prices BTC/USDT
│
▼
2. RunPriceFetcherCommand каждые 5 секунд:
│
├─→ FetchPricesJob::dispatch('BTC/USDT')
│ │
│ ├─→ Exchange::where('is_active', true)->get()
│ │
│ ├─→ Для каждой биржи:
│ │ ├─→ ExchangeServiceFactory::createFromModel($exchange)
│ │ ├─→ $service->getTickerPrice('BTC/USDT')
│ │ │ └─→ HTTP запрос к API биржи
│ │ └─→ Price::create([...])
│ │
│ └─→ Цены сохранены в БД
│
└─→ CheckArbitrageJob::dispatch('BTC/USDT')
│
├─→ ArbitrageService::checkArbitrage('BTC/USDT')
│ │
│ ├─→ Получить все активные биржи
│ ├─→ Получить последние цены (не старше 1 минуты)
│ ├─→ Найти минимальную и максимальную цену
│ ├─→ Вычислить прибыль
│ └─→ Если прибыль >= 0.5%, создать Alert
│
└─→ Если Alert создан:
│
├─→ User::where('is_active', true)
│ ->where('alerts_enabled', true)
│ ->get()
│
└─→ Для каждого пользователя:
└─→ SendTelegramNotificationJob::dispatch($telegramId, $alertId)
│
├─→ Alert::find($alertId)
├─→ TelegramBotService::formatArbitrageAlert($alert)
├─→ TelegramBotService::sendMessage($telegramId, $message)
└─→ Alert::update(['notified' => true])
┌─────────────────────┐
│ Queue Worker │ ← Выполняет Jobs из очереди Redis
│ (php artisan │
│ queue:work) │
└─────────────────────┘
┌─────────────────────┐
│ Telegram Bot │ ← Обрабатывает команды пользователей
│ (php artisan │
│ telegram:bot) │
└─────────────────────┘
┌─────────────────────┐
│ Price Fetcher │ ← Получает цены и проверяет арбитраж
│ (php artisan │
│ arbitrage:...) │
└─────────────────────┘
Все три процесса работают одновременно и независимо друг от друга.
users
├── id (bigint, primary key)
├── telegram_id (string, unique)
├── password_hash (string)
├── is_active (boolean)
├── alerts_enabled (boolean)
├── created_at (timestamp)
└── updated_at (timestamp)
exchanges
├── id (bigint, primary key)
├── name (string, unique) # Binance, OKX, Bybit
├── api_url (string) # URL API биржи
├── priority (integer) # 1 = самый важный
├── is_active (boolean)
├── created_at (timestamp)
└── updated_at (timestamp)
prices
├── id (bigint, primary key)
├── exchange_id (bigint, foreign key → exchanges.id)
├── symbol (string) # BTC/USDT, ETH/USDT
├── price (decimal 8,2)
├── created_at (timestamp)
└── updated_at (timestamp)
alerts
├── id (bigint, primary key)
├── symbol (string)
├── buy_exchange (string) # Название биржи
├── sell_exchange (string)
├── buy_price (decimal 8,2)
├── sell_price (decimal 8,2)
├── profit_percent (decimal 4,2)
├── notified (boolean)
├── created_at (timestamp)
└── updated_at (timestamp)
Exchange → Price (One-to-Many)
- Одна биржа имеет много цен
$exchange->prices- все цены биржи
Price → Exchange (Many-to-One)
- Много цен принадлежат одной бирже
$price->exchange- биржа цены
Пример использования связей:
// Получить все цены биржи Binance
$exchange = Exchange::where('name', 'Binance')->first();
$prices = $exchange->prices;
// Получить биржу, которой принадлежит цена
$price = Price::find(1);
$exchange = $price->exchange;composer installСкопируйте .env.example в .env:
cp .env.example .envОтредактируйте .env:
# База данных
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=crypto_spread
DB_USERNAME=crypto_user
DB_PASSWORD=your_password
# Redis (для очередей)
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
# Telegram
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
TELEGRAM_BOT_PASSWORD=your_secure_password- Найдите @BotFather в Telegram
- Отправьте
/newbotи следуйте инструкциям - Скопируйте токен в
.env
php artisan migrate
php artisan db:seedЭто создаст таблицы и заполнит их начальными данными (биржи).
Откройте 3 терминала:
Терминал 1 - Воркер очередей:
php artisan queue:work redis --tries=3Терминал 2 - Telegram бот:
php artisan telegram:botТерминал 3 - Фетчер цен:
php artisan arbitrage:fetch-prices BTC/USDT --interval=5- Найдите вашего бота в Telegram
- Отправьте
/start - Отправьте
/set_password YOUR_PASSWORD(пароль из.env) - Готово! Вы будете получать уведомления об арбитраже
Шаг 1: Создайте сервис биржи
Создайте файл app/Services/Exchange/KrakenService.php:
<?php
namespace App\Services\Exchange;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Log;
class KrakenService implements ExchangeServiceInterface {
private Client $client;
private string $baseUrl = 'https://api.kraken.com/0/public';
public function __construct() {
$this->client = new Client([
'timeout' => 10,
'verify' => true,
]);
}
public function getTickerPrice(string $symbol): ?float {
try {
// Преобразуем BTC/USDT в формат Kraken (XBTUSDT)
$normalizedSymbol = $this->normalizeSymbol($symbol);
$response = $this->client->get("{$this->baseUrl}/Ticker", [
'query' => ['pair' => $normalizedSymbol],
]);
$data = json_decode($response->getBody()->getContents(), true);
// Парсим ответ Kraken (формат может отличаться)
if (isset($data['result'][$normalizedSymbol]['c'][0])) {
return (float) $data['result'][$normalizedSymbol]['c'][0];
}
return null;
} catch (GuzzleException $e) {
Log::error("Kraken API error: " . $e->getMessage());
return null;
}
}
public function getName(): string {
return 'Kraken';
}
private function normalizeSymbol(string $symbol): string {
// BTC/USDT → XBTUSDT (специфика Kraken)
$symbol = str_replace('/', '', $symbol);
return str_replace('BTC', 'XBT', $symbol);
}
}Шаг 2: Добавьте в Factory
Отредактируйте app/Services/Exchange/ExchangeServiceFactory.php:
public static function create(string $exchangeName): ?ExchangeServiceInterface {
$exchangeName = strtolower($exchangeName);
switch ($exchangeName) {
case 'binance':
return new BinanceService();
case 'okx':
return new OKXService();
case 'bybit':
return new BybitService();
case 'kraken': // ← Добавьте это
return new KrakenService();
default:
return null;
}
}Шаг 3: Добавьте биржу в БД
Отредактируйте database/seeders/ExchangeSeeder.php:
$exchanges = [
// ... существующие биржи
[
'name' => 'Kraken',
'api_url' => 'https://api.kraken.com/0/public',
'priority' => 4,
'is_active' => true,
],
];Шаг 4: Запустите сидер
php artisan db:seed --class=ExchangeSeederГотово! Теперь система будет получать цены с Kraken.
Шаг 1: Добавьте обработчик в TelegramBotCommand.php
private function handleMessage(array $message): void {
// ... существующий код
switch ($command) {
// ... существующие команды
case '/status': // ← Добавьте это
$this->handleStatus($chatId);
break;
}
}
// Добавьте новый метод
private function handleStatus(string $chatId): void {
$user = User::where('telegram_id', $chatId)->first();
if (!$user || !$user->is_active) {
$this->telegramService->sendMessage(
$chatId,
"❌ Please authenticate first using /set_password"
);
return;
}
$alertsCount = Alert::count();
$activeExchanges = Exchange::where('is_active', true)->count();
$message = "📊 <b>System Status</b>\n\n" .
"Active exchanges: {$activeExchanges}\n" .
"Total alerts: {$alertsCount}\n" .
"Your alerts: " . ($user->alerts_enabled ? "✅ Enabled" : "❌ Disabled");
$this->telegramService->sendMessage($chatId, $message);
}Теперь пользователи смогут использовать команду /status.
Отредактируйте app/Services/ArbitrageService.php:
class ArbitrageService {
// Измените значение
private const MIN_PROFIT_PERCENT = 1.0; // Было 0.5, стало 1.0
// Или сделайте настраиваемым через .env
private function getMinProfitPercent(): float {
return (float) config('arbitrage.min_profit_percent', 0.5);
}
}Добавьте в config/arbitrage.php (создайте файл):
<?php
return [
'min_profit_percent' => env('ARBITRAGE_MIN_PROFIT_PERCENT', 0.5),
];В .env:
ARBITRAGE_MIN_PROFIT_PERCENT=1.0A: Eloquent - это ORM (Object-Relational Mapping) Laravel. Он позволяет работать с БД через PHP объекты вместо SQL запросов.
// Вместо SQL:
// SELECT * FROM users WHERE telegram_id = '123'
// Eloquent:
$user = User::where('telegram_id', '123')->first();A: Это когда Laravel автоматически передает зависимости в конструктор:
// Laravel автоматически создаст TelegramBotService
public function __construct(TelegramBotService $telegramService) {
$this->telegramService = $telegramService;
}A: Очереди позволяют выполнять задачи асинхронно:
// Задача будет выполнена в фоне (не блокирует выполнение)
FetchPricesJob::dispatch('BTC/USDT');Воркер очереди (php artisan queue:work) постоянно проверяет очередь и выполняет задачи.
A: Используйте Log facade:
use Illuminate\Support\Facades\Log;
Log::info("Something happened", ['data' => $data]);
Log::warning("Warning message");
Log::error("Error occurred", ['error' => $exception]);Логи сохраняются в storage/logs/laravel.log.
A: Используйте dd() (dump and die):
dd($variable); // Выведет переменную и остановит выполнениеИли dump() (не останавливает выполнение):
dump($variable);A: Laravel использует Composer autoloader. Классы автоматически загружаются по namespace:
// Файл: app/Services/ArbitrageService.php
namespace App\Services;
// Использование:
use App\Services\ArbitrageService;A: Миграции - это версионирование схемы БД. Они описывают структуру таблиц в PHP коде.
// Создать таблицу
Schema::create('prices', function (Blueprint $table) {
$table->id();
$table->foreignId('exchange_id');
$table->string('symbol');
$table->decimal('price', 8, 2);
$table->timestamps();
});A: Конфигурация хранится в config/ и может быть переопределена через .env:
// config/services.php
'telegram' => [
'bot_token' => env('TELEGRAM_BOT_TOKEN'),
],
// .env
TELEGRAM_BOT_TOKEN=your_token_here
// Использование:
config('services.telegram.bot_token')# Список всех команд
php artisan list
# Запустить миграции
php artisan migrate
# Откатить последнюю миграцию
php artisan migrate:rollback
# Запустить сидеры
php artisan db:seed
# Очистить кеш
php artisan cache:clear
php artisan config:clear
# Просмотреть логи
tail -f storage/logs/laravel.log
# Войти в Tinker (интерактивная консоль)
php artisan tinkerЭтот проект демонстрирует основные концепции Laravel:
- Models - работа с БД
- Services - бизнес-логика
- Jobs - асинхронные задачи
- Commands - консольные команды
- Dependency Injection - автоматическая передача зависимостей
Если у вас есть вопросы, изучайте код, читайте логи и экспериментируйте!
Удачи в разработке! 🚀