2025-05-20 16:59:17 +08:00
|
|
|
import os
|
|
|
|
|
import pandas as pd
|
2025-06-25 13:08:07 +08:00
|
|
|
from typing import Optional, Union, Dict, Any, List
|
|
|
|
|
import logging
|
2025-05-20 16:59:17 +08:00
|
|
|
|
2025-06-25 13:08:07 +08:00
|
|
|
from .data_loader import DataLoader
|
|
|
|
|
from .data_saver import DataSaver
|
|
|
|
|
from .result_formatter import ResultFormatter
|
|
|
|
|
from .storage_utils import DataLoadingError, DataSavingError
|
2025-05-20 16:59:17 +08:00
|
|
|
|
2025-06-25 13:08:07 +08:00
|
|
|
RESULTS_DIR = "../results"
|
|
|
|
|
DATA_DIR = "../data"
|
2025-05-20 16:59:17 +08:00
|
|
|
|
|
|
|
|
|
2025-06-25 13:08:07 +08:00
|
|
|
class Storage:
|
|
|
|
|
"""Unified storage interface for data and results operations
|
|
|
|
|
|
|
|
|
|
Acts as a coordinator for DataLoader, DataSaver, and ResultFormatter components,
|
|
|
|
|
maintaining backward compatibility while providing a clean separation of concerns.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, logging=None, results_dir=RESULTS_DIR, data_dir=DATA_DIR):
|
|
|
|
|
"""Initialize storage with component instances
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
logging: Optional logging instance
|
|
|
|
|
results_dir: Directory for results files
|
|
|
|
|
data_dir: Directory for data files
|
|
|
|
|
"""
|
2025-05-20 16:59:17 +08:00
|
|
|
self.results_dir = results_dir
|
|
|
|
|
self.data_dir = data_dir
|
|
|
|
|
self.logging = logging
|
|
|
|
|
|
|
|
|
|
# Create directories if they don't exist
|
|
|
|
|
os.makedirs(self.results_dir, exist_ok=True)
|
|
|
|
|
os.makedirs(self.data_dir, exist_ok=True)
|
|
|
|
|
|
2025-06-25 13:08:07 +08:00
|
|
|
# Initialize component instances
|
|
|
|
|
self.data_loader = DataLoader(data_dir, logging)
|
|
|
|
|
self.data_saver = DataSaver(data_dir, logging)
|
|
|
|
|
self.result_formatter = ResultFormatter(results_dir, logging)
|
|
|
|
|
|
|
|
|
|
def load_data(self, file_path: str, start_date: Union[str, pd.Timestamp],
|
|
|
|
|
stop_date: Union[str, pd.Timestamp]) -> pd.DataFrame:
|
2025-05-20 16:59:17 +08:00
|
|
|
"""Load data with optimized dtypes and filtering, supporting CSV and JSON input
|
2025-06-25 13:08:07 +08:00
|
|
|
|
2025-05-20 16:59:17 +08:00
|
|
|
Args:
|
|
|
|
|
file_path: path to the data file
|
2025-06-25 13:08:07 +08:00
|
|
|
start_date: start date (string or datetime-like)
|
|
|
|
|
stop_date: stop date (string or datetime-like)
|
|
|
|
|
|
2025-05-20 16:59:17 +08:00
|
|
|
Returns:
|
2025-06-25 13:08:07 +08:00
|
|
|
pandas DataFrame with timestamp index
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
DataLoadingError: If data loading fails
|
2025-05-20 16:59:17 +08:00
|
|
|
"""
|
2025-06-25 13:08:07 +08:00
|
|
|
return self.data_loader.load_data(file_path, start_date, stop_date)
|
2025-05-20 18:28:53 +08:00
|
|
|
|
2025-06-25 13:08:07 +08:00
|
|
|
def save_data(self, data: pd.DataFrame, file_path: str) -> None:
|
|
|
|
|
"""Save processed data to a CSV file
|
|
|
|
|
|
2025-05-20 18:28:53 +08:00
|
|
|
Args:
|
2025-06-25 13:08:07 +08:00
|
|
|
data: DataFrame to save
|
|
|
|
|
file_path: path to the data file relative to the data_dir
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
DataSavingError: If saving fails
|
2025-05-20 18:28:53 +08:00
|
|
|
"""
|
2025-06-25 13:08:07 +08:00
|
|
|
self.data_saver.save_data(data, file_path)
|
2025-05-20 18:28:53 +08:00
|
|
|
|
2025-06-25 13:08:07 +08:00
|
|
|
def format_row(self, row: Dict[str, Any]) -> Dict[str, str]:
|
2025-05-20 16:59:17 +08:00
|
|
|
"""Format a row for a combined results CSV file
|
2025-06-25 13:08:07 +08:00
|
|
|
|
2025-05-20 16:59:17 +08:00
|
|
|
Args:
|
2025-06-25 13:08:07 +08:00
|
|
|
row: Dictionary containing row data
|
|
|
|
|
|
2025-05-20 16:59:17 +08:00
|
|
|
Returns:
|
2025-06-25 13:08:07 +08:00
|
|
|
Dictionary with formatted values
|
2025-05-20 16:59:17 +08:00
|
|
|
"""
|
2025-06-25 13:08:07 +08:00
|
|
|
return self.result_formatter.format_row(row)
|
2025-05-20 16:59:17 +08:00
|
|
|
|
2025-06-25 13:08:07 +08:00
|
|
|
def write_results_chunk(self, filename: str, fieldnames: List[str],
|
|
|
|
|
rows: List[Dict], write_header: bool = False,
|
|
|
|
|
initial_usd: Optional[float] = None) -> None:
|
2025-05-20 16:59:17 +08:00
|
|
|
"""Write a chunk of results to a CSV file
|
2025-06-25 13:08:07 +08:00
|
|
|
|
2025-05-20 16:59:17 +08:00
|
|
|
Args:
|
|
|
|
|
filename: filename to write to
|
|
|
|
|
fieldnames: list of fieldnames
|
|
|
|
|
rows: list of rows
|
|
|
|
|
write_header: whether to write the header
|
2025-06-25 13:08:07 +08:00
|
|
|
initial_usd: initial USD value for header comment
|
2025-05-20 16:59:17 +08:00
|
|
|
"""
|
2025-06-25 13:08:07 +08:00
|
|
|
self.result_formatter.write_results_chunk(
|
|
|
|
|
filename, fieldnames, rows, write_header, initial_usd
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def write_backtest_results(self, filename: str, fieldnames: List[str],
|
|
|
|
|
rows: List[Dict], metadata_lines: Optional[List[str]] = None) -> str:
|
|
|
|
|
"""Write combined backtest results to a CSV file
|
2025-05-20 16:59:17 +08:00
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
filename: filename to write to
|
|
|
|
|
fieldnames: list of fieldnames
|
2025-06-25 13:08:07 +08:00
|
|
|
rows: list of result dictionaries
|
2025-05-21 17:03:34 +08:00
|
|
|
metadata_lines: optional list of strings to write as header comments
|
2025-06-25 13:08:07 +08:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Full path to the written file
|
2025-05-20 16:59:17 +08:00
|
|
|
"""
|
2025-06-25 13:08:07 +08:00
|
|
|
return self.result_formatter.write_backtest_results(
|
|
|
|
|
filename, fieldnames, rows, metadata_lines
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def write_trades(self, all_trade_rows: List[Dict], trades_fieldnames: List[str]) -> None:
|
|
|
|
|
"""Write trades to separate CSV files grouped by timeframe and stop loss
|
|
|
|
|
|
2025-05-20 16:59:17 +08:00
|
|
|
Args:
|
2025-06-25 13:08:07 +08:00
|
|
|
all_trade_rows: list of trade dictionaries
|
2025-05-20 16:59:17 +08:00
|
|
|
trades_fieldnames: list of trade fieldnames
|
|
|
|
|
"""
|
2025-06-25 13:08:07 +08:00
|
|
|
self.result_formatter.write_trades(all_trade_rows, trades_fieldnames)
|