Блог им. StanislavDzyuba
Всем привет, ранее я предпринимал попытки оценит обменный курс, используя данные о процентных ставках, темпах инфляции, ВВП и безработице на очень небольшом наборе данных и теперь я расширю это ислледование создав регрессионную модель используя больший объем данных, покажу откуда можно эти данные взять и как протестировать подобного рода стратегию с помощью библиотеки BT, и вы увидите результаты такой торговли.
Как вы, возможно, догадались, я исхожу из предположения, что процентная ставка влияет на валюту, потому что, во-первых, это механизм центральных банков, чтобы сообщать вам, дешево или дорого печатать деньги сейчас. Я имею в виду, что если у вас есть депозитная ставка в банке 20%, а торговля на фондовом рынке дает вам 10% годовых — вам лучше пойти в банк. Так, если сейчас ставка Банка Англии 4,25, а ставка ЕС 2,15, стоит продать евро, купить фунты и открыть депозит в Великобритании. Но процентная ставка устанавливается центральными банками, и центральные банки принимают свои решения, основываясь на макроэкономических данных, таких как — инфляция, безработица, ВВП. В основном их главная цель — поддерживать устойчивый рост (темпы роста ВВП должны неуклонно расти). Вот почему все эти факторы имеют значение, и, конечно же, центральные банки используют эти данные по-разному, они используют математические модели (похожие на IS-LM) для принятия решений о политике ставок. Что касается этого исследования, я сосредоточился на процентной ставке, инфляции, ВВП и безработице, и меня не особо интересует, как они связаны друг с другом. Я не буду рассматривать торговый баланс, баланс банка и доходность казначейских облигаций — они могут стать частью моего следующего исследования. В этом исследовании не учитываются анализ сентимента, Risk On и Risk Off.
Я получаю данные с forexfactory, просто собираю их из консоли разработчика. Возможно, есть более элегантный способ. Теперь я знаю, что investing.com дает возможность скачивать данные напрямую с сайта либо через парсер на питоне, но было уже слишком поздно, когда я об этом узнал.

Я сохранил все серии в формате JSON и смог прочитать данные из JSON в Pandas датафрейм.

