preview
Определение справедливых курсов валют по ППС с помощью данных МВФ

Определение справедливых курсов валют по ППС с помощью данных МВФ

MetaTrader 5Интеграция |
400 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

В какой-то момент я понял, что трачу больше времени на поиск "идеального" прогнозирующего модуля, чем на понимание того, что реально движет валютными курсами. И тогда задался простым вопросом: а что, если забыть про все эти графики и попытаться найти настоящую стоимость валют? Не ту, которую показывает рынок в конкретный момент под влиянием эмоций и спекуляций, а ту, которая следует из фундаментальных экономических законов?

Этот вопрос затянул меня в работу глубиной в несколько месяцев. Я начал с изучения теории паритета покупательной способности, а закончил написанием полноценной системы анализа валютных курсов на Python. И это оказалось намного интереснее любого технического индикатора.


Постановка проблемы

Анализ валютных рынков часто сосредотачивается на поиске технических индикаторов и графических паттернов, игнорируя фундаментальные экономические факторы. Возникает закономерный вопрос: возможно ли определить справедливую стоимость валют, основываясь на экономических законах, а не на краткосрочных рыночных настроениях и спекулятивных движениях?

Данное исследование представляет практический подход к решению этой задачи через создание комплексной системы расчета справедливых валютных курсов на основе теории паритета покупательной способности, реализованной на языке программирования Python.

Теоретические основы паритета покупательной способности
Классический пример с McDonald's Big Mac демонстрирует суть теории: если гамбургер в США стоит 5 долларов, а в Европе 9 евро, справедливый курс должен составлять 1,8 доллара за евро. За этой простой иллюстрацией скрывается фундаментальный инструмент анализа валютных рынков, позволяющий выявлять долгосрочные дисбалансы за пределами краткосрочной рыночной волатильности.

Исторические корни концепции прослеживаются до XVI века, когда испанские торговцы наблюдали ценовые диспропорции между метрополией и колониями. Если один песо в Севилье покупал буханку хлеба, а в Мексике — три буханки, это указывало на фундаментальное несоответствие относительной стоимости валют, требующее коррекции через изменение обменного курса или выравнивание цен.

Формулировки теории ППС
Абсолютный паритет покупательной способности утверждает прямую связь между обменными курсами и соотношением цен идентичных товаров в разных странах. Теоретически элегантный, этот подход на практике сталкивается с множественными искажающими факторами: налоговыми различиями, регулятивными барьерами, транспортными расходами и торговыми ограничениями.

S₁₂ = P₁ / P₂

где S₁₂ — обменный курс валюты 1 к валюте 2, P₁ и P₂ — уровни цен в соответствующих странах.

Относительный паритет покупательной способности фокусируется на динамике ценовых изменений, а не на абсолютных уровнях. Согласно этой формулировке, если инфляция в стране А составляет 10% годовых, а в стране Б — 5%, валюта страны А должна ослабеть на 5% относительно валюты страны Б для сохранения паритета покупательной способности.

S₁₂(t) / S₁₂(0) = [P₁(t) / P₁(0)] / [P₂(t) / P₂(0)]

Или в упрощенном виде:

ΔS₁₂ = π₁ - π₂

где π₁, π₂ — темпы инфляции в странах 1 и 2.

Относительный подход был выбран как методологическая основа алгоритма, благодаря большей практической применимости, лучшему отражению экономической реальности и возможности получения конкретных количественных ориентиров для валютных курсов.


Оценка доступных источников данных

Исследование существующих поставщиков ППС-данных выявило значительные ограничения для практического применения. OECD публикует официальные коэффициенты паритета покупательной способности с задержкой от шести месяцев до года — данные за 2023 год стали доступны только в середине 2024 года, что неприемлемо для динамичных валютных рынков.

Источник Частота обновления
Задержка
Стоимость/год
Прозрачность методологии
Практическая применимость
OECD
Годовая
6-12 мес
$0
Низкая
Исследования
Penn World Table
2-3 года
1-2 года
$0
Средняя
Академическая
Bloomberg Terminal
Real-time
Нет
$24,000
Отсутствует
Профессиональная
Refinitiv Eikon
Real-time
Нет
$22,000
Отсутствует
Профессиональная

Penn World Table обновляется с интервалом в несколько лет, при этом последняя доступная версия содержала информацию только до 2019 года. Для академических исследований такая база представляет ценность, однако для практической торговли подобные временные лаги критичны.

Коммерческие поставщики данных Bloomberg и Refinitiv предлагают более актуальную информацию, но стоимость доступа составляет от $2000 в месяц для Bloomberg Terminal без гарантий методологической прозрачности или качества данных.

