Inteligencia artificial y aprendizaje automático para el comercio de divisas (Fx) Parte 5— Características

Esta serie de artículos está dedicada a comprender AI/ML y cómo se relaciona con el comercio de divisas. La mayoría de los artículos en el éter se centran en predecir un precio y son casi inútiles cuando se trata de encontrar estrategias comerciales rentables, por lo que nos centraremos aquí.

He operado con Fx durante 20 años usando tanto análisis estadístico y gráfico tradicional como AI/ML durante los últimos 5 años más o menos. Con una licenciatura en ingeniería, maestrías y varios certificados en aprendizaje automático, quería compartir algunas de las trampas que me tomó años aprender y explicar por qué es difícil, pero no imposible, hacer que un sistema funcione.

En los primeros cuatro artículos:
1. Creó el ejemplo más básico de «hola mundo». Recolectamos datos, generamos un modelo y medimos nuestro resultado
2. Usamos pesos de clase para obtener nuestro modelo «en el parque de pelota» y tal vez un poco mejor que adivinar y mejorar nuestra medida.
3. En el 3, miramos debajo de las sábanas de Logistic Regression para encontrar sus limitaciones y buscar a dónde podríamos ir a continuación.
4. Examinó la normalización y su impacto y se dio cuenta de que nuestra hipótesis puede ser falsa.
5. Este artículo analiza cómo reforzar nuestra hipótesis agregando más características

Esto no es de ninguna manera un consejo financiero y no aboga por ninguna estrategia comercial específica, sino que está diseñado para ayudar a comprender algunos de los detalles del mercado de divisas y cómo aplicarle las técnicas de ML.

También te puede interesar ¿Le tenemos demasiado miedo a la IA? Separemos el hecho del miedo

Es probable que nuestra hipótesis anterior no fuera cierta (algo en el precio de cierre de las últimas 4 horas predijo un movimiento repentino del precio en las próximas 4 horas). Por lo tanto, debemos intentar probar una hipótesis completamente nueva (y lo haremos en artículos futuros) o agregar/modificar características para ver si podemos hacer que nuestras características se correlacionen mejor con nuestra variable y_true. Este proceso se conoce como ingeniería de características.

En este artículo vamos a utilizar el código anterior, pero agregaremos las características adicionales simples. En nuestros datos sin procesar, tenemos datos para el par de precios AUDUSD y los pares de precios EURUSD al comienzo de cada hora con:
– Precio de apertura: el precio al comienzo de cada período de 1 hora
– Precio Alto: El precio más alto alcanzado durante esa hora
– Precio bajo: El precio más bajo alcanzado durante esa hora
– Precio de cierre: el precio al final del período de 1 hora
– Volumen: el número de cambios de precio durante ese período (no es estrictamente el número de operaciones, sino un indicador). Tenga en cuenta que esto normalmente es muy específico del corredor)

Gráfico de barras de ejemplo

Estos valores «OHLC» (apertura, máximo, mínimo, cierre) normalmente se representan en forma de barra o vela (forma de barra arriba).

Actualmente nuestras características incluyen
– Precio de cierre en el último periodo (que es imprescindible “ahora”)
– Precio de cierre hace 1 período (60 minutos)
– Precio de cierre hace 2 períodos (120 minutos)
– Precio de cierre hace 3 períodos (180 minutos)

También te puede interesar El futuro de las pequeñas empresas es la IA: cómo la inteligencia artificial transformará las operaciones en 2023

Recuerde que comenzamos una predicación al comienzo de cada período. Entonces, los períodos actuales «abiertos» y los últimos períodos «cerrados» serán los mismos (o muy similares). Por lo tanto, «abrir» y «cerrar» son esencialmente lo mismo y usar ambos no tendría ningún sentido. Entonces, podemos usar:

Características disponibles en los datos dados

Esto nos da 8 funciones en cada período de tiempo o 32 funciones en total. También necesitamos incluir una «base» para tener una base para mover «puntos» desde el precio bruto cuando normalizamos (último artículo)

Entonces, nuestra hipótesis ahora se ha convertido en ¿más características, y específicamente las características anteriores, mejorarán nuestro modelo? Vamos a averiguar:

En primer lugar, simplifiquemos los datos de carga para mantener el procesamiento de funciones por separado, ya que lo desarrollaremos en las próximas semanas.

También te puede interesarResumen semanal de arXiv #11
import numpy as np
import pandas as pd
from datetime import datetime

def load_data():
url = 'https://raw.githubusercontent.com/the-ml-bull/Hello_World/main/Fx60.csv'
dateparse = lambda x: datetime.strptime(x, '%d/%m/%Y %H:%M')

df = pd.read_csv(url, parse_dates=['date'], date_parser=dateparse)

return df

Cree una función casi nueva create_x_values ​​que cree la base de cada función (sin normalización). En lugar de nombres fijos para cada función, ahora recorremos cada nombre y la cantidad de períodos anteriores para crear la nueva función. En otros artículos desarrollaremos esto aún más. También devuelve los nombres de las funciones para que podamos usarlas en otras partes del código dándole la designación ‘x’, podemos separarlo de nuestros nombres genéricos de funciones.

