惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
Arctic Wolf
N
News and Events Feed by Topic
E
Exploit-DB.com RSS Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
T
Tailwind CSS Blog
N
News | PayPal Newsroom
Google DeepMind News
Google DeepMind News
博客园 - Franky
T
The Blog of Author Tim Ferriss
Engineering at Meta
Engineering at Meta
W
WeLiveSecurity
N
Netflix TechBlog - Medium
GbyAI
GbyAI
Hacker News: Ask HN
Hacker News: Ask HN
N
News and Events Feed by Topic
O
OpenAI News
TaoSecurity Blog
TaoSecurity Blog
Cloudbric
Cloudbric
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
CERT Recently Published Vulnerability Notes
T
Threatpost
I
InfoQ
小众软件
小众软件
博客园 - 三生石上(FineUI控件)
WordPress大学
WordPress大学
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
云风的 BLOG
云风的 BLOG
AWS News Blog
AWS News Blog
The Hacker News
The Hacker News
Recorded Future
Recorded Future
Webroot Blog
Webroot Blog
A
About on SuperTechFans
S
Secure Thoughts
G
GRAHAM CLULEY
C
Check Point Blog
U
Unit 42
PCI Perspectives
PCI Perspectives
H
Help Net Security
C
Cisco Blogs
L
LINUX DO - 热门话题
Blog — PlanetScale
Blog — PlanetScale
M
MIT News - Artificial intelligence
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Hacker News - Newest:
Hacker News - Newest: "LLM"
aimingoo的专栏
aimingoo的专栏
B
Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Кто решает судьбу вашего проекта? Разбираем заинтересованные стороны. BABOK #1 Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Десктопный аналитик криптовалют: как устроена мультифакторная система сигналов на TA-Lib
Юрий · 2026-06-15 · via Все публикации подряд на Хабре

Средний

11 мин

0

Когда я начал торговать криптой, меня раздражало одно: большинство аналитических инструментов выдают «BUY» или «SELL» без объяснений. Три индикатора сказали покупать — вот тебе сигнал. Никаких весов, никакого контекста, никакой логики за цифрой.

Ссылка на статью о библиотеке Ta-Lib: TA-Lib Python: руководство для алготрейдера

Я решил сделать иначе. Программа, о которой пойдёт речь — это десктопное приложение для Windows, которое запускается двойным кликом, подключается к открытому API Binance и выдаёт взвешенный Score по шести категориям индикаторов, бэктест на последних 100 барах, уровни поддержки/сопротивления по фракталам и ATR-метрики риска. В статье расскажу, как всё это устроено изнутри — с формулами и кодом.

Десктопный аналитик криптовалют

Десктопный аналитик криптовалют

Архитектура: как данные превращаются в сигнал

Поток данных выглядит так:

Binance REST API
     ↓
get_binance_data() — 500 свечей OHLCV
     ↓
calculate_all_indicators() — 17+ индикаторов через TA-Lib
     ↓
detect_patterns() — 58 свечных паттернов
     ↓
generate_professional_signals() — взвешенный Score по 6 категориям
     ↓
Flask API → браузер → интерактивные графики + аналитическая панель

Данные приходят с публичного эндпоинта api.binance.com/api/v3/klines — он не требует авторизации. Программа запрашивает 300–500 свечей, конвертирует их в pandas DataFrame с типами float64 и передаёт дальше по цепочке.

Индикаторы: что считается и как

Все индикаторы рассчитываются через TA-Lib — C-библиотеку с Python-обвязкой. Скорость расчёта на 500 свечах — единицы миллисекунд.

Скользящие средние

indicators['SMA_7']   = talib.SMA(close, timeperiod=7)
indicators['SMA_25']  = talib.SMA(close, timeperiod=25)
indicators['SMA_50']  = talib.SMA(close, timeperiod=50)
indicators['SMA_100'] = talib.SMA(close, timeperiod=100)
indicators['SMA_200'] = talib.SMA(close, timeperiod=200)
indicators['EMA_12']  = talib.EMA(close, timeperiod=12)
indicators['EMA_26']  = talib.EMA(close, timeperiod=26)
Скользящие средние