Проблема методологической непрозрачности: критическим недостатком существующих решений является отсутствие ясности в вычислительных процедурах. Методология расчета коэффициентов OECD, состав потребительских корзин, алгоритмы обработки статистических выбросов и корректировки качественных различий товаров остаются недокументированными.

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

Разработка собственной системы: принято решение о создании независимой системы расчета ППС с использованием публично доступных данных международных организаций — Международного валютного фонда, Всемирного банка и национальных статистических служб. Система должна обеспечивать полную прозрачность алгоритмов и возможность адаптации методологии под специфические аналитические задачи.

Несмотря на значительную сложность разработки, по сравнению с приобретением готового решения, и необходимость изучения множественных источников данных, форматов API и статистических методов, такой подход гарантирует полный контроль над процессом расчета и глубокое понимание функционирования системы.


Архитектурное решение: система множественных методов

Фундаментальная проблема любого ППС-расчета заключается в существовании множественных подходов к оценке справедливого обменного курса, каждый из которых обладает специфическими преимуществами и ограничениями. Выбор единственного метода неизбежно приводит к субъективности результатов.

Источники данных Методы расчета ППС Результаты и сигналы
МВФ API Price Level
Справедливые курсы
Всемирный банк GDP-Implied
Отклонения от рынка
Национальная статистика Inflation-Adjusted
Торговые сигналы
Fallback данные Big Mac Proxy
Индексы уверенности
Рыночные курсы валютных пар Composite Method
Классификация валют

Разработанная система использует параллельно несколько независимых методов расчета с последующим объединением результатов. Согласованность оценок различных методов повышает уверенность в результате, тогда как значительные расхождения указывают на высокую степень неопределенности, что само по себе представляет ценную информацию о состоянии рынка.

Первый подход основан на международных сопоставлениях уровня цен.Логика простая: если житель Швейцарии тратит на жизнь в полтора раза больше американца при том же уровне жизни, то швейцарский франк переоценен на 50%.

Данные для этого метода я взял из международных программ сопоставления цен, которые проводятся под эгидой Всемирного банка и OECD. Эти программы сравнивают стоимость стандартных потребительских корзин в разных странах и вычисляют относительные уровни цен.

def _calculate_price_level_ppp(self) -> Dict:
    ppp_rates = {}
    for country, price_level in self.price_levels_2024.items():
        if country != 'US':
            ppp_factor = price_level / 100.0
            ppp_rates[country] = {
                'ppp_conversion_factor': ppp_factor,
                'price_level_index': price_level,
                'method': 'price_level_adjustment'
            }
    return ppp_rates

Преимущество метода — он отражает реальные различия в стоимости жизни. Недостаток — данные обновляются редко, раз в несколько лет.

Второй подход я придумал сам, и до сих пор им горжусь. Идея пришла во время изучения данных МВФ: а что, если сравнить ВВП страны в национальной валюте (из официальной статистики) с оценкой того же ВВП в долларах (из международных баз данных)?

Логика такая: если Банк России сообщает, что ВВП России составляет 150 триллионов рублей, а Всемирный банк оценивает российский ВВП в 2 триллиона долларов, то имплицитный обменный курс — 75 рублей за доллар. Это тот курс, при котором экономики оценены "справедливо" относительно друг друга.

def _calculate_gdp_implied_ppp(self, economic_data: pd.DataFrame):
    for country, gdp_usd_2023 in self.gdp_usd_estimates_2023.items():
        country_gdp_lcu = gdp_lcu_data[gdp_lcu_data['REF_AREA'] == country]
        if not country_gdp_lcu.empty:
            latest_data = country_gdp_lcu.sort_values('year').iloc[-1]
            gdp_lcu = latest_data['value']
            
            if gdp_lcu > 0:
                implied_rate = gdp_lcu / gdp_usd_2023

Этот метод оказался удивительно точным и актуальным. ВВП-данные обновляются ежеквартально, так что оценки получаются свежими. А поскольку ВВП — это агрегированный показатель всей экономической активности, он хорошо отражает фундаментальную стоимость валюты.

Третий метод реализует классическую формулу относительного ППС из учебников. Берем исторический обменный курс из какой-то базовой точки и корректируем его на накопленную разницу в инфляции между странами.

Я выбрал 2020 год как базовую точку — достаточно недавно, чтобы быть актуальным, но до пандемии, чтобы избежать искажений от экстремальной денежной политики.

def _calculate_inflation_adjusted_ppp(self, economic_data: pd.DataFrame):
    # Получаем базовый курс 2020 года
    base_rate_2020 = GetBaseRate2020(base_currency, quote_currency)
    
    # Рассчитываем дифференциал инфляции
    inflation_differential = (base_data.inflation_rate - quote_data.inflation_rate) / 100.0
    
    # Корректируем базовый курс
    adjusted_rate = base_rate_2020 * (1 + inflation_differential)

