2025-05-20 18:28:53 +08:00
|
|
|
import logging
|
|
|
|
|
import seaborn as sns
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
import pandas as pd
|
|
|
|
|
|
|
|
|
|
from cycles.utils.storage import Storage
|
|
|
|
|
from cycles.utils.data_utils import aggregate_to_daily
|
|
|
|
|
from cycles.Analysis.boillinger_band import BollingerBands
|
|
|
|
|
from cycles.Analysis.rsi import RSI
|
2025-05-22 16:44:59 +08:00
|
|
|
from cycles.Analysis.strategies import Strategy
|
2025-05-20 18:28:53 +08:00
|
|
|
|
|
|
|
|
logging.basicConfig(
|
|
|
|
|
level=logging.INFO,
|
|
|
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
|
|
|
handlers=[
|
|
|
|
|
logging.FileHandler("backtest.log"),
|
|
|
|
|
logging.StreamHandler()
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
config_minute = {
|
2025-05-22 16:44:59 +08:00
|
|
|
"start_date": "2023-01-01",
|
|
|
|
|
"stop_date": "2024-01-01",
|
2025-05-20 18:28:53 +08:00
|
|
|
"data_file": "btcusd_1-min_data.csv"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
config_day = {
|
2025-05-22 16:44:59 +08:00
|
|
|
"start_date": "2023-01-01",
|
|
|
|
|
"stop_date": "2024-01-01",
|
2025-05-20 18:28:53 +08:00
|
|
|
"data_file": "btcusd_1-day_data.csv"
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-22 16:44:59 +08:00
|
|
|
config_strategy = {
|
|
|
|
|
"bb_width": 0.05,
|
|
|
|
|
"bb_period": 20,
|
|
|
|
|
"rsi_period": 14,
|
|
|
|
|
"trending": {
|
|
|
|
|
"rsi_threshold": [30, 70],
|
|
|
|
|
"bb_std_dev_multiplier": 2.5,
|
|
|
|
|
},
|
|
|
|
|
"sideways": {
|
|
|
|
|
"rsi_threshold": [40, 60],
|
|
|
|
|
"bb_std_dev_multiplier": 1.8,
|
|
|
|
|
},
|
|
|
|
|
"strategy_name": "MarketRegimeStrategy",
|
|
|
|
|
"SqueezeStrategy": True
|
|
|
|
|
}
|
2025-05-20 18:28:53 +08:00
|
|
|
|
2025-05-22 16:44:59 +08:00
|
|
|
IS_DAY = False
|
2025-05-20 18:28:53 +08:00
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
storage = Storage(logging=logging)
|
|
|
|
|
|
|
|
|
|
if IS_DAY:
|
|
|
|
|
config = config_day
|
|
|
|
|
else:
|
|
|
|
|
config = config_minute
|
|
|
|
|
|
|
|
|
|
data = storage.load_data(config["data_file"], config["start_date"], config["stop_date"])
|
|
|
|
|
|
|
|
|
|
if not IS_DAY:
|
|
|
|
|
data_daily = aggregate_to_daily(data)
|
|
|
|
|
storage.save_data(data, "btcusd_1-day_data.csv")
|
|
|
|
|
df_to_plot = data_daily
|
|
|
|
|
else:
|
|
|
|
|
df_to_plot = data
|
|
|
|
|
|
2025-05-22 16:44:59 +08:00
|
|
|
bb = BollingerBands(config=config_strategy)
|
2025-05-20 18:28:53 +08:00
|
|
|
data_bb = bb.calculate(df_to_plot.copy())
|
|
|
|
|
|
2025-05-22 16:44:59 +08:00
|
|
|
rsi_calculator = RSI(config=config_strategy)
|
2025-05-20 18:28:53 +08:00
|
|
|
data_with_rsi = rsi_calculator.calculate(df_to_plot.copy(), price_column='close')
|
|
|
|
|
|
|
|
|
|
# Combine BB and RSI data into a single DataFrame for signal generation
|
|
|
|
|
# Ensure indices are aligned; they should be as both are from df_to_plot.copy()
|
|
|
|
|
if 'RSI' in data_with_rsi.columns:
|
|
|
|
|
data_bb['RSI'] = data_with_rsi['RSI']
|
|
|
|
|
else:
|
|
|
|
|
# If RSI wasn't calculated (e.g., not enough data), create a dummy column with NaNs
|
|
|
|
|
# to prevent errors later, though signals won't be generated.
|
|
|
|
|
data_bb['RSI'] = pd.Series(index=data_bb.index, dtype=float)
|
|
|
|
|
logging.warning("RSI column not found or not calculated. Signals relying on RSI may not be generated.")
|
|
|
|
|
|
2025-05-22 16:44:59 +08:00
|
|
|
strategy = Strategy(config=config_strategy)
|
|
|
|
|
buy_condition, sell_condition = strategy.run(data_bb, config_strategy["strategy_name"])
|
2025-05-20 18:28:53 +08:00
|
|
|
|
|
|
|
|
buy_signals = data_bb[buy_condition]
|
|
|
|
|
sell_signals = data_bb[sell_condition]
|
|
|
|
|
|
|
|
|
|
# plot the data with seaborn library
|
|
|
|
|
if df_to_plot is not None and not df_to_plot.empty:
|
|
|
|
|
# Create a figure with two subplots, sharing the x-axis
|
2025-05-22 16:44:59 +08:00
|
|
|
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(16, 8), sharex=True)
|
2025-05-20 18:28:53 +08:00
|
|
|
|
|
|
|
|
# Plot 1: Close Price and Bollinger Bands
|
|
|
|
|
sns.lineplot(x=data_bb.index, y='close', data=data_bb, label='Close Price', ax=ax1)
|
|
|
|
|
sns.lineplot(x=data_bb.index, y='UpperBand', data=data_bb, label='Upper Band (BB)', ax=ax1)
|
|
|
|
|
sns.lineplot(x=data_bb.index, y='LowerBand', data=data_bb, label='Lower Band (BB)', ax=ax1)
|
|
|
|
|
# Plot Buy/Sell signals on Price chart
|
|
|
|
|
if not buy_signals.empty:
|
|
|
|
|
ax1.scatter(buy_signals.index, buy_signals['close'], color='green', marker='o', s=20, label='Buy Signal', zorder=5)
|
|
|
|
|
if not sell_signals.empty:
|
|
|
|
|
ax1.scatter(sell_signals.index, sell_signals['close'], color='red', marker='o', s=20, label='Sell Signal', zorder=5)
|
|
|
|
|
ax1.set_title('Price and Bollinger Bands with Signals')
|
|
|
|
|
ax1.set_ylabel('Price')
|
|
|
|
|
ax1.legend()
|
|
|
|
|
ax1.grid(True)
|
|
|
|
|
|
|
|
|
|
# Plot 2: RSI
|
|
|
|
|
if 'RSI' in data_bb.columns: # Check data_bb now as it should contain RSI
|
2025-05-22 16:44:59 +08:00
|
|
|
sns.lineplot(x=data_bb.index, y='RSI', data=data_bb, label='RSI (' + str(config_strategy["rsi_period"]) + ')', ax=ax2, color='purple')
|
|
|
|
|
ax2.axhline(config_strategy["trending"]["rsi_threshold"][1], color='red', linestyle='--', linewidth=0.8, label='Overbought (' + str(config_strategy["trending"]["rsi_threshold"][1]) + ')')
|
|
|
|
|
ax2.axhline(config_strategy['trending']['rsi_threshold'][0], color='green', linestyle='--', linewidth=0.8, label='Oversold (' + str(config_strategy['trending']['rsi_threshold'][0]) + ')')
|
2025-05-20 18:28:53 +08:00
|
|
|
# Plot Buy/Sell signals on RSI chart
|
|
|
|
|
if not buy_signals.empty:
|
|
|
|
|
ax2.scatter(buy_signals.index, buy_signals['RSI'], color='green', marker='o', s=20, label='Buy Signal (RSI)', zorder=5)
|
|
|
|
|
if not sell_signals.empty:
|
|
|
|
|
ax2.scatter(sell_signals.index, sell_signals['RSI'], color='red', marker='o', s=20, label='Sell Signal (RSI)', zorder=5)
|
|
|
|
|
ax2.set_title('Relative Strength Index (RSI) with Signals')
|
|
|
|
|
ax2.set_ylabel('RSI Value')
|
|
|
|
|
ax2.set_ylim(0, 100) # RSI is typically bounded between 0 and 100
|
|
|
|
|
ax2.legend()
|
|
|
|
|
ax2.grid(True)
|
|
|
|
|
else:
|
|
|
|
|
logging.info("RSI data not available for plotting.")
|
|
|
|
|
|
2025-05-22 16:44:59 +08:00
|
|
|
# Plot 3: BB Width
|
|
|
|
|
sns.lineplot(x=data_bb.index, y='BBWidth', data=data_bb, label='BB Width', ax=ax3)
|
|
|
|
|
sns.lineplot(x=data_bb.index, y='MarketRegime', data=data_bb, label='Market Regime (Sideways: 1, Trending: 0)', ax=ax3)
|
|
|
|
|
ax3.set_title('Bollinger Bands Width')
|
|
|
|
|
ax3.set_ylabel('BB Width')
|
|
|
|
|
ax3.legend()
|
|
|
|
|
ax3.grid(True)
|
|
|
|
|
|
2025-05-20 18:28:53 +08:00
|
|
|
plt.xlabel('Date') # Common X-axis label
|
|
|
|
|
fig.tight_layout() # Adjust layout to prevent overlapping titles/labels
|
|
|
|
|
plt.show()
|
|
|
|
|
else:
|
|
|
|
|
logging.info("No data to plot.")
|
|
|
|
|
|