Скользящие средние

SMA — простая скользящая средняя, среднее арифметическое цен закрытия за N баров:

SMA(n) = (C₁ + C₂ + ... + Cₙ) / n

EMA — экспоненциальная, придаёт больший вес свежим ценам:

EMA(t) = C(t) × k + EMA(t-1) × (1 - k),   где k = 2 / (n + 1)

Для EMA(12): k = 2/13 ≈ 0.154. Для EMA(26): k = 2/27 ≈ 0.074. Именно поэтому EMA(12) реагирует на движение цены быстрее.

MACD

MACD

MACD

macd, signal, hist = talib.MACD(close, fastperiod=12, slowperiod=26, signalperiod=9)

MACD — разность двух EMA:

MACD = EMA(12) - EMA(26)
Signal = EMA(9) от MACD
Histogram = MACD - Signal

Гистограмма — ключевой элемент. Когда она растёт в положительной зоне — импульс усиливается. Когда начинает падать, оставаясь положительной — импульс слабеет, хотя тренд ещё вверх. Это ранний сигнал разворота.

RSI

RSI

RSI

indicators['RSI'] = talib.RSI(close, timeperiod=14)

RSI Уайлдера измеряет скорость и размер ценовых изменений:

RS = Avg(Up closes, 14) / Avg(Down closes, 14)
RSI = 100 - 100 / (1 + RS)

Диапазон 0–100. Традиционные уровни 30/70. Программа использует расширенные: < 25 — «глубоко перепродан» (вес сигнала 1.0), < 30 — «перепродан» (0.7), < 40 — «близко к перепроданности» (0.2). Аналогично для верхней зоны.

Stochastic

Stochastic

Stochastic

stoch_k, stoch_d = talib.STOCH(
    high, low, close,
    fastk_period=14, slowk_period=3,
    slowk_matype=0, slowd_period=3, slowd_matype=0
)

Стохастик сравнивает цену закрытия с диапазоном цен за период:

%K = (Close - Lowest Low₁₄) / (Highest High₁₄ - Lowest Low₁₄) × 100
%D = SMA(3) от %K

Программа реагирует не только на уровни, но и на кроссовер %K/%D в зонах перекупленности/перепроданности — это более точный сигнал, чем просто факт вхождения в зону.

Bollinger Bands

Bollinger Bands

Bollinger Bands

bb_upper, bb_middle, bb_lower = talib.BBANDS(
    close, timeperiod=20, nbdevup=2, nbdevdn=2, matype=0
)

Полосы Боллинджера:

Middle = SMA(20)
Upper  = SMA(20) + 2 × σ(20)
Lower  = SMA(20) - 2 × σ(20)

где σ(20) — стандартное отклонение цен закрытия за 20 баров. При нормальном распределении ~95% цен находится внутри полос. Статистически цена за пределами полосы — аномалия, которая имеет тенденцию к возврату к средней.

Ключевой производный показатель — %B (Percent Bandwidth):

%B = (Close - Lower) / (Upper - Lower)

%B = 0 — цена на нижней полосе, %B = 1 — на верхней, < 0 или > 1 — за полосой.

Второй производный — BB Width, ширина полос:

BBW = (Upper - Lower) / Middle × 100

Когда BBW в нижних 10% своего исторического диапазона — BB Squeeze: волатильность сжата до минимума, рынок накапливает энергию для сильного движения.

ADX и направленное движение

indicators['ADX']      = talib.ADX(high, low, close, timeperiod=14)
indicators['PLUS_DI']  = talib.PLUS_DI(high, low, close, timeperiod=14)
indicators['MINUS_DI'] = talib.MINUS_DI(high, low, close, timeperiod=14)