Метод теоретически безупречен и легко объясним. Если в стране А инфляция была выше, чем в стране Б, то валюта А должна ослабнуть пропорционально этой разнице.

Единственная проблема — качество данных по инфляции. Разные страны считают инфляцию по-разному, и некоторые склонны "приукрашивать" статистику. Но в целом, метод работает хорошо.

Четвертый метод вдохновлен знаменитым Big Mac Index от The Economist. Идея в том, что Big Mac — это стандартизированный товар, который производится по одинаковой технологии во всем мире. Его цена должна отражать реальные различия в стоимости ресурсов между странами.

Проблема в том, что собирать актуальные цены Big Mac — это отдельная исследовательская задача. Вместо этого, я использую известные данные об относительных уровнях цен для моделирования того, сколько должен стоить стандартизированный товар в каждой стране.

def _calculate_big_mac_proxy_ppp(self):
    us_big_mac_price = 5.50
    for country, price_level in self.price_levels_2024.items():
        if country != 'US':
            local_big_mac_price = us_big_mac_price * (price_level / 100.0)
            ppp_rate = local_big_mac_price / us_big_mac_price

Метод не самый точный, но он дает хорошую интуитивную проверку результатов других методов. Если все остальные способы показывают, что валюта переоценена на 20%, а "Big Mac метод" дает похожий результат, это повышает уверенность в оценке.

Пятый метод объединяет результаты всех предыдущих через взвешенное усреднение. Я потратил много времени на калибровку весов, тестируя разные комбинации на исторических данных.

В итоге остановился на следующем распределении:

  • Price Level PPP: 30% (самый фундаментальный)
  • GDP-Implied PPP: 25% (самый актуальный)
  • Inflation-Adjusted PPP: 25% (самый теоретически обоснованный)
  • Big Mac Proxy: 20% (самый интуитивный)
def _calculate_composite_ppp(self, all_methods: Dict):
    weights = [0.30, 0.25, 0.25, 0.20]
    
    # Динамическая нормализация весов
    if valid_methods < 4:
        total_weight = sum(weights[:valid_methods])
        normalized_weights = [w / total_weight for w in weights[:valid_methods]]
    
    composite_rate = sum(rate * weight for rate, weight in zip(rates, normalized_weights))

Ключевая фишка — динамическая нормализация весов. Если какой-то метод не может дать результат (например, нет данных по инфляции), веса остальных методов пропорционально увеличиваются. Это гораздо умнее простого исключения "проблемных" методов.


Практика создания программы

После месяцев теоретической подготовки пришло время браться за клавиатуру. Я решил писать на Python — язык достаточно быстрый для финансовых расчетов, но при этом имеет отличные библиотеки для работы с данными.

Первый и самый важный вопрос — откуда брать данные? После изучения десятков источников, выбор пал на API Международного валютного фонда. Публичный, бесплатный, с хорошей документацией и, что особенно важно, регулярно обновляемый.

def __init__(self):
    self.base_url = "http://dataservices.imf.org/REST/SDMX_JSON.svc"
    self.session = requests.Session()
    self.session.headers.update({
        'User-Agent': 'Manual-PPP-Calculator/1.0',
        'Accept': 'application/json'
    })

Маленькая, но важная деталь: я использую requests.Session() вместо простых вызовов requests.get() . Это позволяет переиспользовать TCP-соединения и заметно ускоряет работу при множественных запросах к одному API.

User-Agent тоже не случаен. Многие API блокируют запросы без явно указанного User-Agent или с дефолтным значением от Python. Лучше сразу представиться как серьезное приложение.

Следующая проблема оказалась более коварной, чем ожидалось. Валютные коды (USD, EUR, GBP) нужно как-то связать с кодами стран в системе МВФ. И тут начались сюрпризы.

EUR — это не код страны, а валютного союза. В базе данных МВФ еврозона обозначается как U2. Швейцария — это CH, а не SW. Великобритания — GB, а не UK.

self.currency_country_map = {
    'USD': 'US', 'EUR': 'U2', 'GBP': 'GB', 'JPY': 'JP',
    'AUD': 'AU', 'CAD': 'CA', 'CHF': 'CH', 'NZD': 'NZ',
    'SEK': 'SE', 'NOK': 'NO', 'DKK': 'DK', 'PLN': 'PL'
}

Казалось бы, мелочь, но без правильного маппинга вся система просто не работает. Я потратил целый день на отладку, пока не понял, что ищу данные по несуществующим кодам стран.

