
Análisis del impacto del clima en las divisas de los países agrícolas usando Python
Introducción al tema: la conexión entre el clima y los mercados financieros
La teoría económica clásica no ha reconocido durante mucho tiempo la influencia de los factores meteorológicos en el comportamiento del mercado. Pero las investigaciones de las últimas décadas han modificado por completo nuestra forma de ver esta situación. El profesor Edward Saikin, de la Universidad de Michigan, que realizó un estudio al respecto en 2023, demostró que los tráders toman decisiones un 27% más comedidas cuando hace mal tiempo que en los días soleados.
Esto resulta especialmente notable en los mayores centros financieros. Los días en que las temperaturas superan los 30°C, el volumen comercial la Bolsa de Nueva York desciende aproximadamente un 15% de media. En las bolsas asiáticas, cuando la presión atmosférica exterior es inferior a 740 mmHg, este hecho se correlaciona con un aumento de la volatilidad. Y los largos periodos de inclemencias meteorológicas en Londres provocan un notable aumento de la demanda de activos defensivos.
En este artículo, partiremos de la recopilación de datos meteorológicos para finalmente crear un sistema comercial completo que analizará los factores meteorológicos. Nuestro trabajo se basará en datos comerciales reales de los últimos cinco años en los principales centros financieros del mundo: Nueva York, Londres, Tokio, Hong Kong y Fráncfort. Aplicando herramientas reales de análisis de datos y aprendizaje automático, obtendremos señales comerciales procesables a partir de las observaciones meteorológicas.
Recopilación de datos meteorológicos
Uno de los factores más importantes del sistema será el módulo de adquisición y preprocesamiento de datos. Para trabajar con datos meteorológicos, usaremos la API Meteostat, que permite acceder a datos meteorológicos archivados en todo el planeta. Veamos cómo se implementa la función de obtención de datos:
def fetch_agriculture_weather(): """ Fetching weather data for important agricultural regions """ key_regions = { "AU_WheatBelt": { "lat": -31.95, "lon": 116.85, "description": "Key wheat production region in Australia" }, "NZ_Canterbury": { "lat": -43.53, "lon": 172.63, "description": "Main dairy production region in New Zealand" }, "CA_Prairies": { "lat": 50.45, "lon": -104.61, "description": "Canada's breadbasket, wheat and canola production" } }
En esta función, identificaremos las regiones agrícolas más importantes con sus coordenadas de localización. Las coordenadas elegidas para el cinturón de cultivo de trigo de Australia serán las de la parte central de la región, para Nueva Zelanda las del emplazamiento de Canterbury y para Canadá las de la zona central de la pradera.
Una vez obtenidos los datos brutos, deberemos someterlos a un riguroso procesamiento. Para ello, implementaremos la funciónProcess_weather_data:
def process_weather_data(raw_data): if not isinstance(raw_data.index, pd.DatetimeIndex): raw_data.index = pd.to_datetime(raw_data.index) processed_data = pd.DataFrame(index=raw_data.index) processed_data['temperature'] = raw_data['tavg'] processed_data['temp_min'] = raw_data['tmin'] processed_data['temp_max'] = raw_data['tmax'] processed_data['precipitation'] = raw_data['prcp'] processed_data['wind_speed'] = raw_data['wspd'] processed_data['growing_degree_days'] = calculate_gdd( processed_data['temp_max'], base_temp=10 ) return processed_data
También deberemos prestar atención al cálculo del indicador GrowingDegreeDays (GDD), que supondrá una métrica necesaria para evaluar la posibilidad de crecimiento de los cultivos. Esta cifra se basará en la temperatura máxima durante el día, considerando la temperatura normal de crecimiento de las plantas.
def analyze_and_visualize_correlations(merged_data): plt.style.use('default') plt.rcParams['figure.figsize'] = [15, 10] plt.rcParams['axes.grid'] = True # Weather-price correlation analysis for each region for region, data in merged_data.items(): if data.empty: continue weather_cols = ['temperature', 'precipitation', 'wind_speed', 'growing_degree_days'] price_cols = ['close', 'volatility', 'range_pct', 'price_momentum', 'monthly_change'] correlation_matrix = pd.DataFrame() for w_col in weather_cols: if w_col not in data.columns: continue for p_col in price_cols: if p_col not in data.columns: continue correlations = [] lags = [0, 5, 10, 20, 30] # Days to lag price data for lag in lags: corr = data[w_col].corr(data[p_col].shift(-lag)) correlations.append({ 'weather_factor': w_col, 'price_metric': p_col, 'lag_days': lag, 'correlation': corr }) correlation_matrix = pd.concat([ correlation_matrix, pd.DataFrame(correlations) ]) return correlation_matrix def plot_correlation_heatmap(pivot_table, region): plt.figure() im = plt.imshow(pivot_table.values, cmap='RdYlBu', aspect='auto') plt.colorbar(im) plt.xticks(range(len(pivot_table.columns)), pivot_table.columns, rotation=45) plt.yticks(range(len(pivot_table.index)), pivot_table.index) # Add correlation values in each cell for i in range(len(pivot_table.index)): for j in range(len(pivot_table.columns)): text = plt.text(j, i, f'{pivot_table.values[i, j]:.2f}', ha='center', va='center') plt.title(f'Weather Factors and Price Correlations for {region}') plt.tight_layout()
La obtención de datos sobre pares de divisas y su sincronización
Tras configurar la recogida de datos meteorológicos, deberemos implementar la obtención de información sobre el movimiento de los pares de divisas. Para ello, usaremos la plataforma MetaTrader 5, que ofrece una cómoda API para trabajar con los datos históricos de instrumentos financieros.
Vamos a analizar la función de obtención de datos sobre los pares de divisas:
def get_agricultural_forex_pairs(): """ Getting data on currency pairs via MetaTrader 5 """ if not mt5.initialize(): print("MT5 initialization error") return None pairs = ["AUDUSD", "NZDUSD", "USDCAD"] timeframes = { "H1": mt5.TIMEFRAME_H1, "H4": mt5.TIMEFRAME_H4, "D1": mt5.TIMEFRAME_D1 } # ... the rest of the function code
En esta función, trabajaremos con tres grandes pares de divisas que se corresponderán con nuestras regiones agrícolas: AUDUSD para el cinturón de trigo de Australia, NZDUSD para la región de Canterbury y USDCAD para las praderas canadienses. Para cada par, recopilaremos los datos en tres marcos temporales: horas (H1), cuatro horas (H4) y diario (D1).
Deberemos prestar especial atención al proceso de combinación de los datos meteorológicos y financieros. Para ello hemos implementado una función especial:
def merge_weather_forex_data(weather_data, forex_data): """ Combining weather and financial data """ synchronized_data = {} region_pair_mapping = { 'AU_WheatBelt': 'AUDUSD', 'NZ_Canterbury': 'NZDUSD', 'CA_Prairies': 'USDCAD' } # ... the rest of the function code
Esta función resuelve el complejo problema de la sincronización de los datos de distintas fuentes. Los datos meteorológicos y las cotizaciones de divisas tienen intervalos de actualización distintos, por lo que utilizaremos un método especial merge_asof de la biblioteca pandas que nos permitirá comparar correctamente los valores considerando las etiquetas de tiempo.
Luego procesaremos de forma adicional los datos combinados para mejorar la calidad de los análisis:
def calculate_derived_features(data): """ Calculation of derived indicators """ if not data.empty: data['price_volatility'] = data['volatility'].rolling(24).std() data['temp_change'] = data['temperature'].diff() data['precip_intensity'] = data['precipitation'].rolling(24).sum() # ... the rest of the function code
Aquí calcularemos importantes indicadores derivados, como la volatilidad de los precios en las últimas 24 horas, los cambios de temperatura y la intensidad de las precipitaciones. También añadiremos una característica binaria que indicará la estación de crecimiento, especialmente importante para analizar los cultivos.
Prestaremos especial atención a limpiar los datos de valores atípicos y a rellenar los valores que faltan:
def clean_merged_data(data): """ Cleaning up merged data """ weather_cols = ['temperature', 'precipitation', 'wind_speed'] # Fill in the blanks for col in weather_cols: if col in data.columns: data[col] = data[col].ffill(limit=3) # Removing outliers for col in weather_cols: if col in data.columns: q_low = data[col].quantile(0.01) q_high = data[col].quantile(0.99) data = data[ (data[col] > q_low) & (data[col] < q_high) ] # ... the rest of the function code
Esta función utilizará el método de rellenado directo (forward fill) para procesar los valores que faltan en los datos meteorológicos, pero con un límite de 3 periodos para evitar la presencia de valores incorrectos si se dan omisiones largas. También eliminaremos los valores extremos fuera de los percentiles 1 y 99, lo cual nos ayudará a evitar que los resultados del análisis se vean distorsionados por valores atípicos.
Aquí tenemos el resultado de la ejecución de las funciones del conjunto de datos:
Análisis de la correlación entre los factores meteorológicos y los tipos de cambio
Durante la observación hemos analizado diversos puntos de interrelación entre las condiciones meteorológicas y la dinámica de los precios de los pares de divisas. Para encontrar patrones que no resulten evidentes a primera vista, hemos creado una metodología especial para calcular las correlaciones considerando los desfases temporales:
def analyze_weather_price_correlations(merged_data): """ Analysis of correlations with time lags between weather conditions and price movements """ def calculate_lagged_correlations(data, weather_col, price_col, max_lag=72): print(f"Calculating lagged correlations: {weather_col} vs {price_col}") correlations = [] for lag in range(max_lag): corr = data[weather_col].corr(data[price_col].shift(-lag)) correlations.append({ 'lag': lag, 'correlation': corr, 'weather_factor': weather_col, 'price_metric': price_col }) return pd.DataFrame(correlations) correlations = {} weather_factors = ['temperature', 'precipitation', 'wind_speed', 'growing_degree_days'] price_metrics = ['close', 'volatility', 'price_momentum', 'monthly_change'] for region, data in merged_data.items(): if data.empty: print(f"Skipping empty dataset for {region}") continue print(f"\nAnalyzing correlations for region: {region}") region_correlations = {} for w_col in weather_factors: for p_col in price_metrics: key = f"{w_col}_{p_col}" region_correlations[key] = calculate_lagged_correlations(data, w_col, p_col) correlations[region] = region_correlations return correlations def analyze_seasonal_patterns(data): """ Analysis of seasonal correlation patterns """ print("Starting seasonal pattern analysis...") seasonal_correlations = {} data['month'] = data.index.month monthly_correlations = [] for month in range(1, 13): print(f"Analyzing month: {month}") month_data = data[data['month'] == month] month_corr = {} for w_col in ['temperature', 'precipitation', 'wind_speed']: month_corr[w_col] = month_data[w_col].corr(month_data['close']) monthly_correlations.append(month_corr) return pd.DataFrame(monthly_correlations, index=range(1, 13))
El análisis de los datos encontrados ha revelado patrones interesantes. En el caso del cinturón de trigo australiano, la correlación más fuerte (0,21) se observa entre la velocidad del viento y las variaciones mensuales de AUDUSD. Esto puede atribuirse al hecho de que los fuertes vientos durante la madurez del trigo pueden reducir el rendimiento de la cosecha. El factor de temperatura también muestra una fuerte correlación (0,18), además, esta influencia particular se manifiesta casi sin desfase temporal.
La región neozelandesa de Canterbury muestra pautas más complejas. La correlación más fuerte (0,084) se manifiesta entre la temperatura y la volatilidad con un desfase de 10 días. Cabe señalar que la influencia de los factores meteorológicos en NZDUSD se refleja más en la volatilidad que en la dirección del movimiento de los precios, mientras que las correlaciones estacionales alcanzan a veces el 1,00, lo cual indica una correlación perfecta.
Creando el modelo de aprendizaje automático para la predicción
El núcleo de nuestra estrategia será el modelo de gradient bousting CatBoost, que ha demostrado su eficacia en series temporales. Vamos a desglosar paso a paso el proceso de creación del modelo.
Preparamos las características
El primer paso consistirá en generar las características para el modelo. Para ello, recopilaremos una selección de indicadores técnicos y meteorológicos:
def prepare_ml_features(data): """ Preparation of features for the ML model """ print("Starting feature preparation...") features = pd.DataFrame(index=data.index) # Weather features weather_cols = [ 'temperature', 'precipitation', 'wind_speed', 'growing_degree_days' ] for col in weather_cols: if col not in data.columns: print(f"Warning: {col} not found in data") continue print(f"Processing weather feature: {col}") # Base values features[col] = data[col] # Moving averages features[f"{col}_ma_24"] = data[col].rolling(24).mean() features[f"{col}_ma_72"] = data[col].rolling(72).mean() # Changes features[f"{col}_change"] = data[col].pct_change() features[f"{col}_change_24"] = data[col].pct_change(24) # Volatility features[f"{col}_volatility"] = data[col].rolling(24).std() # Price indicators price_cols = ['volatility', 'range_pct', 'monthly_change'] for col in price_cols: if col not in data.columns: continue features[f"{col}_ma_24"] = data[col].rolling(24).mean() # Seasonal features features['month'] = data.index.month features['day_of_week'] = data.index.dayofweek features['growing_season'] = ( (data.index.month >= 4) & (data.index.month <= 9) ).astype(int) return features.dropna() def create_prediction_targets(data, forecast_horizon=24): """ Creation of target variables for prediction """ print(f"Creating prediction targets with horizon: {forecast_horizon}") targets = pd.DataFrame(index=data.index) # Price change percentage targets['price_change'] = data['close'].pct_change( forecast_horizon ).shift(-forecast_horizon) # Price direction targets['direction'] = (targets['price_change'] > 0).astype(int) # Future volatility targets['volatility'] = data['volatility'].rolling( forecast_horizon ).mean().shift(-forecast_horizon) return targets.dropna()
Creamos y entrenamos los modelos
Para cada variable considerada, crearemos un modelo independiente con parámetros optimizados:
from catboost import CatBoostClassifier, CatBoostRegressor from sklearn.metrics import accuracy_score, mean_squared_error from sklearn.model_selection import TimeSeriesSplit # Define categorical features cat_features = ['month', 'day_of_week', 'growing_season'] # Create models for different tasks models = { 'direction': CatBoostClassifier( iterations=1000, learning_rate=0.01, depth=7, l2_leaf_reg=3, loss_function='Logloss', eval_metric='Accuracy', random_seed=42, verbose=False, cat_features=cat_features ), 'price_change': CatBoostRegressor( iterations=1000, learning_rate=0.01, depth=7, l2_leaf_reg=3, loss_function='RMSE', random_seed=42, verbose=False, cat_features=cat_features ), 'volatility': CatBoostRegressor( iterations=1000, learning_rate=0.01, depth=7, l2_leaf_reg=3, loss_function='RMSE', random_seed=42, verbose=False, cat_features=cat_features ) } def train_ml_models(merged_data, region): """ Training ML models using time series cross-validation """ print(f"Starting model training for region: {region}") data = merged_data[region] features = prepare_ml_features(data) targets = create_prediction_targets(data) # Split into folds tscv = TimeSeriesSplit(n_splits=5) results = {} for target_name, model in models.items(): print(f"\nTraining model for target: {target_name}") fold_metrics = [] predictions = [] test_indices = [] for fold_idx, (train_idx, test_idx) in enumerate(tscv.split(features)): print(f"Processing fold {fold_idx + 1}/5") X_train = features.iloc[train_idx] y_train = targets[target_name].iloc[train_idx] X_test = features.iloc[test_idx] y_test = targets[target_name].iloc[test_idx] # Training with early stopping model.fit( X_train, y_train, eval_set=(X_test, y_test), early_stopping_rounds=50, verbose=False ) # Predictions and evaluation pred = model.predict(X_test) predictions.extend(pred) test_indices.extend(test_idx) # Metric calculation metric = ( accuracy_score(y_test, pred) if target_name == 'direction' else mean_squared_error(y_test, pred, squared=False) ) fold_metrics.append(metric) print(f"Fold {fold_idx + 1} metric: {metric:.4f}") results[target_name] = { 'model': model, 'metrics': fold_metrics, 'mean_metric': np.mean(fold_metrics), 'predictions': pd.Series( predictions, index=features.index[test_indices] ) } print(f"Mean {target_name} metric: {results[target_name]['mean_metric']:.4f}") return results
Características de la implementación
Los siguientes parámetros serán fundamentales para nuestra aplicación:
- Trabajo con característica categóricas: CatBoost procesa de forma productiva variables categóricas como el mes y el día de la semana, por ejemplo, sin necesidad de codificación adicional.
- Parada anticipada: para evitar los intentos de reentrenamiento, se utiliza un mecanismo de parada anticipada con early_stopping_rounds=50.
- Equilibrio entre profundidad y generalización: los parámetros depth=7 y l2_leaf_reg=3 se seleccionan para maximizar el equilibrio entre la profundidad del árbol y la regularización.
- Trabajo con series temporales: el uso de TimeSeriesSplit garantiza que los datos de las series temporales se dividan correctamente, evitando posibles fugas de datos en el futuro.
Esta arquitectura del modelo nos ayudará a captar de forma productiva las dependencias a corto y largo plazo entre las condiciones meteorológicas y los movimientos de las divisas, como demuestran los resultados obtenidos en las pruebas.
Evaluación de la precisión del modelo y visualización de los resultados
Los modelos de aprendizaje automático resultantes se han probado con los datos de un periodo de 5 años utilizando un método de ventana deslizante con cinco pliegues. Hemos preparado tres tipos de modelos para cada área: la predicción de la dirección del movimiento de precios (clasificación), la predicción de la magnitud del cambio de precios (regresión) y la predicción de la volatilidad (regresión).
import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import confusion_matrix, classification_report def evaluate_model_performance(results, region_data): """ Comprehensive model evaluation across all regions """ print(f"\nEvaluating model performance for {len(results)} regions") evaluation = {} for region, models in results.items(): print(f"\nAnalyzing {region} performance:") region_metrics = { 'direction': { 'accuracy': models['direction']['mean_metric'], 'fold_metrics': models['direction']['metrics'], 'max_accuracy': max(models['direction']['metrics']), 'min_accuracy': min(models['direction']['metrics']) }, 'price_change': { 'rmse': models['price_change']['mean_metric'], 'fold_metrics': models['price_change']['metrics'] }, 'volatility': { 'rmse': models['volatility']['mean_metric'], 'fold_metrics': models['volatility']['metrics'] } } print(f"Direction prediction accuracy: {region_metrics['direction']['accuracy']:.2%}") print(f"Price change RMSE: {region_metrics['price_change']['rmse']:.4f}") print(f"Volatility RMSE: {region_metrics['volatility']['rmse']:.4f}") evaluation[region] = region_metrics return evaluation def plot_feature_importance(models, region): """ Visualize feature importance for each model type """ plt.figure(figsize=(15, 10)) for target, model_info in models.items(): feature_importance = pd.DataFrame({ 'feature': model_info['model'].feature_names_, 'importance': model_info['model'].feature_importances_ }) feature_importance = feature_importance.sort_values('importance', ascending=False) plt.subplot(3, 1, list(models.keys()).index(target) + 1) sns.barplot(x='importance', y='feature', data=feature_importance.head(10)) plt.title(f'{target.capitalize()} Model - Top 10 Important Features') plt.tight_layout() plt.show() def visualize_seasonal_patterns(results, region_data): """ Create visualization of seasonal patterns in predictions """ for region, data in region_data.items(): print(f"\nVisualizing seasonal patterns for {region}") # Create monthly aggregation of accuracy monthly_accuracy = pd.DataFrame(index=range(1, 13)) data['month'] = data.index.month for month in range(1, 13): month_predictions = results[region]['direction']['predictions'][ data.index.month == month ] month_actual = (data['close'].pct_change() > 0)[ data.index.month == month ] accuracy = accuracy_score( month_actual, month_predictions ) monthly_accuracy.loc[month, 'accuracy'] = accuracy # Plot seasonal accuracy plt.figure(figsize=(12, 6)) monthly_accuracy['accuracy'].plot(kind='bar') plt.title(f'Seasonal Prediction Accuracy - {region}') plt.xlabel('Month') plt.ylabel('Accuracy') plt.show() def plot_correlation_heatmap(correlation_data): """ Create heatmap visualization of correlations """ plt.figure(figsize=(12, 8)) sns.heatmap( correlation_data, cmap='RdYlBu', center=0, annot=True, fmt='.2f' ) plt.title('Weather-Price Correlation Heatmap') plt.tight_layout() plt.show()
Resultados por región
AU_WheatBelt (Cinturón de trigo de Australia)
- Precisión media en la predicción de la dirección de AUDUSD: 62.67%
- Precisión máxima en pliegues individuales: 82.22%
- RMSE de la predicción de la variación de precios: 0.0303
- RMSE de la volatilidad: 0.0016
Región de Canterbury (Nueva Zelanda)
- Precisión media de predicción de NZDUSD: 62.81%
- Precisión máxima: 75.44%
- Precisión mínima: 54.39%
- RMSE de la predicción de la variación de precios: 0.0281
- RMSE de la volatilidad: 0.0015
Región de las praderas canadienses
- Precisión media de predicción de la dirección: 56.92%
- Precisión máxima (tercer pliegue): 71.79%
- RMSE de la predicción de la variación de precios: 0.0159
- RMSE de la volatilidad: 0.0023
Análisis y visualización de la estacionalidad
def analyze_model_seasonality(results, data): """ Analyze seasonal performance patterns of the models """ print("Starting seasonal analysis of model performance") seasonal_metrics = {} for region, region_results in results.items(): print(f"\nAnalyzing {region} seasonal patterns:") # Extract predictions and actual values predictions = region_results['direction']['predictions'] actuals = data[region]['close'].pct_change() > 0 # Calculate monthly accuracy monthly_acc = [] for month in range(1, 13): month_mask = predictions.index.month == month if month_mask.any(): acc = accuracy_score( actuals[month_mask], predictions[month_mask] ) monthly_acc.append(acc) print(f"Month {month} accuracy: {acc:.2%}") seasonal_metrics[region] = pd.Series( monthly_acc, index=range(1, 13) ) return seasonal_metrics def plot_seasonal_performance(seasonal_metrics): """ Visualize seasonal performance patterns """ plt.figure(figsize=(15, 8)) for region, metrics in seasonal_metrics.items(): plt.plot(metrics.index, metrics.values, label=region, marker='o') plt.title('Model Accuracy by Month') plt.xlabel('Month') plt.ylabel('Accuracy') plt.legend() plt.grid(True) plt.show()
Los resultados de la visualización demuestran una estacionalidad significativa en el rendimiento del modelo.
Resultan especialmente notables los picos en la precisión de las predicciones:
- Para el AUDUSD: diciembre-febrero (periodo de maduración del trigo)
- NZDUSD: periodos de máxima producción de leche
- Para USDCAD: estaciones de crecimiento activas en las praderas
Estos resultados apoyan la hipótesis de que existe un impacto significativo de las condiciones meteorológicas en los tipos de cambio de las divisas agrícolas, sobre todo durante los periodos críticos de la producción agrícola.
Conclusión
El estudio ha hallado relaciones significativas entre las condiciones meteorológicas en las regiones agrícolas y la dinámica de los pares de divisas. El sistema de previsión ha demostrado una gran precisión durante los periodos de clima extremo y máxima producción agrícola, mostrando una precisión media de hasta el 62,67% para AUDUSD, el 62,81% para NZDUSD y el 56,92% para USDCAD.
Recomendaciones:
- AUDUSD: negociación de diciembre a febrero, centrada en el viento y la temperatura.
- NZDUSD: negociación a medio plazo durante la producción lechera activa.
- USDCAD: negociación en las épocas de siembra y cosecha.
El sistema requiere actualizaciones periódicas de los datos para mantener su exactitud, sobre todo ante las perturbaciones del mercado. Nuestras perspectivas incluyen la ampliación de las fuentes de datos y la aplicación del aprendizaje profundo para mejorar la solidez de las predicciones.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/16060
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso
para muchas personas será una revelación que CAD no es tanto el petróleo, pero las mezclas de cereales para piensos :-))
Lo que se negocia principalmente en las bolsas nacionales para la moneda nacional, afecta...
para USDCAD e incluso sólo las temporadas agrícolas debe ser rastreable.