También te puede interesar Desmitificando el procesamiento del lenguaje natural: la clave para comprender el contenido generado por IA
def create_x_values(df, feature_names):

x_values_df = pd.DataFrame()

# loop thorugh feature name and "back periods" to go back
x_feature_names = []
for feature in feature_names:
for period in [1,2,3,4]:
# create the name (eg 'x_audusd_close_t-1')
feature_name = 'x_' + feature + '_t-' + str(period)
x_feature_names.append(feature_name)
x_values_df[feature_name] = df[feature].shift(period)

# Add "starting" values when used in normalization
x_values_df['x_audusd_open'] = df['audusd_open'].shift(4)
x_values_df['x_eurusd_open'] = df['eurusd_open'].shift(4)
x_values_df['audusd_open'] = df['audusd_open']
x_values_df['eurusd_open'] = df['eurusd_open']

# add all future y values for future periods
for period in [0,1,2,3]:
name = 'y_t-' + str(period)
x_values_df[name] = df['audusd_close'].shift(-period)

# y is points 4 periods into the future - the open price now (not close)
x_values_df['y_future'] = df['audusd_close'].shift(-3)
x_values_df['y_change_price'] = x_values_df['y_future'] - df['audusd_open']
x_values_df['y_change_points'] = x_values_df['y_change_price'] * 100000
x_values_df['y'] = np.where(x_values_df['y_change_points'] >= 200, 1, 0)

# and reset df and done
x_values_df = x_values_df.copy()
return x_values_df, x_feature_names

Nuestro enrutamiento de normalización ha cambiado más. Centrado solo en las funciones que necesitamos (la designación x), compatible con diferentes valores iniciales para audusd, eurusd y volúmenes. En puntos y porcentajes, también escalamos manualmente el volumen para ponerlo «más o menos» en las mismas escalas y devolver los campos ‘_norm’ para que podamos usarlos en otros lugares.

from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import MinMaxScaler, StandardScaler

def normalize_data(df, x_fields, method):

norm_df = df.copy()
y_fields = ['y_t-0', 'y_t-1', 'y_t-2', 'y_t-3']

if method == 'price':
for field in x_fields:
norm_df[field + '_norm'] = df[field]

for field in y_fields:
norm_df[field + '_norm'] = df[field]

if method == 'points':
for field in x_fields:
if 'volume' in field:
norm_df[field + '_norm'] = df[field] / 100
elif 'audusd' in field:
norm_df[field + '_norm'] = (df[field] - df['x_audusd_open']) * 100000
elif 'eurusd' in field:
norm_df[field + '_norm'] = (df[field] - df['x_eurusd_open']) * 100000

for field in y_fields:
norm_df[field + '_norm'] = (df[field] - df['audusd_open']) * 100000

if method == 'percentage':
for field in x_fields:
if 'volume' in field:
norm_df[field + '_norm'] = df[field] / 10000
elif 'audusd' in field:
norm_df[field + '_norm'] = (df[field] - df['x_audusd_open']) / df[field] * 100
elif 'eurusd' in field:
norm_df[field + '_norm'] = (df[field] - df['x_eurusd_open']) / df[field] * 100

for field in y_fields:
norm_df[field + '_norm'] = (df[field] - df['audusd_open']) / df[field] * 100

if method == 'minmax':
scaler = MinMaxScaler()
scaled = scaler.fit_transform(df[x_fields + y_fields])
norm_field_names = [x + '_norm' for x in x_fields + y_fields]
norm_df[norm_field_names] = scaled

if method == 'stddev':
scaler = StandardScaler()
scaled = scaler.fit_transform(df[x_fields + y_fields])
norm_field_names = [x + '_norm' for x in x_fields + y_fields]
norm_df[norm_field_names] = scaled

x_feature_names_norm = [x + '_norm' for x in x_fields]
return norm_df, x_feature_names_norm

No hay cambios reales en nuestras funciones de tren/val, peso de clase o métricas. Ahora, vamos a ejecutarlo para cada método de normalización y comparar el resultado con la versión de función única de la semana pasada.

def get_train_val(df, x_feature_names_norm):
#
# Create Train and Val datasets
#

x = df[x_feature_names_norm]
y = df['y']
y_points = df['y_change_points']

# Note Fx "follows" (time series) so randomization is NOT a good idea
# create train and val datasets.
no_train_samples = int(len(x) * 0.7)
x_train = x[4:no_train_samples]
y_train = y[4:no_train_samples]

x_val = x[no_train_samples:-3]
y_val = y[no_train_samples:-3]
y_val_change_points = y_points[no_train_samples:-3]

return x_train, y_train, x_val, y_val, y_val_change_points

def get_class_weights(y_train, display=True):

#
# Create class weights
#
from sklearn.utils.class_weight import compute_class_weight

num_ones = np.sum(y_train)
num_zeros = len(y_train) - num_ones

classes = np.unique(y_train)
class_weights = compute_class_weight(class_weight='balanced', classes=classes, y=y_train)
class_weights = dict(zip(classes, class_weights))