Чтение из json делается довольно просто, выглядит это примерно так:
<code>def get_inflation_rates(country: str) -> list:
path:str = os.getcwd() + "/inflation.json"
with open(path, 'r') as j:
data:list = json.load(j)
interest_rates = data[country]
df_ir = pd.DataFrame.from_records(interest_rates)
return df_ir</code>После получения данных я активно объединяю их в один датафрейм и скачиваю ежедневные цены с помощью AlphaVantage. Я знаю, что тут много копипасты но мне просто лень заниматься рефакторингом:
<code>import yfinance as yf
from alpha_vantage.foreignexchange import ForeignExchange
api_key = "YOUR_API_KEY"
fx = ForeignExchange(key=api_key, output_format='pandas')
def prepare_data_for_regression(first_currency:str,second_currency:str):
#FIRST CURRENCY
#interest rate
df_ir_f_currency = get_interest_rates(first_currency)
format_date_reg(df_ir_f_currency)
df_ir_f_currency = df_ir_f_currency.set_index('date')
df_ir_f_currency_f = df_ir_f_currency[['actual']]
df_ir_f_currency_f = df_ir_f_currency_f.rename(columns={'actual':'fcurr_ir'})
#inflation rate
df_inf_f_currency = get_inflation_rates(first_currency)
format_date_reg(df_inf_f_currency)
df_inf_f_currency = df_inf_f_currency.set_index('date')
df_inf_f_currency_f = df_inf_f_currency[['actual']]
df_inf_f_currency_f = df_inf_f_currency_f.rename(columns={'actual':'fcurr_inf'})
#unemployment
df_u_f_currency = get_unemployment_rates(first_currency)
format_date_reg(df_u_f_currency)
df_u_f_currency = df_u_f_currency.set_index('date')
df_u_f_currency_f = df_u_f_currency[['actual']]
df_u_f_currency_f = df_u_f_currency_f.rename(columns={'actual':'fcurr_unem'})
#GDP
df_gdp_f_currency = get_gdp_rates(first_currency)
format_date_reg(df_gdp_f_currency)
df_gdp_f_currency = df_gdp_f_currency.set_index('date')
df_gdp_f_currency_f = df_gdp_f_currency[['actual']]
df_gdp_f_currency_f = df_gdp_f_currency_f.rename(columns={'actual':'fcurr_gdp'})
#SECOND CURRENCY
#interest rate
df_ir_s_currency = get_interest_rates(second_currency)
format_date_reg(df_ir_s_currency)
df_ir_s_currency = df_ir_s_currency.set_index('date')
df_ir_s_currency_f = df_ir_s_currency[['actual']]
df_ir_s_currency_f = df_ir_s_currency_f.rename(columns={'actual':'scurr_ir'})
#inflation rate
df_inf_s_currency = get_inflation_rates(second_currency)
format_date_reg(df_inf_s_currency)
df_inf_s_currency = df_inf_s_currency.set_index('date')
df_inf_s_currency_f = df_inf_s_currency[['actual']]
df_inf_s_currency_f = df_inf_s_currency_f.rename(columns={'actual':'scurr_inf'})
#unemployment
df_u_s_currency = get_unemployment_rates(second_currency)
format_date_reg(df_u_s_currency)
df_u_s_currency = df_u_s_currency.set_index('date')
df_u_s_currency_f = df_u_s_currency[['actual']]
df_u_s_currency_f = df_u_s_currency_f.rename(columns={'actual':'scurr_unem'})
#GDP
df_gdp_s_currency = get_gdp_rates(second_currency)
format_date_reg(df_gdp_s_currency)
df_gdp_s_currency = df_gdp_s_currency.set_index('date')
df_gdp_s_currency_f = df_gdp_s_currency[['actual']]
df_gdp_s_currency_f = df_gdp_s_currency_f.rename(columns={'actual':'scurr_gdp'})
#FIRST CURRENCY MERGE
df_ir_f_currency_f.index = pd.to_datetime(df_ir_f_currency_f.index,format="%d.%m.%Y")
df_inf_f_currency_f.index = pd.to_datetime(df_inf_f_currency_f.index,format="%d.%m.%Y")
df_u_f_currency_f.index = pd.to_datetime(df_u_f_currency_f.index,format="%d.%m.%Y")
df_gdp_f_currency_f.index = pd.to_datetime(df_gdp_f_currency_f.index,format="%d.%m.%Y")
ir_inf_f_currency = pd.merge_asof(df_ir_f_currency_f.sort_index(), df_inf_f_currency_f.sort_index(), left_index=True, right_index=True)
ir_inf_f_currency.index = pd.to_datetime(ir_inf_f_currency.index,format="%d.%m.%Y")
ir_inf_u_f_currency = pd.merge_asof(ir_inf_f_currency.sort_index(), df_u_f_currency_f.sort_index(), left_index=True, right_index=True)
ir_inf_u_f_currency.index = pd.to_datetime(ir_inf_u_f_currency.index,format="%d.%m.%Y")
ir_inf_u_gdp_f_currency = pd.merge_asof(ir_inf_u_f_currency.sort_index(), df_gdp_f_currency_f.sort_index(), left_index=True, right_index=True)
#SECOND CURRENCY MERGE
df_ir_s_currency_f.index = pd.to_datetime(df_ir_s_currency_f.index,format="%d.%m.%Y")
df_inf_s_currency_f.index = pd.to_datetime(df_inf_s_currency_f.index,format="%d.%m.%Y")
df_u_s_currency_f.index = pd.to_datetime(df_u_s_currency_f.index,format="%d.%m.%Y")
df_gdp_s_currency_f.index = pd.to_datetime(df_gdp_s_currency_f.index,format="%d.%m.%Y")
ir_inf_s_currency = pd.merge_asof(df_ir_s_currency_f.sort_index(), df_inf_s_currency_f.sort_index(), left_index=True, right_index=True)
ir_inf_s_currency.index = pd.to_datetime(ir_inf_s_currency.index,format="%d.%m.%Y")
ir_inf_u_s_currency = pd.merge_asof(ir_inf_s_currency.sort_index(), df_u_s_currency_f.sort_index(), left_index=True, right_index=True)
ir_inf_u_s_currency.index = pd.to_datetime(ir_inf_u_s_currency.index,format="%d.%m.%Y")
ir_inf_u_gdp_s_currency = pd.merge_asof(ir_inf_u_s_currency.sort_index(), df_gdp_s_currency_f.sort_index(), left_index=True, right_index=True)
#MERGE BOTH CURRENCIES
econ_data = pd.merge_asof(ir_inf_u_gdp_f_currency.sort_index(), ir_inf_u_gdp_s_currency.sort_index(), left_index=True, right_index=True)
#LOAD EXCHANGE RATE
ticker = first_currency.upper() + second_currency.upper() + '=X'
rate, meta_data = fx.get_currency_exchange_daily(from_symbol=first_currency.upper(), to_symbol=second_currency.upper(), outputsize='full')
rate = rate.rename(columns={'4. close':'exchange_rate'})
data = pd.merge_asof(econ_data.sort_index(), rate.sort_index(),left_index=True,right_index=True)
data = data.reset_index()
return data</code>Далее вызываем эту функцию, чтобы подготовить данные для регрессии:
<code>data= prepare_data_for_regression('eur', 'usd')</code>Сама регрессия:
<code>import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error
# Step 3: Preprocessing
data = data.dropna() # Drop rows with missing values
data['exchange_rate'] = data['exchange_rate'].diff(2)
data['fcurr_ir'] = data['fcurr_ir'].diff(1)
data['fcurr_inf'] = data['fcurr_inf'].diff(1)
data['fcurr_unem'] = data['fcurr_unem'].diff(1)
data['fcurr_ir'] = data['fcurr_ir'].diff(1)
data['fcurr_inf'] = data['fcurr_inf'].diff(1)
data['fcurr_unem'] = data['fcurr_unem'].diff(1)
data['scurr_ir'] = data['scurr_ir'].diff(1)
data['scurr_inf'] = data['scurr_inf'].diff(1)
data['scurr_unem'] = data['scurr_unem'].diff(1)
data['scurr_ir'] = data['scurr_ir'].diff(1)
data['scurr_inf'] = data['scurr_inf'].diff(1)
data['scurr_unem'] = data['scurr_unem'].diff(1)
data = data.dropna() # Drop rows again after shifting
# Step 4: Feature and Target
X = data.drop(columns=['date', 'exchange_rate'])
y = data['exchange_rate']
# Step 5: Split into training and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Step 6: Train the model
model = RandomForestRegressor(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
# Step 7: Evaluate the model
y_pred = model.predict(X_test)
mae = mean_absolute_error(y_test, y_pred)
mse = mean_squared_error(y_test, y_pred)
print(f"Mean Absolute Error: {mae}")
print(f"Mean Squared Error: {mse}")
# Step 8: Plot predictions vs actual values
plt.figure(figsize=(10, 6))
plt.plot(y_test.values, label='Actual')
plt.plot(y_pred, label='Predicted')
plt.legend()
plt.title('Exchange Rate Prediction')
plt.show()
# Step 9: Feature Importance
feature_importance = pd.DataFrame({'Feature': X.columns, 'Importance': model.feature_importances_})
feature_importance = feature_importance.sort_values(by='Importance', ascending=False)
print(feature_importance)</code>Вот качественные показатели по регрессионной модели для EURUSD:
> Mean Absolute Error: 0.04001494210526312
> Mean Squared Error: 0.0024860521025789452

По сути, это уже подготовленный сигнал — если фактический курс выше прогнозируемого, это означает, что он, вероятно, снизится до прогнозируемого значения, и наоборот — классическая стратегия возврата к среднему.
Теперь я могу сохранить результаты регрессии в CSV-файл и использовать его в качестве сигнала с помощью библиотеки BT. Датафрейм будет выглядеть примерно так:

Заполнение столбца «signal» выглядит так. Я также добавил к сигналу фильтрацию по скользящей средней c 50-дневным периодом.
<code>for i in range(len(df)):
el = df.iloc[i]
if(el['exchange_rate']>el['predicted_signal'] and el['exchange_rate']>el['ma50']):
df.at[i, 'signal'] = -1
elif(el['exchange_rate']<el['predicted_signal'] and el['exchange_rate']<el['ma50']):
df.at[i, 'signal'] = 1
df.at[i, 'date'] = datetime.strptime(df.at[i, 'date'], "%Y-%m-%d")</code>Вот так выглядит стратегия
<code>import bt
def run_ml_strategy(df):
price = df[["exchange_rate"]]
signal = df[["signal"]]
signal = signal.rename(columns={'signal':'exchange_rate'})
strategy = bt.Strategy(
"MLStrategy",
[
bt.algos.RunDaily(),
bt.algos.SelectAll(),
bt.algos.WeighTarget(signal),
bt.algos.Rebalance()
]
)
backtest = bt.Backtest(strategy, price)
return bt.run(backtest)
result = run_ml_strategy(df)
result.plot(title="ML Strategy - Equity Curv</code>Результат:

Получим больше результатов:
<code>result.display()</code>
<code>Stat MLStrategy ------------------- ------------ Start 2006-04-25 End 2025-05-28 Risk-free rate 0.00% Total Return 19.86% Daily Sharpe 1.03 Daily Sortino 1.74 CAGR 0.95% Max Drawdown -14.24% Calmar Ratio 0.07 MTD 5.72% 3m 4.60% 6m 1.22% YTD 1.22% 1Y -2.09% 3Y (ann.) 1.08% 5Y (ann.) 2.16% 10Y (ann.) 0.38% Since Incep. (ann.) 0.95% Daily Sharpe 1.03 Daily Sortino 1.74 ... Avg. Up Month 3.83% Avg. Down Month -0.62% Win Year % 47.37% Win 12m % 12.33%</code>
Я думаю, это доказывает, что фундаментальный анализ на форексе в целом работают.
Можете посмотреть полную версию кода тут.