Десктопный аналитик криптовалют: как устроена мультифакторная система сигналов на TA-Lib

от автора

Когда я начал торговать криптой, меня раздражало одно: большинство аналитических инструментов выдают «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) от MACDHistogram = 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|, иначе 0TR  = 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 > PrevCloseOBV(t) = OBV(t-1) - Volume(t),  если Close < PrevCloseOBV(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) / 3CCI = (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) / 3Money Flow = TP × VolumePositive MF = сумма Money Flow за дни, когда TP > PrevTPNegative MF = сумма Money Flow за дни, когда TP < PrevTPMFI = 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 BUYScore ≥ +0.20  →  BUY−0.20 < Score < +0.20  →  NEUTRALScore ≤ −0.20  →  SELLScore ≤ −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] * 100rsi_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] * 100mh_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 * 100max_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._MEIPASSelse:    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.

ссылка на оригинал статьи https://habr.com/ru/articles/1047376/