Система Уайлдера для оценки силы и направления тренда:

+DM = max(High - PrevHigh, 0), если > |Low - PrevLow|, иначе 0
-DM = max(PrevLow - Low, 0),   если > |High - PrevHigh|, иначе 0

TR  = max(High - Low, |High - PrevClose|, |Low - PrevClose|)

+DI = 100 × EMA(14, +DM) / EMA(14, TR)
-DI = 100 × EMA(14, -DM) / EMA(14, TR)

DX  = 100 × |+DI - -DI| / (+DI + -DI)
ADX = EMA(14, DX)
Трендовые индикаторы

Трендовые индикаторы

ADX измеряет силу тренда, не направление. ADX > 25 — сильный тренд, > 40 — очень сильный. Направление даёт сравнение +DI и -DI: +DI > -DI означает восходящий тренд.

В программе ADX используется двояко: определяет направление сигнала через +DI/-DI, и влияет на Confidence (уверенность итогового сигнала).

ATR

indicators['ATR'] = talib.ATR(high, low, close, timeperiod=14)

True Range — наибольшее из трёх значений:

TR = max(High - Low, |High - PrevClose|, |Low - PrevClose|)
ATR = EMA(14, TR)
Волатильность

Волатильность

ATR — мера волатильности в абсолютных единицах цены. Используется в программе в трёх местах: как составляющая риск-метрик (стоп/тейк), для оценки текущего режима волатильности (сравнение с 20-баровым средним ATR) и как исходный параметр для определения размера позиции.

OBV

indicators['OBV'] = talib.OBV(close, volume)
OBV

OBV

On-Balance Volume — накопительный индикатор давления покупок/продаж:

OBV(t) = OBV(t-1) + Volume(t),  если Close > PrevClose
OBV(t) = OBV(t-1) - Volume(t),  если Close < PrevClose
OBV(t) = OBV(t-1),              если Close = PrevClose

Смысл в дивергенции: если OBV растёт, а цена падает — умные деньги покупают на снижении. Программа считает наклон OBV за последние 10 баров и сравнивает его с направлением цены.

CCI, Williams %R, MFI

indicators['CCI']   = talib.CCI(high, low, close, timeperiod=20)
indicators['WILLR'] = talib.WILLR(high, low, close, timeperiod=14)
indicators['MFI']   = talib.MFI(high, low, close, volume, timeperiod=14)

CCI измеряет отклонение типичной цены от её скользящей средней:

TP  = (High + Low + Close) / 3
CCI = (TP - SMA(TP, 20)) / (0.015 × MeanDeviation)

Зоны ±100 — традиционные уровни, ±150 — экстремальные.

Williams %R — перевёрнутый стохастик:

%R = (Highest High₁₄ - Close) / (Highest High₁₄ - Lowest Low₁₄) × (-100)

Диапазон от 0 до -100. < -80 — перепроданность, > -20 — перекупленность.

MFI (Money Flow Index) — RSI, взвешенный по объёму:

TP = (High + Low + Close) / 3
Money Flow = TP × Volume

Positive MF = сумма Money Flow за дни, когда TP > PrevTP
Negative MF = сумма Money Flow за дни, когда TP < PrevTP

MFI = 100 - 100 / (1 + Positive MF / Negative MF)
Осцилляторы

Осцилляторы

Отличие от RSI: учитывает объём. MFI < 20 означает, что деньги уходят с рынка при снижении объёма — давление продавцов ослабевает.

Parabolic SAR

indicators['SAR'] = talib.SAR(high, low, acceleration=0.02, maximum=0.2)

Параболический SAR — следящий стоп, который автоматически поднимается за ценой при восходящем тренде:

SAR(t) = SAR(t-1) + AF × (EP - SAR(t-1))