Внешние API имеют неприятную особенность ломаться в самый неподходящий момент. API МВФ — не исключение. Иногда он недоступен по несколько часов, иногда возвращает неполные данные, иногда вообще отвечает ошибкой без видимых причин.

Я решил заложить систему резервных данных с самого начала:

self.fallback_market_rates = {
    'EURUSD': 1.0850, 'GBPUSD': 1.2650, 'USDJPY': 148.50,
    'AUDUSD': 0.6750, 'USDCAD': 1.3550, 'USDCHF': 0.8850,
    'NZDUSD': 0.6150
}

self.price_levels_2024 = {
    'US': 100.0,    # Базовый уровень
    'U2': 88.5,     # Еврозона дешевле США на 11.5%
    'GB': 85.2,     # Великобритания
    'JP': 67.4,     # Япония значительно дешевле
    'AU': 95.8,     # Австралия близко к США
    'CA': 91.3,     # Канада умеренно дешевле
    'CH': 125.6,    # Швейцария — самая дорогая
    'NZ': 89.7      # Новая Зеландия
}

Эти данные основаны на последних доступных международных сопоставлениях цен и актуальных рыночных курсах на момент написания кода. Они не идеально актуальные, но достаточно точные для работы системы в автономном режиме.

Некоторые могут сказать, что это костыль. Я считаю это продуманной архитектурой. В финансах надежность важнее идеальной точности. Лучше иметь приблизительно правильный результат, чем вообще никакого результата.

Для GDP-implied метода мне нужны были оценки ВВП разных стран в долларах США. Казалось бы, простая задача — берешь данные Всемирного банка и готово. Но тут тоже оказались подводные камни.

Всемирный банк публикует ВВП в текущих долларах (по рыночным курсам) и в долларах по ППС. Для моих целей нужны именно рыночные доллары, потому что я сравниваю их с ВВП в национальной валюте из статистики МВФ.

self.gdp_usd_estimates_2023 = {
    'US': 27000,    # $27 триллионов
    'U2': 17500,    # Еврозона ~$17.5 триллиона
    'GB': 3300,     # Великобритания ~$3.3 триллиона
    'JP': 4200,     # Япония ~$4.2 триллиона
    'AU': 1700,     # Австралия ~$1.7 триллиона
    'CA': 2100,     # Канада ~$2.1 триллиона
    'CH': 900,      # Швейцария ~$0.9 триллиона
    'NZ': 250       # Новая Зеландия ~$0.25 триллиона
}

Для inflation-adjusted метода нужна была опорная точка — исторические курсы, от которых считать инфляционную корректировку. Я выбрал 2020 год по нескольким причинам.

Во-первых, это достаточно недавно, чтобы быть актуальным. Во-вторых, 2020 год был до пандемии и связанных с ней экстремальных мер денежной политики. В-третьих, данные за 2020 год уже устоялись и не пересматриваются статистическими службами.

self.base_rates_2020 = {
    'U2': 0.85,   # EURUSD
    'GB': 0.78,   # GBPUSD  
    'JP': 106.0,  # USDJPY
    'AU': 1.45,   # AUDUSD
    'CA': 1.34,   # USDCAD
    'CH': 0.92,   # USDCHF
    'NZ': 1.52    # NZDUSD
}

Курсы взяты как средние значения за 2020 год, чтобы сгладить краткосрочную волатильность. Это важно, потому что inflation-adjusted метод предполагает, что базовый курс был "справедливым" в исходной точке.


Сложности работы с API МВФ

Самая неблагодарная, но критически важная часть любого финансового проекта — это работа с внешними источниками данных. API МВФ мощный и информативный, но у него есть свои причуды, которые пришлось изучить методом проб и ошибок.

Первое, с чем я столкнулся — API не любит большие запросы. Если пытаешься запросить сразу много индикаторов для множества стран, сервер отвечает ошибкой или таймаутом. Пришлось реализовать  разбиение запросов на мелкие части.

def fetch_all_available_data(self, countries: List[str], years: int = 10):
    all_indicators = [
        'NGDP_XDC',       # ВВП в национальной валюте
        'NGDP_USD',       # ВВП в долларах США  
        'PCPIPCH',        # Уровень инфляции
        'NGDP_RPCH',      # Реальный рост ВВП
        'ENDA_XDC_USD_RATE',  # Обменный курс
        'PCPI_IX',        # Индекс потребительских цен
        'LP'              # Население
    ]
    
    chunk_size = 5  # Максимум 5 индикаторов за запрос
    
    for i in range(0, len(all_indicators), chunk_size):
        chunk = all_indicators[i:i + chunk_size]
        
        countries_string = '+'.join(countries)
        indicators_string = '+'.join(chunk)
        
        url = f"{self.base_url}/CompactData/IFS/A.{countries_string}.{indicators_string}"

