Manage output files in less-janky fashion.
This commit is contained in:
@@ -1,11 +1,5 @@
|
||||
run *name:
|
||||
python3 ./model.py
|
||||
@if [[ ! -z "{{ name }}" ]]; then \
|
||||
OUT_DIR="output/{{ name }}"; \
|
||||
rm -rf "${OUT_DIR}"; \
|
||||
mkdir -p "${OUT_DIR}"; \
|
||||
mv bitcoin_*.png bitcoin_*.txt "${OUT_DIR}"; \
|
||||
fi
|
||||
python3 ./model.py -n "{{ name }}"
|
||||
|
||||
fmt:
|
||||
black ./*.py
|
||||
@@ -14,4 +8,4 @@ lint:
|
||||
ruff check ./model.py
|
||||
|
||||
clean:
|
||||
rm -f bitcoin_*.png bitcoin_*.txt
|
||||
rm -rf output
|
||||
|
||||
@@ -1,10 +1,58 @@
|
||||
import argparse
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import os
|
||||
import seaborn as sns
|
||||
import shutil
|
||||
import warnings
|
||||
from datetime import timedelta
|
||||
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
|
||||
@@ -640,7 +688,7 @@ def analyze_bitcoin_prices(csv_path):
|
||||
# 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.
|
||||
"""
|
||||
@@ -832,7 +880,9 @@ def create_plots(df, start=None, end=None, project_days=365):
|
||||
# Save the plot
|
||||
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")
|
||||
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
|
||||
plt.tight_layout(pad=2.0)
|
||||
@@ -842,7 +892,7 @@ def create_plots(df, start=None, end=None, project_days=365):
|
||||
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.
|
||||
"""
|
||||
@@ -1041,12 +1091,17 @@ def visualize_cycle_patterns(df, cycle_returns, cycle_volatility):
|
||||
plt.tight_layout()
|
||||
|
||||
# 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()
|
||||
|
||||
|
||||
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.
|
||||
@@ -1246,7 +1301,9 @@ def create_backtest_plot(
|
||||
|
||||
# Adjust layout and save
|
||||
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.close()
|
||||
|
||||
@@ -1254,11 +1311,11 @@ def create_backtest_plot(
|
||||
|
||||
|
||||
def run_projection(args):
|
||||
df, start = args
|
||||
_ = create_plots(df, start=start, project_days=365 * 4)
|
||||
df, start, output = args
|
||||
_ = create_plots(df, output, start=start, project_days=365 * 4)
|
||||
|
||||
|
||||
def run_projections(df):
|
||||
def run_projections(df, output: Output):
|
||||
# Create main projection
|
||||
projection_starts = [
|
||||
"2011-01-01",
|
||||
@@ -1268,7 +1325,7 @@ def run_projections(df):
|
||||
"2015-01-01",
|
||||
"2016-07-09",
|
||||
]
|
||||
args = [(df, start) for start in projection_starts]
|
||||
args = [(df, start, output) for start in projection_starts]
|
||||
with Pool() as pool:
|
||||
pool.map(run_projection, args)
|
||||
|
||||
@@ -1281,13 +1338,13 @@ def run_single_backtest(args):
|
||||
Args:
|
||||
args: tuple of (params dict, DataFrame)
|
||||
"""
|
||||
params, df = args
|
||||
params, df, output = args
|
||||
try:
|
||||
# Create a copy of params without the description
|
||||
backtest_params = params.copy()
|
||||
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
|
||||
if metrics is None:
|
||||
@@ -1317,7 +1374,9 @@ def run_single_backtest(args):
|
||||
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.
|
||||
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']}")
|
||||
|
||||
# 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
|
||||
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)
|
||||
|
||||
# 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("==========================\n\n")
|
||||
|
||||
@@ -1601,20 +1660,38 @@ def run_systematic_backtests(df, validation_years=2, min_training_years=8):
|
||||
)
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# analysis, df = analyze_bitcoin_prices("prices.csv")
|
||||
# procs = []
|
||||
#
|
||||
# for proc in procs:
|
||||
# proc.join()
|
||||
#
|
||||
# CLI
|
||||
|
||||
|
||||
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")
|
||||
run_projections(df)
|
||||
run_projections(df, output)
|
||||
normal_metrics, stress_metrics, normal_results, stress_results, failed_tests = (
|
||||
run_systematic_backtests(df)
|
||||
run_systematic_backtests(df, output)
|
||||
)
|
||||
|
||||
print("\nAggregate Metrics:")
|
||||
@@ -1631,3 +1708,7 @@ if __name__ == "__main__":
|
||||
for test in failed_tests:
|
||||
print(f"Period: {test['params']['description']}")
|
||||
print(f"Error: {test['error']}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user