Реализация запуска/остановки торговых стратегий через мобильное приложение
Кажется, что запуск/остановка бота — это одна кнопка. На самом деле это sequence of actions с подтверждениями, проверками состояния и обработкой edge cases. Нажать «Стоп», пока есть открытые позиции, — и что должно произойти? Закрыть позиции? Дождаться естественного закрытия? Просто остановить новые ордера?
Состояния бота и переходы
Торговый бот — это FSM. Минимальный набор состояний:
- STOPPED — бот не работает, позиций нет
- STARTING — идёт инициализация (подключение к бирже, загрузка состояния)
- RUNNING — активно торгует
- STOPPING — получил команду стоп, дожидается закрытия текущего цикла
- ERROR — ошибка (неверный API-ключ, недостаточно средств, биржа недоступна)
Мобильный UI должен отражать каждое состояние и блокировать несовместимые действия:
// iOS, SwiftUI
struct BotControlView: View {
@ObservedObject var viewModel: BotControlViewModel
var body: some View {
VStack(spacing: 16) {
StatusBadge(status: viewModel.bot.status)
switch viewModel.bot.status {
case .stopped:
Button("Запустить") { viewModel.start() }
.buttonStyle(.primary)
case .running:
Button("Остановить") { viewModel.showStopDialog = true }
.buttonStyle(.destructive)
case .starting, .stopping:
HStack {
ProgressView()
Text(viewModel.bot.status == .starting ? "Запуск..." : "Останавливаем...")
}
.foregroundColor(.secondary)
case .error(let message):
VStack {
Label(message, systemImage: "exclamationmark.triangle")
.foregroundColor(.red)
Button("Перезапустить") { viewModel.start() }
}
}
}
.confirmationDialog("Остановить бота?", isPresented: $viewModel.showStopDialog) {
Button("Остановить и закрыть позиции", role: .destructive) {
viewModel.stop(closePositions: true)
}
Button("Остановить без закрытия") {
viewModel.stop(closePositions: false)
}
Button("Отмена", role: .cancel) {}
}
}
}
confirmationDialog на iOS — нативный action sheet. Пользователь явно выбирает поведение при остановке.
Оптимистичный vs. реальный статус
Когда пользователь нажал «Запустить», кнопка должна сразу перейти в STARTING, не ждать ответа сервера. Но настоящий статус приходит с сервера. Два источника истины конфликтуют.
Решение: оптимистично ставим локальный pendingStatus, одновременно отправляем запрос. При ответе сервера — заменяем локальный статус серверным. Если запрос упал — откатываем локальный статус и показываем ошибку.
// Android, ViewModel
fun start() {
_uiState.update { it.copy(localStatus = BotStatus.STARTING) }
viewModelScope.launch {
runCatching { repository.startBot(botId) }
.onSuccess { serverBot -> _uiState.update { it.copy(bot = serverBot, localStatus = null) } }
.onFailure { err ->
_uiState.update { it.copy(localStatus = null, error = err.message) }
}
}
}
Мультистратегийный бот
Если бот поддерживает несколько стратегий, запуск/стоп — отдельно для каждой. Список стратегий с индивидуальными toggle-переключателями и агрегированным статусом бота сверху. Toggle для конкретной стратегии отправляет PATCH /bots/{id}/strategies/{strategyId} с {active: true/false}.
Важно: изменение активности стратегии и остановка бота — разные операции. Деактивация стратегии убирает её из торговли, но бот продолжает работать с оставшимися.
Push-уведомления о смене статуса
При переходе бота в ERROR или при автоматической остановке (например, достигнут дневной лимит убытков) — push через FCM/APNs. Пользователь должен узнать, не открывая приложение.
На бэкенде: после каждого изменения статуса публикуется событие в очередь, воркер отправляет push. Мобильное приложение регистрирует FCM token при логине и обновляет при каждом запуске приложения.
Что входит в работу
- Компонент управления ботом с отображением всех статусов
- Confirmation dialog с выбором режима остановки
- Оптимистичное обновление статуса
- Управление отдельными стратегиями (если применимо)
- Push-уведомления при автоматических изменениях статуса
Сроки
3–5 рабочих дней в зависимости от числа стратегий и сложности FSM. Стоимость рассчитывается индивидуально после анализа требований.