Размер чанки я подобрал эмпирически. 3 индикатора — слишком консервативно, приходится делать много запросов. 7-8 индикаторов — часто приводят к таймаутам. 5 оказалось оптимальным компромиссом.

API МВФ иногда ведет себя непредсказуемо. Одни и те же запросы могут проходить успешно утром и падать вечером. Иногда сервер возвращает частичные данные без предупреждения. Иногда данные приходят в неожиданном формате.

Я добавил обширную систему обработки ошибок с логированием:

try:
    response = self.session.get(url, params={
        'startPeriod': str(start_year),
        'endPeriod': str(end_year)
    }, timeout=60)
    
    if response.status_code == 200:
        raw_data = response.json()
        df_chunk = self._parse_response_data(raw_data)
        
        if not df_chunk.empty:
            all_data.append(df_chunk)
            logger.info(f"Chunk {i//chunk_size + 1}: {len(df_chunk)} data points loaded")
        else:
            logger.warning(f"Chunk {i//chunk_size + 1}: empty response")
    else:
        logger.error(f"HTTP {response.status_code}: {response.text}")

except requests.exceptions.Timeout:
    logger.warning(f"Timeout for chunk {i//chunk_size + 1}")
except requests.exceptions.RequestException as e:
    logger.error(f"Request failed for chunk {i//chunk_size + 1}: {e}")
except Exception as e:
    logger.error(f"Unexpected error in chunk {i//chunk_size + 1}: {e}")
    continue

Без такого подробного логирования отладка превратилась бы в ад. Когда запрос падает, нужно понимать, на каком этапе это произошло и почему.

Формат ответа API МВФ заслуживает отдельного упоминания. Они используют SDMX-JSON — "стандартный" формат для обмена статистическими данными. На практике это оказалось довольно болезненно.

def _parse_response_data(self, data: Dict) -> pd.DataFrame:
    records = []
    
    try:
        compact_data = data['CompactData']
        dataset = compact_data['DataSet']
        
        if 'Series' not in dataset:
            return pd.DataFrame()
        
        series_list = dataset['Series']
        # API может вернуть одну серию как объект или массив серий как список
        if not isinstance(series_list, list):
            series_list = [series_list]
        
        for series in series_list:
            # Все атрибуты помечены символом '@' — нужно это обрабатывать
            series_attrs = {k.replace('@', ''): v for k, v in series.items() 
                          if k.startswith('@')}
            
            obs_list = series.get('Obs', [])
            if not isinstance(obs_list, list):
                obs_list = [obs_list]
            
            for obs in obs_list:
                if isinstance(obs, dict):
                    record = series_attrs.copy()
                    record.update({
                        'year': obs.get('@TIME_PERIOD', ''),
                        'value': obs.get('@OBS_VALUE', ''),
                        'status': obs.get('@OBS_STATUS', '')
                    })
                    records.append(record)
        
        df = pd.DataFrame(records)
        
        if 'value' in df.columns:
            df['value'] = pd.to_numeric(df['value'], errors='coerce')
        
        if 'year' in df.columns:
            df['year'] = pd.to_numeric(df['year'], errors='coerce')
        
        return df
        
    except Exception as e:
        logger.error(f"Error parsing SDMX-JSON response: {e}")
        return pd.DataFrame()

Все атрибуты в SDMX-JSON помечены символом '@', что создает неудобства при работе с данными. Плюс API может вернуть одну серию данных как объект или несколько серий как массив — это тоже нужно обрабатывать.

Еще одна неприятность: иногда API возвращает данные без раздела 'Obs' (наблюдения), иногда с пустым 'Obs', иногда 'Obs' содержит не список, а единственный объект. Каждый случай требует отдельной обработки.

Если актуальных данных по инфляции нет, я использую типичные значения для каждой страны:

def _approximate_inflation_adjustment(self) -> Dict:
    logger.info("Using approximate inflation adjustment...")
    
    # Типичные уровни инфляции 2020-2024 (на основе исторических данных)
    typical_inflation = {
        'US': 4.5,   # США: относительно высокая инфляция из-за стимулов
        'U2': 3.8,   # Еврозона: умеренная инфляция
        'GB': 4.2,   # Великобритания: Brexit + энергетический кризис
        'JP': 1.8,   # Япония: традиционно низкая инфляция
        'AU': 4.1,   # Австралия: сырьевая инфляция
        'CA': 3.9,   # Канада: близко к США
        'CH': 2.1,   # Швейцария: низкая инфляция
        'NZ': 4.0    # Новая Зеландия: умеренная инфляция
    }
    
    inflation_adjusted = {}
    
    for country, base_rate in self.base_rates_2020.items():
        us_inflation = typical_inflation.get('US', 4.5)
        country_inflation = typical_inflation.get(country, 3.5)
        
        inflation_differential = us_inflation - country_inflation
        adjustment_factor = 1 + (inflation_differential / 100)
        adjusted_rate = base_rate * adjustment_factor
        
        inflation_adjusted[country] = {
            'inflation_adjusted_rate': adjusted_rate,
            'base_rate_2020': base_rate,
            'inflation_differential': inflation_differential,
            'method': 'approximate_inflation'
        }
        
        logger.info(f"{country}: Approx inflation diff {inflation_differential:+.2f}pp")
    
    return inflation_adjusted

