Чому бектестинг на Python упирається у швидкість
На тестовому відрізку в тиждень торгів прогін вкладається в хвилини, на кількох роках історії — вже в години.
Бектестинг на Python упирається при цьому не в обсяг даних як такий: кожне перегрупування, ковзне вікно та зріз створюють копії фрейму, а поелементний цикл по барах додає накладні витрати на кожен виклик.
Вузьке місце розрахунку майже завжди виявляється не в математиці стратегії, а в перекладанні даних між структурами.
Друга межа — пам'ять. Уся історія завантажена одним фреймом, до неї приєднуються сигнали та результати угод, на кожен бар пишуться метрики — споживання зростає до межі машини, і прогін падає посеред тесту. Допомагає не додавання пам'яті, а потокова обробка вікнами та явні числові типи замість об'єктів.
Оптимізувати наосліп безглуздо: замір продуктивності показує, яка ділянка з'їдає основний час, і тільки після нього правки дають кратний ефект, а не переписування заради переписування.
Відтворюваність результатів — проблема менш очевидна. Порядок операцій із плаваючою точкою, нестабільне сортування, незафіксовані генератори випадкових чисел і різний порядок обходу призводять до того, що повторний запуск на тих самих даних видає інші цифри. Порівняння двох версій стратегії втрачає сенс: незрозуміло, це покращення чи шум округлень.
Повільний прогін і розбіжність цифр — не властивості мови, а наслідок устрою конвеєра розрахунку: де живуть дані, хто володіє копіями, що і в якому порядку рахується. Нижче розбираємо, як закривається кожне з цих місць — від замірів до фіксації середовища прогону.
Що дає переробка рушія бектестингу на Python
Швидкість прогону на історичних даних
Переробка рушія починається з профілювання: знаходимо, де розрахунок упирається в інтерпретатор, а де — в зайве копіювання даних. Векторизуємо обчислення індикаторів, прибираємо повторні проходи по котируваннях і переводимо важкі ділянки в компільований код Python.
Швидкість прогону на багаторічній історії зростає в рази: те, що вважалося десятками хвилин, вкладається в хвилини, і вся сітка параметрів перевіряється за один запуск замість ночі очікування.
Стійкість розрахунку
Другий ефект — передбачуваність результату. Пікове навантаження на пам'ять перестає залежати від довжини історії: дані читаються порціями, проміжні ряди звільняються одразу. Зникає цілий клас відмов — падіння через пам'ять і «тихі» розбіжності між однаковими прогонами.
На однакових вхідних даних виходить та сама крива, тому розбіжність у звітах стає сигналом про логіку стратегії, а не про рушій.
Звільнений час команда витрачає на гіпотези, а не на очікування та розбір падінь. Звіт за кожним прогоном відтворюється, відкат до попередньої конфігурації займає один запуск — перевірка ідей іде безперервно, без ручних обходів і перезбирання середовища.
Варіанти організації розрахунків: порівняння режимів
Режим розрахунку визначає не лише швидкість, а й те, які перевірки взагалі відтворювані. Подієвий прогін веде історію в тому самому порядку, у якому працює торгова логіка, векторний рахує масив цілком за один прохід, розподілений розносить незалежні сценарії по вузлах.
Вибір режиму диктують кількість сценаріїв і вимоги до точності моделі виконання — нижче альтернативи й те, чим за кожну платять.
| Варіант | Коли підходить | Обмеження |
|---|---|---|
| Подієвий прогін | Логіка залежить від потоку заявок, часткових виконань і спрацювання стопів усередині бару | Один повний прохід по історії на кожен сценарій, дорого при переборі параметрів |
| Векторний розрахунок | Масова перевірка сигналів та індикаторів на широкій історії | Не відтворює чергу заявок, прослизання та пріоритет ордерів |
| Розподілені обчислення на кількох вузлах | Сотні незалежних наборів параметрів і прогонів по різних інструментах | Потрібні спільний кеш ринкових даних, серіалізація стану й контроль, що результат на кластері збігається з одиночним запуском |
| Інкрементальний перерахунок | Змінилася остання ділянка даних або один блок правил | Потребує карти залежностей: частину результатів доведеться рахувати заново цілком |
Поєднання режимів в одному рушії
Режими рідко обирають раз і назавжди. Зазвичай векторний прохід відсіює заздалегідь слабкі комбінації на всій історії, подієвий перевіряє відібрані з точною моделлю виконання, а інкрементальний перерахунок тримає результат актуальним при надходженні нових даних.
Розподілення підключається шаром вище — на рівні постановки задач, без правок логіки стратегії: той самий набір правил дає ідентичний звіт і на локальній машині, і на кластері.
Етапи переробки: від аудиту розрахунку до передачі
Переробка розрахункового ядра йде ітераціями: спочатку вимірюємо, потім змінюємо, потім звіряємо на контрольних прогонах. Після кожного етапу залишається артефакт, за яким видно, чи можна рухатися далі.
- Аудит рушія та замір вузьких місць. Профілюємо розрахунок на вашій історії: де час іде в очікування, де дані рахуються повторно, де підсумок залежить від порядку обходу.
- Схема розрахунку. Фіксуємо потік даних: джерела котирувань, правила очищення, момент рішення, облік комісій і прослизання. Схема стає контрактом для приймання.
- Переробка ядра. Прибираємо повторні обчислення та зайві копії, розділяємо підготовку даних і прогін, важкі ділянки переводимо на векторизацію. Python залишається мовою опису стратегій — змінюється внутрішність рушія.
- Перевірка на історії. Прогоняємо новий рушій на тих самих даних, що й старий, і звіряємо метрики угода за угодою. Розбіжності розбираємо до причини, а не до «приблизно схоже».
- Приймання результатів. Ви запускаєте свої сценарії, ми показуємо збіг і розбір розбіжностей за схемою розрахунку, а не за відчуттями.
- Передача й навчання. Віддаємо код, конфігурації та регламент прогону, розбираємо з вашою командою, як додавати стратегії та дані без нас.
Паралельна робота з вашою інфраструктурою
Поки йде переробка, частину кроків закриваємо на вашій стороні: підключаємося до вашого сховища даних і запускаємо прогони у вашому середовищі. Так результат видно на реальних даних до фінальної передачі, а не після неї.
Комплект постачання: розрахункове ядро, схеми, регламенти
Комплект передається не архівом «як вийшло», а набором артефактів, за якими ваша команда запускає рушій, відтворює розрахунок і доопрацьовує його без нас. Нижче — що саме передається замовнику.
- Вихідний код рушія — модулі розрахунку позицій і метрик, тести, приклади прогонів на історичних даних.
- Схеми розрахунку — граф викликів, порядок обробки свічок і тіків, точки обліку комісій, прослизання та затримок виконання.
- Схеми потоків даних — шлях від джерела котирувань до вітрини результатів: формати файлів, розбиття на партії, куди пишуться метрики.
- Конфігурації середовищ — параметри запуску, набори даних, профілі для одиночного та розподіленого прогону.
- Політики доступу та ролі — хто запускає розрахунок, хто змінює параметри стратегій, хто читає результати; секрети зберігаються поза репозиторієм.
- Регламент запуску та відновлення — старт прогону, продовження з контрольної точки, відкат до попередньої версії ядра.
- Документація та інструкції для команди — опис інтерфейсів, довідник параметрів, порядок додавання нової стратегії.
Як проходить приймання
Приймання йде за контрольними прогонами, а не за списком файлів. На вашій стороні запускаємо обумовлений набір стратегій, звіряємо метрики з еталонним результатом і перевіряємо відновлення після примусової зупинки.
Окремо дивимося, що новий інженер за документацією піднімає середовище й проводить розрахунок без підказок.
Кейс: як скоротили час прогону історичних даних
Клієнт прийшов із рушієм бектестингу на Python, який працював, поки історія вкладалася в один торговий рік.
Повний прогін по п'ятирічному вікну займав близько чотирнадцяти годин і часто не доживав до кінця: розрахунок падав на довгих рядах, а два запуски однієї стратегії давали різний результат на третьому знаку. Порівнювати гіпотези доводилося вручну, а рішенням за такими цифрами ніхто не довіряв.
Заміри за ділянками коду показали: більшу частину часу з'їдали рядкові цикли по барах і перерахунок індикаторів на кожному кроці, а нестабільність давали плаваючі порядки додавання та стан, що протікає між запусками.
Що саме переробили
Логіку розрахунку перевели на векторні операції над масивами: індикатори рахуються один раз по всій історії, а не заново на кожному барі. Важкі ділянки винесли в окремі процеси, проміжні результати зберігаються, тому повторний прогін не рахує готове заново.
Зафіксували порядок агрегації та ізолювали стан: кожен запуск стартує з чистого середовища, і результат відтворюється біт у біт.
Після переробки повний прогін по тому самому вікну вкладається в сорок хвилин, розбіг між запусками — нуль, а на довгій історії за квартал спостережень не було жодного падіння з кількох сотень запусків.
Обмеження проговорюємо одразу: на тіковій історії за кілька років розрахунок усе ще йде годинами, такі задачі ставимо в нічне вікно, а кількість паралельних прогонів обмежена пам'яттю — перебір параметрів іде чергами, а не всім полем одразу.
Чим відрізняється векторний розрахунок від подієвого?
Векторний розрахунок застосовує правило одразу до всього ряду цін: індикатор і сигнал рахуються масивами, без циклу за часом. Історію за кілька років такий прогін проходить за секунди, тому на ньому перебирають десятки тисяч комбінацій параметрів і відсіюють заздалегідь слабкі. Обмеження одне, але жорстке: логіка має бути безумовною, без опори на стан позиції.
Подієвий прогін іде свічка за свічкою і на кожній відновлює стан: відкриті позиції, залишок коштів, виставлені заявки. Тільки так рахуються стопи й тейки всередині свічки, часткове виконання, перенесення позиції через ніч, комісія та прослизання, що залежить від обсягу.
Векторно це не виражається: підсумок залежить від того, що було раніше, і порядок подій змінює результат.
Різниця в точності розрахунку проявляється там, де рішення залежить від поточного рахунку. Вектор покаже вхід і вихід за ціною закриття, а подієвий прогін — що частина заявок не виконалася, а стоп спрацював за гіршою ціною. Перша відповідь годиться, щоб відібрати ідеї; друга — щоб вирішити, чи можна довіряти стратегії.
Звідси поєднання підходів в одному рушії: векторний розрахунок як фільтр по широкій сітці параметрів, подієвий прогін — для фіналістів і перевірки на стійкість до витрат.
Обидва режими мають читати одні дані й застосовувати одну логіку, інакше результати розійдуться і буде незрозуміло, якому вірити.
Практична перевірка — прогнати одну стратегію двома способами: розбіжність у межах витрат підтверджує коректність моделі виконання, помітна розбіжність указує на помилку в ній.
Часті технічні питання щодо розрахункового ядра та даних
Перед першим прогоном спливають ті самі сумніви: чи переписувати все заново, чи збігатимуться розрахунки з ринком, чи повторюватимуться результати. Нижче — п'ять питань із технічної зустрічі та те, як ми їх закриваємо.
Чи доведеться переписувати наявний код?
Ні. Ядро розрахунку відокремлюємо від завантаження даних і опису стратегій стабільним інтерфейсом: поточні правила переносимо, замінюємо лише вузькі місця — перерахунок індикаторів і обхід історії. Самі стратегії залишаються у звичному вигляді.
Наскільки розрахунок збіжиться з реальними умовами?
У модель витрат закладаємо комісію, прослизання за книгою заявок, затримку виконання та часткове заповнення заявок. Без цього бектестинг на Python показує дохідність, якої на ринку не було.
Як забезпечити відтворюваність прогонів?
Прогін фіксується цілком: версія набору даних, параметри стратегії, початковий стан генератора випадкових чисел, налаштування витрат. Повтор через місяць дає той самий результат — це база для порівняння гіпотез.
Що робити з неповною історією?
Пропуски та розсинхрон таймфреймів ловимо до розрахунку: звірка з другим джерелом, відновлення барів, явна позначка періодів із низьким покриттям. Спірні ділянки позначаємо, а не викидаємо мовчки.
Чи витримає ядро зростання кількості інструментів?
Розрахунок розбивається на незалежні задачі й іде паралельно по ядрах і машинах, спільні індикатори кешуються. Додавання сотні інструментів збільшує час прогону лінійно, а не обвалює його.
Що потрібно для старту: обговоримо задачу і повернемося з планом
Точна оцінка рушія бектестингу тримається не на здогадках, а на вхідних даних.
Поки не видно, які сценарії прогоняються і на якому обсязі історії, будь-які терміни будуть приблизними — тому старт завжди один: коротке обговорення задачі, після якого ми повертаємося з планом впровадження та розбивкою за етапами.
Щоб порахувати витрати праці, надішліть:
- опис сценаріїв прогону — які стратегії перевіряються, як часто змінюються параметри, чи потрібен перебір комбінацій;
- обсяг і формат історії — кількість інструментів, глибина рядів, джерело даних та їхня якість: пропуски, розриви, корпоративні події;
- поточну схему розрахунку — як рахуєте зараз: самописні скрипти, готову бібліотеку чи таблиці, і що саме перестало влаштовувати;
- вимоги до часу повного прогону — яка тривалість проходу вважається робочою і на якій конфігурації ви її чекаєте;
- обмеження за інфраструктурою — де виконуватиметься розрахунок: локальні машини, власні сервери чи хмара, чи є вимоги до ізоляції даних;
- формат результатів — звіти, метрики, вивантаження, чи потрібна візуалізація та інтерфейс для суміжних систем.
За цими вхідними даними ми збираємо оцінку робіт, пропонуємо архітектуру розрахунку й називаємо терміни. План впровадження розбитий на етапи так, щоб результат кожного можна було перевірити на ваших даних, а не на демонстраційному прикладі.