if display:
print('In the training set we have 0s {} ({:.2f}%), 1s {} ({:.2f}%)'.format(num_zeros, num_zeros/len(y_train)*100, num_ones, num_ones/len(y_train)*100))
print('class weights {}'.format(class_weights))

return class_weights
from sklearn.metrics import log_loss, confusion_matrix, precision_score, recall_score, f1_score

def show_metrics(lr, x, y_true, y_change_points, display=True):

# predict from teh val set meas we have predictions and true values as binaries
y_pred = lr.predict(x)

#basic error types
log_loss_error = log_loss(y_true, y_pred)
score = lr.score(x, y_true)

#
# Customized metrics
#
tp = np.where((y_pred == 1) & (y_change_points >= 0), 1, 0).sum()
fp = np.where((y_pred == 1) & (y_change_points < 0), 1, 0).sum()
tn = np.where((y_pred == 0) & (y_change_points < 0), 1, 0).sum()
fn = np.where((y_pred == 0) & (y_change_points >= 0), 1, 0).sum()

precision = 0
if (tp + fp) > 0:
precision = tp / (tp + fp)

recall = 0
if (tp + fn) > 0:
recall = tp / (tp + fn)

f1 = 0
if (precision + recall) > 0:
f1 = 2 * precision * recall / (precision + recall)

# output the errors
if display:
print('Errors Loss: {:.4f}'.format(log_loss_error))
print('Errors Score: {:.2f}%'.format(score*100))
print('Errors tp: {} ({:.2f}%)'.format(tp, tp/len(y_val)*100))
print('Errors fp: {} ({:.2f}%)'.format(fp, fp/len(y_val)*100))
print('Errors tn: {} ({:.2f}%)'.format(tn, tn/len(y_val)*100))
print('Errors fn: {} ({:.2f}%)'.format(fn, fn/len(y_val)*100))
print('Errors Precision: {:.2f}%'.format(precision*100))
print('Errors Recall: {:.2f}%'.format(recall*100))
print('Errors F1: {:.2f}'.format(f1*100))

errors = {
'loss': log_loss_error,
'score': score,
'tp': tp,
'fp': fp,
'tn': tn,
'fn': fn,
'precision': precision,
'recall': recall,
'f1': f1
}

return errors

Así que vamos a ejecutarlo pasando por cada método de normalización.


for norm_method in ['price', 'points', 'percentage', 'minmax', 'stddev']:
df = load_data()

feature_names =['audusd_open', 'audusd_close', 'audusd_high', 'audusd_low', 'audusd_volume', \
'eurusd_open', 'eurusd_close', 'eurusd_high', 'eurusd_low', 'eurusd_volume']
df, x_feature_names = create_x_values(df, feature_names)

norm_df, x_feature_names_norm = normalize_data(df, x_feature_names, method=norm_method)
x_train, y_train, x_val, y_val, y_val_change_points = get_train_val(norm_df, x_feature_names_norm)
class_weights = get_class_weights(y_train, display=False)

lr = LogisticRegression(class_weight=class_weights)
lr.fit(x_train, y_train)

print('Errrors for method {}'.format(norm_method))
errors = show_metrics(lr, x_val, y_val, y_val_change_points, display=True)

Podemos ver que no hay mucha mejora. Sin embargo, hay algunas cosas que vale la pena señalar.

  1. Errores de “no convergieron”. Resaltado en cursiva aquí en puntos, porcentaje y normalización minmax. Esto significa que las matemáticas que tratan de encontrar la mejor solución «lineal» no pudieron minimizarla antes de que alcanzara su límite de iteración. El algoritmo tiene un límite de 100 iteraciones, pero puede cambiarlo fácilmente a 1000 con max_iter=1000 agregándolo en la línea LogisticRegression. Esto corregirá el problema, pero exploremos sin hacer esto, ya que agrega algunos puntos interesantes. El «error de convergencia no puede» probablemente se deba a algunas cosas.
    – No hay suficientes datos disponibles
    – Demasiadas funciones (frente a datos insuficientes)
    – Los datos no están correlacionados con las características.
    Constantemente obtenemos precisiones > 50% pero solo por poco. Por lo tanto, es probable (nuestra conclusión) que haya algo en el pasado que prediga el futuro, pero que es muy, muy débil o inexistente. Por lo tanto, debemos equilibrar todas estas cosas (número de características, cantidad de datos y correlación de datos/características) para obtener el equilibrio adecuado.
  2. El método de puntos se destaca con una puntuación F1 muy alta en comparación con otros métodos. ¿Es esto una anomalía? No convergió, por lo que bien podría ser solo una coincidencia aleatoria o ¿estamos en algo? La pérdida de entrenamiento es grande (¿recuerda que en nuestro primer artículo se sugirió que la pérdida y el retorno no estaban necesariamente correlacionados?)

Tal como está, esto no podría funcionar como un sistema comercial, pero hay algunos ajustes que podemos hacer y hay algunas lagunas en nuestra medición. ¡Exploraremos este próximo artículo!

Scroll al inicio