Эти цифры я собрал из разных источников и усреднил за период 2020-2024. Они не идеально точные, но дают разумную аппроксимацию для стран с пропущенными данными.


Превращаем справедливые курсы в реальные деньги

Рассчитать справедливые курсы — это только половина дела. Главное — понять, что с ними делать. Как превратить академические расчеты в практические торговые сигналы?

def calculate_ppp_fair_values(self, currency_pairs: List[str]) -> Dict:
    logger.info("Starting manual PPP fair value calculation...")
    
    # Определяем, какие страны нам нужны
    countries = set()
    for pair in currency_pairs:
        base_currency = pair[:3]
        quote_currency = pair[3:]
        
        base_country = self.currency_country_map.get(base_currency)
        quote_country = self.currency_country_map.get(quote_currency)
        
        if base_country and quote_country:
            countries.add(base_country)
            countries.add(quote_country)
    
    logger.info(f"Will analyze {len(countries)} countries for {len(currency_pairs)} pairs")
    
    # Загружаем экономические данные
    economic_data = self.fetch_all_available_data(list(countries))
    
    # Рассчитываем ППС всеми методами
    ppp_calculation_results = self.calculate_manual_ppp_rates(economic_data)
    
    # Получаем текущие рыночные курсы
    market_rates = self.fallback_market_rates  # В реальной системе тут API рыночных данных
    
    # Структура результатов
    results = {
        'ppp_calculation_methods': ppp_calculation_results,
        'fair_values': {},
        'deviations': {},
        'market_rates': market_rates,
        'summary': {}
    }
    
    composite_ppp = ppp_calculation_results.get('composite_ppp_rates', {})
    
    # Рассчитываем справедливые курсы и отклонения для каждой пары
    for pair in currency_pairs:
        base_currency = pair[:3]
        quote_currency = pair[3:]
        
        base_country = self.currency_country_map.get(base_currency)
        quote_country = self.currency_country_map.get(quote_currency)
        
        if not base_country or not quote_country:
            logger.warning(f"Cannot map currencies for {pair}")
            continue
        
        # Вычисляем справедливый курс
        fair_value = self._calculate_pair_fair_value_from_ppp(
            composite_ppp, base_country, quote_country, pair
        )
        
        if fair_value:
            results['fair_values'][pair] = fair_value
            
            # Сравниваем с рыночным курсом
            market_rate = market_rates.get(pair)
            if market_rate and fair_value.get('fair_rate'):
                deviation = ((market_rate - fair_value['fair_rate']) / 
                           fair_value['fair_rate']) * 100
                
                # Классифицируем отклонение
                if deviation > 15:
                    status = 'significantly_overvalued'
                    magnitude = 'high'
                elif deviation > 5:
                    status = 'overvalued'
                    magnitude = 'moderate'
                elif deviation < -15:
                    status = 'significantly_undervalued'
                    magnitude = 'high'
                elif deviation < -5:
                    status = 'undervalued'
                    magnitude = 'moderate'
                else:
                    status = 'fair'
                    magnitude = 'low'
                
                results['deviations'][pair] = {
                    'market_rate': market_rate,
                    'fair_value': fair_value['fair_rate'],
                    'deviation_pct': deviation,
                    'status': status,
                    'magnitude': magnitude,
                    'confidence': fair_value.get('confidence', 0.5),
                    'signal_strength': abs(deviation) * fair_value.get('confidence', 0.5)
                }
                
                logger.info(f"{pair}: Market {market_rate:.4f}, Fair {fair_value['fair_rate']:.4f}, "
                          f"Deviation {deviation:+.1f}% ({status})")
    
    # Генерируем сводную статистику
    results['summary'] = self._generate_summary(results['deviations'])
    
    return results