где AF (Acceleration Factor) начинается с 0.02 и увеличивается на 0.02 при каждом новом экстремуме, но не более 0.20. EP — крайняя точка (Extreme Point): максимум при восходящем тренде, минимум при нисходящем.

Когда цена пересекает SAR — тренд считается сменившимся.

58 свечных паттернов

Каждый паттерн проверяется функцией TA-Lib вида talib.CDL*(open, high, low, close). Возвращаемое значение: +100 (бычий), -100 (медвежий), 0 (паттерн не обнаружен). Некоторые паттерны возвращают промежуточные значения от -200 до +200 для обозначения силы.

Свечные паттерны

Свечные паттерны

Программа проверяет последние 5 свечей — это нужно для многосвечных паттернов (утренняя звезда, три белых солдата), которые формируются на нескольких барах.

Примеры логики обнаружения (внутри TA-Lib):

Молот (Hammer): маленькое тело в верхней части диапазона, длинная нижняя тень не менее 2× тела, маленькая или отсутствующая верхняя тень. Появляется после нисходящего тренда.

Поглощение (Engulfing): вторая свеча полностью поглощает тело первой. При бычьем поглощении: первая красная, вторая зелёная и больше по телу.

Три белых солдата: три последовательных длинных зелёных свечи, каждая открывается выше предыдущей и закрывается в верхней части своего диапазона.

В системе сигналов каждый паттерн имеет свой вес от 0.3 до 1.0. Вес определяет вклад паттерна в категорию «Паттерны» (8% итогового Score).

Мультифакторная система сигналов

Детальные сигналы

Детальные сигналы

Это центральная часть программы. Логика реализована в signal_engine.py (775 строк).

Структура весов

CATEGORY_WEIGHTS = {
    'trend':      0.28,
    'momentum':   0.22,
    'macd':       0.18,
    'volatility': 0.14,
    'volume':     0.10,
    'patterns':   0.08,
}

Сумма весов = 1.0. Каждая категория возвращает cat_score в диапазоне [-1, +1].

Итоговый Score

total_score = sum(cat_scores.get(cat, 0) * w for cat, w in CATEGORY_WEIGHTS.items())

Это взвешенная сумма: Score = 0.28×trend + 0.22×momentum + 0.18×macd + 0.14×volatility + 0.10×volume + 0.08×patterns

Результат в диапазоне [-1, +1]:

Score ≥ +0.45  →  STRONG BUY
Score ≥ +0.20  →  BUY
−0.20 < Score < +0.20  →  NEUTRAL
Score ≤ −0.20  →  SELL
Score ≤ −0.45  →  STRONG SELL

Как считается cat_score для каждой категории

Каждый сигнал внутри категории имеет свой вес. Например, категория «Тренд»:

Событие

Вес сигнала

Golden Cross SMA 50/200

+1.0

Death Cross SMA 50/200

−1.0

Golden Cross EMA 12/26

+0.8

Death Cross EMA 12/26

−0.8

SMA 50 > SMA 200 (устойчиво)

+0.4

SAR разворот вверх

+0.6

SAR разворот вниз

−0.6

Цена выше SAR

+0.2

ADX направление (+DI > -DI)

+0.4

Цена >5% выше SMA 50

−0.3 (перекуп)

cat_score нормируется делением на 3 (максимально возможная сумма весов в категории) и обрезается до [-1, +1]:

cat_scores['trend'] = max(-1.0, min(1.0, trend_score / 3))

Дивергенции

Дивергенции — один из самых сильных сигналов разворота. Реализованы в упрощённой форме через сравнение изменений за фиксированный период:

# RSI дивергенция
price_chg = (close[-1] - close[-5]) / close[-5] * 100
rsi_chg   = rsi_val - _safe(rsi, -5)

if price_chg < -2 and rsi_chg > 2:
    # Бычья: цена падает, RSI растёт → вес 0.8
  
# MACD гистограмма дивергенция
price_chg = (close[-1] - close[-10]) / close[-10] * 100
mh_chg    = float(macd_hist[-1]) - float(macd_hist[-10])

