Manage output files in less-janky fashion.

This commit is contained in:
sam
2024-11-17 17:14:44 -08:00
parent 7211ed1723
commit d2ac5749dc
2 changed files with 108 additions and 33 deletions
+2 -8
View File
@@ -1,11 +1,5 @@
run *name: run *name:
python3 ./model.py python3 ./model.py -n "{{ name }}"
@if [[ ! -z "{{ name }}" ]]; then \
OUT_DIR="output/{{ name }}"; \
rm -rf "${OUT_DIR}"; \
mkdir -p "${OUT_DIR}"; \
mv bitcoin_*.png bitcoin_*.txt "${OUT_DIR}"; \
fi
fmt: fmt:
black ./*.py black ./*.py
@@ -14,4 +8,4 @@ lint:
ruff check ./model.py ruff check ./model.py
clean: clean:
rm -f bitcoin_*.png bitcoin_*.txt rm -rf output
+106 -25
View File
@@ -1,10 +1,58 @@
import argparse
import pandas as pd import pandas as pd
import numpy as np import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import os
import seaborn as sns import seaborn as sns
import shutil
import warnings import warnings
from datetime import timedelta from datetime import timedelta
from multiprocessing import Pool from multiprocessing import Pool
from typing import BinaryIO
# Output
class Output:
"""
Output ensures result files get written to the right directory.
Example:
output = Output("output", "test-1")
with output.create("summary.txt") as f:
# path to f is "output/test-1/summary.txt"
"""
out_dir: str
def __init__(self, base_dir: str, name: str):
"""
Initialize the output manager. This will both fully delete any existing
output directory, and then create an empty directory for the output.
Args:
base_dir: The root directory for outputs
name: The subdir of the root, where the results files go
"""
self.out_dir = os.path.join(base_dir, name)
shutil.rmtree(self.out_dir, ignore_errors=True)
os.makedirs(self.out_dir)
def create(self, filename) -> BinaryIO:
"""
Create a new file for writing in the output directory.
"""
full_path = self.named(filename)
f = open(full_path, "w")
return f
def named(self, filename) -> str:
"""
Get the full path within the output directory for a named results file.
"""
full_path = os.path.join(self.out_dir, filename)
return full_path
# Utility functions # Utility functions
@@ -640,7 +688,7 @@ def analyze_bitcoin_prices(csv_path):
# Main plotting functions # Main plotting functions
def create_plots(df, start=None, end=None, project_days=365): def create_plots(df, output: Output, start=None, end=None, project_days=365):
""" """
Create enhanced plots including market maturity visualization. Create enhanced plots including market maturity visualization.
""" """
@@ -832,7 +880,9 @@ def create_plots(df, start=None, end=None, project_days=365):
# Save the plot # Save the plot
start_str = start if start else plot_df["Date"].min().strftime("%Y-%m-%d") start_str = start if start else plot_df["Date"].min().strftime("%Y-%m-%d")
end_str = end if end else plot_df["Date"].max().strftime("%Y-%m-%d") end_str = end if end else plot_df["Date"].max().strftime("%Y-%m-%d")
filename = f"bitcoin_analysis_{start_str}_to_{end_str}_with_projections.png" filename = output.named(
f"bitcoin_analysis_{start_str}_to_{end_str}_with_projections.png"
)
# Use tight_layout with adjusted parameters # Use tight_layout with adjusted parameters
plt.tight_layout(pad=2.0) plt.tight_layout(pad=2.0)
@@ -842,7 +892,7 @@ def create_plots(df, start=None, end=None, project_days=365):
return projections return projections
def visualize_cycle_patterns(df, cycle_returns, cycle_volatility): def visualize_cycle_patterns(df, output: Output, cycle_returns, cycle_volatility):
""" """
Create enhanced visualization of Bitcoin's behavior across halving cycles. Create enhanced visualization of Bitcoin's behavior across halving cycles.
""" """
@@ -1041,12 +1091,17 @@ def visualize_cycle_patterns(df, cycle_returns, cycle_volatility):
plt.tight_layout() plt.tight_layout()
# Save the plot # Save the plot
plt.savefig("bitcoin_cycle_patterns.png", dpi=300, bbox_inches="tight") filename = output.named("bitcoin_cycle_patterns.png")
plt.savefig(filename, dpi=300, bbox_inches="tight")
plt.close() plt.close()
def create_backtest_plot( def create_backtest_plot(
df, backtest_date="2020-05-11", start_date="2012-11-28", project_days=1650 df,
output: Output,
backtest_date="2020-05-11",
start_date="2012-11-28",
project_days=1650,
): ):
""" """
Create a plot comparing actual price history against model projections from a historical date. Create a plot comparing actual price history against model projections from a historical date.
@@ -1246,7 +1301,9 @@ def create_backtest_plot(
# Adjust layout and save # Adjust layout and save
plt.tight_layout() plt.tight_layout()
filename = f'bitcoin_backtest_{start_date.strftime("%Y%m%d")}_to_{backtest_date.strftime("%Y%m%d")}.png' filename = output.named(
f'bitcoin_backtest_{start_date.strftime("%Y%m%d")}_to_{backtest_date.strftime("%Y%m%d")}.png'
)
plt.savefig(filename, dpi=300, bbox_inches="tight") plt.savefig(filename, dpi=300, bbox_inches="tight")
plt.close() plt.close()
@@ -1254,11 +1311,11 @@ def create_backtest_plot(
def run_projection(args): def run_projection(args):
df, start = args df, start, output = args
_ = create_plots(df, start=start, project_days=365 * 4) _ = create_plots(df, output, start=start, project_days=365 * 4)
def run_projections(df): def run_projections(df, output: Output):
# Create main projection # Create main projection
projection_starts = [ projection_starts = [
"2011-01-01", "2011-01-01",
@@ -1268,7 +1325,7 @@ def run_projections(df):
"2015-01-01", "2015-01-01",
"2016-07-09", "2016-07-09",
] ]
args = [(df, start) for start in projection_starts] args = [(df, start, output) for start in projection_starts]
with Pool() as pool: with Pool() as pool:
pool.map(run_projection, args) pool.map(run_projection, args)
@@ -1281,13 +1338,13 @@ def run_single_backtest(args):
Args: Args:
args: tuple of (params dict, DataFrame) args: tuple of (params dict, DataFrame)
""" """
params, df = args params, df, output = args
try: try:
# Create a copy of params without the description # Create a copy of params without the description
backtest_params = params.copy() backtest_params = params.copy()
backtest_params.pop("description", None) backtest_params.pop("description", None)
projections, metrics = create_backtest_plot(df, **backtest_params) projections, metrics = create_backtest_plot(df, output, **backtest_params)
# Ensure metrics has all required keys with default values # Ensure metrics has all required keys with default values
if metrics is None: if metrics is None:
@@ -1317,7 +1374,9 @@ def run_single_backtest(args):
return {"params": params, "error": str(e), "success": False} return {"params": params, "error": str(e), "success": False}
def run_systematic_backtests(df, validation_years=2, min_training_years=8): def run_systematic_backtests(
df, output: Output, validation_years=2, min_training_years=8
):
""" """
Run a comprehensive suite of backtests with consistent validation periods. Run a comprehensive suite of backtests with consistent validation periods.
Uses sliding windows for both start and end dates. Uses sliding windows for both start and end dates.
@@ -1463,7 +1522,7 @@ def run_systematic_backtests(df, validation_years=2, min_training_years=8):
print(f"- {period['description']}") print(f"- {period['description']}")
# Create args tuples with params and DataFrame # Create args tuples with params and DataFrame
args = [(params, df) for params in unique_periods] args = [(params, df, output) for params in unique_periods]
# Use multiprocessing # Use multiprocessing
with Pool() as pool: with Pool() as pool:
@@ -1517,7 +1576,7 @@ def run_systematic_backtests(df, validation_years=2, min_training_years=8):
stress_metrics = calculate_category_metrics(stress_periods_results) stress_metrics = calculate_category_metrics(stress_periods_results)
# Write detailed results # Write detailed results
with open("bitcoin_backtest_results_summary.txt", "w") as f: with output.create("bitcoin_backtest_results_summary.txt") as f:
f.write("Systematic Backtest Results\n") f.write("Systematic Backtest Results\n")
f.write("==========================\n\n") f.write("==========================\n\n")
@@ -1601,20 +1660,38 @@ def run_systematic_backtests(df, validation_years=2, min_training_years=8):
) )
# if __name__ == "__main__": # CLI
# analysis, df = analyze_bitcoin_prices("prices.csv")
# procs = []
#
# for proc in procs:
# proc.join()
#
if __name__ == "__main__": def get_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="model",
description="Bitcoin price model",
)
parser.add_argument(
"-o",
"--output",
help="output base directory",
default="./output",
)
parser.add_argument(
"-n",
"--name",
help="subdir of output base directory",
default="baseline",
)
return parser.parse_args()
def main():
args = get_args()
global output
output = Output(args.output, args.name)
analysis, df = analyze_bitcoin_prices("prices.csv") analysis, df = analyze_bitcoin_prices("prices.csv")
run_projections(df) run_projections(df, output)
normal_metrics, stress_metrics, normal_results, stress_results, failed_tests = ( normal_metrics, stress_metrics, normal_results, stress_results, failed_tests = (
run_systematic_backtests(df) run_systematic_backtests(df, output)
) )
print("\nAggregate Metrics:") print("\nAggregate Metrics:")
@@ -1631,3 +1708,7 @@ if __name__ == "__main__":
for test in failed_tests: for test in failed_tests:
print(f"Period: {test['params']['description']}") print(f"Period: {test['params']['description']}")
print(f"Error: {test['error']}\n") print(f"Error: {test['error']}\n")
if __name__ == "__main__":
main()