Самая хитрая часть — правильно вычислить справедливый курс для валютной пары на основе ППС-коэффициентов отдельных стран:
def _calculate_pair_fair_value_from_ppp(self, composite_ppp: Dict, 
                                      base_country: str, quote_country: str, pair: str) -> Dict:
    """Вычисляем справедливый курс валютной пары из композитных ППС-данных"""
    
    base_ppp = composite_ppp.get(base_country, {})
    quote_ppp = composite_ppp.get(quote_country, {})
    
    # Случай 1: Одна из валют — доллар США (базовая валюта ППС)
    if quote_country == 'US':  # Пары вида XXXUSD
        if base_ppp:
            fair_rate = base_ppp['composite_ppp_rate']
            confidence = base_ppp['confidence']
        else:
            return {}
    elif base_country == 'US':  # Пары вида USDXXX
        if quote_ppp:
            fair_rate = quote_ppp['composite_ppp_rate']
            confidence = quote_ppp['confidence']
        else:
            return {}
    else:
        # Случай 2: Кросс-пары (без USD)
        if base_ppp and quote_ppp:
            # Для кросс-пар: PPP_base / PPP_quote
            fair_rate = base_ppp['composite_ppp_rate'] / quote_ppp['composite_ppp_rate']
            # Уверенность — минимум из двух валют
            confidence = min(base_ppp['confidence'], quote_ppp['confidence'])
        else:
            return {}
    
    return {
        'pair': pair,
        'fair_rate': fair_rate,
        'confidence': confidence,
        'base_country': base_country,
        'quote_country': quote_country,
        'base_ppp_data': base_ppp,
        'quote_ppp_data': quote_ppp
    }

Логика такая: если у нас есть ППС-коэффициенты для евро (0.885) и фунта (0.852) относительно доллара, то справедливый курс EUR/GBP = 0.885 / 0.852 = 1.039.

Простое сравнение с рыночными курсами дает процентное отклонение, но для практического применения нужна классификация:

# Классифицируем отклонение
if deviation > 15:
    status = 'significantly_overvalued'
    signal = 'STRONG_SELL'
elif deviation > 5:
    status = 'overvalued'
    signal = 'SELL'
elif deviation < -15:
    status = 'significantly_undervalued'
    signal = 'STRONG_BUY'
elif deviation < -5:
    status = 'undervalued'
    signal = 'BUY'
else:
    status = 'fair'
    signal = 'HOLD'

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

Важная инновация — расчет силы сигнала как произведения величины отклонения и индекса уверенности:

'signal_strength': abs(deviation) * fair_value.get('confidence', 0.5)

Это позволяет ранжировать торговые возможности. Сигнал с 20% отклонением и 50% уверенностью (сила = 10) менее привлекателен, чем сигнал с 15% отклонением и 80% уверенностью (сила = 12).

Самая частая проблема — пропущенные данные. У Швейцарии есть данные по ВВП, но нет по инфляции. У Новой Зеландии есть инфляция, но нет актуальных данных по ВВП. И так далее.

Изначально я планировал просто исключать страны с неполными данными, но это оказалось неэффективным — исключались бы почти все страны. Вместо этого я реализовал graceful degradation:

# Каждый метод работает независимо
methods_results = {
    'method_1': self._calculate_price_level_ppp(),
    'method_2': self._calculate_gdp_implied_ppp(economic_data),
    'method_3': self._calculate_inflation_adjusted_ppp(economic_data),
    'method_4': self._calculate_big_mac_proxy_ppp()
}

# Композитный метод адаптируется к доступным данным
for country in all_countries:
    available_methods = []
    available_rates = []
    
    for method_name, method_results in methods_results.items():
        if country in method_results:
            available_methods.append(method_name)
            available_rates.append(method_results[country]['rate'])
    
    if available_rates:
        # Пересчитываем веса для доступных методов
        weights = self._get_adjusted_weights(available_methods)
        composite_rate = sum(rate * weight for rate, weight in zip(available_rates, weights))



Полученные результаты

После нескольких месяцев разработки и отладки, система наконец заработала стабильно. Пришло время самого интересного — практического применения.

Анализ графика реальных цен евро-доллара vs цен по ППС, показывает — очень часто изменения курса по ППС предшествуют реальному изменению настоящего курса:

Запустив систему на данных за 2024 год, я получил несколько интересных результатов:

  • EURUSD: справедливый курс 0.8753, рыночный 1.0850 → евро переоценен 
  • GBPUSD: справедливый курс 0.8288, рыночный 1.2650 → фунт переоценен 
  • USDJPY: справедливый курс 136.20, рыночный 148.50 → йена недооценена
  • USDCHF: справедливый курс 1.150, рыночный 0.8850 → франк переоценен

Самым интересным оказался результат по йене. Все четыре метода единодушно показали, что при курсе 148+ йена серьезно недооценена. Confidence score составил 0.85 — один из самых высоких.

Я не стал создавать отдельную торговую систему на основе ППС. Вместо этого интегрировал расчеты с существующими алгоритмами как дополнительный фильтр.