if price_chg < -3 and mh_chg > 0:
    # Бычья: цена падает, гистограмма растёт → вес 0.9

Это упрощение: классическое определение дивергенции требует поиска локальных экстремумов. Но сравнение за фиксированный период работает как быстрый и достаточно надёжный фильтр.

Уверенность (Confidence)

adx_conf     = min(1.0, adx_val / 50)
signal_count = количество сработавших сигналов

confidence = min(1.0, adx_conf * 0.5 + min(signal_count, 10) / 10 * 0.5)

Confidence = среднее двух составляющих:

  • ADX-компонента: чем сильнее тренд, тем выше уверенность (ADX 50+ → 100%)

  • Насыщенность: чем больше сигналов сработало, тем выше уверенность (10+ сигналов → 100%)

Уровни поддержки и сопротивления: алгоритм фракталов

def _find_support_resistance(high, low, close, n_levels=4, window=5):
    for i in range(window, len(highs) - window):
        if highs[i] == max(highs[i-window:i+window+1]):
            fractal_highs.append(float(highs[i]))
        if lows[i] == min(lows[i-window:i+window+1]):
            fractal_lows.append(float(lows[i]))

Шаг 1: Фракталы. Точка является фрактальным максимумом, если она выше всех соседних точек в окне ±5 баров. Аналогично для минимумов. Это классическое определение Билла Вильямса.

Шаг 2: Кластеризация. Близкие уровни сливаются в один:

def cluster(levels, radius_pct=0.5):
    levels = sorted(levels)
    clusters = []
    group = [levels[0]]
    for v in levels[1:]:
        if abs(v - group[-1]) / group[-1] * 100 <= radius_pct:
            group.append(v)  # уровни в радиусе 0.5% — один кластер
        else:
            clusters.append(np.mean(group))  # среднее кластера
            group = [v]

Порог 0.5%: если два уровня отстоят менее чем на 0.5% — они сливаются в один (среднее значение). Это убирает «кашу» из близких уровней и даёт чистые значимые зоны.

Шаг 3: Фильтрация. Из кластеров выбираются 4 ближайших поддержки (ниже текущей цены, с отступом 0.2%) и 4 ближайших сопротивления (выше цены).

Бэктест: как проверяется историческая точность

def _backtest_rule(signal_series, close, forward=3, lookback=100):
    for i in range(start, end):
        s = sig[i]
        if s == 0:
            continue
        entry = close[i]
        exit_ = close[i + forward]  # закрытие через 3 бара
        ret   = (exit_ - entry) / entry * 100 * s
        returns.append(ret)
        if ret > 0:
            wins += 1

Для каждого момента в прошлом, где сработал сигнал, программа смотрит, что было через 3 бара. Если сигнал был на покупку (s = +1) и цена выросла — сделка прибыльная.

Результат: количество сделок, win rate (%), средняя доходность сделки (%).

Важное ограничение: бэктест на тех же данных что используются для анализа — это in-sample тест. Он показывает, как правило работало на недавней истории. Не является доказательством будущей эффективности.

Риск-метрики

atr_stop_long = round(price - 2 * atrv, 6)   # стоп-лосс
atr_tp_long   = round(price + 3 * atrv, 6)   # тейк-профит
rr = 3.0                                       # R/R = 1:3
Риск-метрики

Риск-метрики

ATR-стоп — один из наиболее распространённых методов постановки стопа. Логика: стоп ставится на расстоянии 2×ATR от цены входа. ATR отражает среднюю волатильность — нормальные колебания цены не должны выбивать стоп.

Волатильность за 20 баров:

returns20 = np.diff(np.log(close[-21:]))
vol20     = float(np.std(returns20)) * 100