Логика простая: если техническая система дает сигнал на покупку валюты, которая по ППС сильно переоценена, размер позиции уменьшается или сигнал игнорируется. И наоборот — сигналы в направлении ППС-дисбаланса усиливаются.


Дальнейшие планы по развитию системы

Текущая версия системы — это только начало

Планирую несколько направлений развития. Машинное обучение поможет динамически корректировать веса методов на основе исторической точности и текущих условий. Альтернативные источники данных включат данные OECD, индексы стоимости жизни, реальные цены Big Mac, satellite data и social sentiment. Расширение на криптовалюты, сырьевые валюты, акции и облигации откроет новые возможности. Автоматизация торговли с dynamic hedging и интеграцией с брокерскими API сделает систему полностью самостоятельной. Real-time мониторинг с web dashboard завершит картину.

Что я понял о ППС и валютных рынках

Несколько месяцев работы над этим проектом дали мне больше знаний о валютных рынках, чем годы технического анализа. ППС работает, но не так, как ожидаешь.

Это компас для долгосрочного направления, а не магическая формула быстрого обогащения. Отклонения корректируются за месяцы и годы, не дни и недели. Качество данных критично — я потратил больше времени на поиск и валидацию данных, чем на алгоритмы.

Простота оказалась лучше сложности — пять простых методов работают лучше любых нейронных сетей. ППС-торговля требует терпения, которого у большинства трейдеров нет. Фундаментальный анализ дает привязку к экономической реальности в эпоху алготорговли. Код — только инструмент для решения реальной задачи.


Итоги и выводы

Проект начинался, как простое любопытство: а можно ли рассчитать справедливые валютные курсы самостоятельно? Закончился же созданием полноценной системы анализа, которая помогает принимать более обоснованные торговые решения.

Путь от идеи до рабочего кода занял несколько месяцев, сотни часов изучения экономической теории, десятки итераций алгоритма и бесчисленные часы отладки. Но результат того стоил.

Самое главное, что я понял: не нужно изобретать велосипед или гнаться за сложностью. Иногда лучший алгоритм — это хорошо реализованная классика. Паритет покупательной способности работал 500 лет назад, работает сейчас и будет работать в будущем. Нужно только правильно его применять.

Полный исходный код системы доступен как open source проект. Буду рад вкладу сообщества в виде новых источников данных, альтернативных методов расчета ППС, улучшений обработки ошибок, интеграции с брокерскими API и бэктестинга на исторических данных. Если занимаетесь валютной торговлей или количественным анализом, попробуйте систему в действии — возможно, она изменит ваш взгляд на рынки.

Прикрепленные файлы |
PPP_Currency.py (44.66 KB)
Применение Grey-модели в техническом анализе финансовых временных рядов Применение Grey-модели в техническом анализе финансовых временных рядов
Данная статья посвящена изучению grey-модели — перспективного инструмента, способного расширить возможности трейдера. Мы рассмотрим некоторые варианты применения этой модели для технического анализа и построения торговых стратегий.
Введение в исследование фрактальных рыночных структур с помощью машинного обучения Введение в исследование фрактальных рыночных структур с помощью машинного обучения
В данной статье предпринята попытка рассмотрения финансовых временных рядов с точки зрения самоподобных фрактальных структур. Поскольку мы имеем слишком много аналогий, которые подтверждают возможность рассматривать рыночные котировки в качестве самоподобных фракталов, то имеем возможность составить представления о горизонтах прогнозирования таких структур.
Трейдинг с экономическим календарем MQL5 (Часть 2): Создание новостной панели Трейдинг с экономическим календарем MQL5 (Часть 2): Создание новостной панели
В этой статье мы создадим практичную новостную панель с использованием экономического календаря MQL5 для улучшения нашей торговой стратегии. Начнем с проектирования макета, уделив особое внимание ключевым элементам, таким как названия событий, важность и время, а затем перейдем к настройке в MQL5. Наконец, мы внедрим систему сортировки для отображения только самых актуальных новостей, предоставляя трейдерам быстрый доступ к важным экономическим событиям.
Инженерия признаков с Python и MQL5 (Часть II): Угол наклона цены Инженерия признаков с Python и MQL5 (Часть II): Угол наклона цены
На форуме MQL5 есть множество сообщений с просьбами помочь рассчитать угол наклона изменения цены. В этой статье мы рассмотрим один из способов расчета наклона изменения цены. Этот способ применим на любом рынке. Кроме того, мы определим, стоит ли разработка этой новой функции дополнительных усилий и времени. Выясним, может ли угол наклона цены улучшить точность нашей AI-модели при прогнозировании пары USDZAR на минутном таймфрейме.