Стандартное отклонение лог-доходностей × 100. Это «однобаровая волатильность» в процентах. Для дневных данных умножение на √252 даёт годовую волатильность, но в программе используется именно однобаровая — она нагляднее для оценки риска текущей позиции.

Max Drawdown за 50 баров:

peak = np.maximum.accumulate(close[-50:])
dd   = (close[-50:] - peak) / peak * 100
max_drawdown = float(np.min(dd))

Максимальное отклонение от исторического пика на последних 50 барах. Характеризует риск удержания позиции.

Технические решения, которые стоят упоминания

Почему Flask, а не обычное окно приложения?

Интерактивные финансовые графики с зумом, панорамой и переключением индикаторов — это задача для браузерных библиотек (Chart.js). Написать то же самое на tkinter или PyQt потребовало бы в несколько раз больше кода и дало худший результат визуально. Flask запускает локальный сервер на порту 5000, браузер открывается автоматически.

Зум и панорама на canvas-графиках

Полноэкранные графики (свечной, BB, RSI, MACD, Stochastic, объём) рисуются вручную на <canvas> без Chart.js. Для них реализован собственный зум и панорама через modalViewport:

function handleModalWheel(e) {
    const zoomFactor = e.deltaY > 0 ? 1.15 : (1 / 1.15);
    let newRange = Math.round(range * zoomFactor);
    const ratio = (e.clientX - rect.left) / rect.width;
    const centerIdx = view.start + range * ratio;
    // масштаб с центром в позиции курсора
    let newStart = Math.round(centerIdx - newRange * ratio);
}

Колесо мыши масштабирует диапазон относительно позиции курсора. Перетаскивание сдвигает видимую область. Кнопка «Сброс» — modalViewport = null.

Сборка в .exe

Программа поставляется как standalone-бинарник, собранный через PyInstaller (--onefile --collect-all talib). При запуске Python-интерпретатор и все зависимости распаковываются в системную временную папку. Для корректной работы Flask с templates внутри .exe используется sys._MEIPASS:

if getattr(sys, 'frozen', False):
    BASE_DIR = sys._MEIPASS
else:
    BASE_DIR = os.path.dirname(os.path.abspath(__file__))

app = Flask(__name__, template_folder=os.path.join(BASE_DIR, 'templates'))

Что не реализовано и почему

Нет реального времени. WebSocket-подписка на тикер Binance технически несложна, но усложняет архитектуру: нужен фоновый поток, синхронизация с основным, инкрементальное обновление графиков. Текущая версия — запрос по кнопке.

Нет скринера. Анализировать список из 50 монет последовательно займёт ~2 минуты (4 секунды на тикер: запрос к API + расчёт). Параллельные запросы упираются в rate limit Binance. Реализуемо, но требует очереди с задержками.

Нет кэша. Каждый запрос «Анализировать» — новый HTTP-запрос к Binance. Для нормального использования это не проблема. При частом нажатии Binance начинает отвечать 429 (Too Many Requests).

Дивергенции упрощены. Классический алгоритм поиска дивергенций требует нахождения локальных экстремумов через пиковый детектор (например, scipy.signal.argrelmax). В текущей версии сравниваются значения с фиксированным лагом (5 и 10 баров). Это даёт ложные срабатывания в боковом рынке.

AROON не влияет на Score. Рассчитывается, отображается в таблице индикаторов, но в signal_engine.py не используется. Причина прагматичная: AROON хорошо работает для идентификации начала тренда, но его интерпретация сильно зависит от таймфрейма.

Итого

Программа состоит из двух смысловых частей: стандартный конвейер расчёта индикаторов через TA-Lib и нестандартный движок сигналов с категорийными весами. Второе — основная идея. Вместо «три индикатора сказали покупать» — взвешенный Score от -1 до +1, который учитывает относительную важность каждой группы сигналов.

Всё это работает локально, данные никуда не уходят, интернет нужен только для запроса свечей с Binance.

Код написан на Python, собран в .exe для Windows.