feat(calibration): finalize Step 2 Risk & Stops with inline PDF reports and visual validation

This commit is contained in:
DaM
2026-02-13 20:56:34 +01:00
parent 44667df3dd
commit f4f4e8e5be
20 changed files with 184 additions and 1925 deletions

View File

@@ -24,7 +24,12 @@ from reportlab.platypus import PageBreak
# HELPERS
# ============================================================
def _create_stop_histogram(stop_distances):
def _create_stop_histogram(
stop_distances,
p50=None,
p90=None,
p99=None,
):
fig, ax = plt.subplots(figsize=(6, 4))
@@ -34,6 +39,16 @@ def _create_stop_histogram(stop_distances):
alpha=0.7,
)
# Percentile lines
if p50 is not None:
ax.axvline(p50 * 100, linestyle=":", linewidth=1)
if p90 is not None:
ax.axvline(p90 * 100, linestyle=":", linewidth=1)
if p99 is not None:
ax.axvline(p99 * 100, linestyle=":", linewidth=1)
ax.set_title("Stop Distance Distribution")
ax.set_xlabel("Stop Distance (%)")
ax.set_ylabel("Frequency")
@@ -46,18 +61,30 @@ def _create_stop_histogram(stop_distances):
return buf
def _create_position_size_plot(timestamps, position_sizes):
# Align and be robust
def _create_position_size_plot(
timestamps,
position_sizes,
*,
max_position_fraction=None,
):
ts, ps = _align_xy(timestamps, position_sizes)
if not ps:
return None
x = list(range(len(ps))) # robust axis (avoid matplotlib categorical date issues)
x = list(range(len(ps)))
y = [p * 100 for p in ps]
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x, y, linewidth=0.8)
if max_position_fraction is not None:
ax.axhline(
max_position_fraction * 100,
linestyle="--",
linewidth=1,
)
ax.set_title("Position Size Over Time")
ax.set_ylabel("Position Size (% of equity)")
ax.set_xlabel("Samples")
@@ -67,9 +94,19 @@ def _create_position_size_plot(timestamps, position_sizes):
plt.savefig(buf, format="png", dpi=150)
plt.close(fig)
buf.seek(0)
return buf
def _create_effective_risk_plot(timestamps, effective_risks):
def _create_effective_risk_plot(
timestamps,
effective_risks,
*,
risk_target=None,
p50=None,
p90=None,
p99=None,
):
ts, er = _align_xy(timestamps, effective_risks)
if not er:
return None
@@ -78,8 +115,18 @@ def _create_effective_risk_plot(timestamps, effective_risks):
y = [r * 100 for r in er]
fig, ax = plt.subplots(figsize=(6, 4))
# Main curve
ax.plot(x, y, linewidth=0.8)
# Risk target line
if risk_target is not None:
ax.axhline(
risk_target * 100,
linestyle="--",
linewidth=1,
)
ax.set_title("Effective Risk Over Time")
ax.set_ylabel("Effective Risk (%)")
ax.set_xlabel("Samples")
@@ -89,6 +136,7 @@ def _create_effective_risk_plot(timestamps, effective_risks):
plt.savefig(buf, format="png", dpi=150)
plt.close(fig)
buf.seek(0)
return buf
def _align_xy(x, y):
@@ -386,7 +434,18 @@ def generate_risk_report_pdf(
story.append(Paragraph("6. Stop Distance Distribution", heading_style))
story.append(Spacer(1, 8))
img_buffer = _create_stop_histogram(stop_distances)
stop_metrics = (
results.get("checks", {})
.get("stop_sanity", {})
.get("metrics", {})
)
img_buffer = _create_stop_histogram(
stop_distances,
p50=stop_metrics.get("p50"),
p90=stop_metrics.get("p90"),
p99=stop_metrics.get("p99"),
)
img = Image(img_buffer, width=400, height=250)
story.append(img)
@@ -405,7 +464,12 @@ def generate_risk_report_pdf(
story.append(Paragraph("7. Position Size Over Time", heading_style))
story.append(Spacer(1, 8))
img_buffer = _create_position_size_plot(timestamps, position_sizes)
img_buffer = _create_position_size_plot(
timestamps,
position_sizes,
max_position_fraction=config.get("Max position fraction (%)", 0) / 100,
)
if img_buffer:
img = Image(img_buffer, width=400, height=250)
story.append(img)
@@ -422,7 +486,14 @@ def generate_risk_report_pdf(
story.append(Paragraph("8. Effective Risk Over Time", heading_style))
story.append(Spacer(1, 8))
img_buffer = _create_effective_risk_plot(timestamps, effective_risks)
risk_target = config.get("Risk per trade (%)", 0) / 100
img_buffer = _create_effective_risk_plot(
timestamps,
effective_risks,
risk_target=risk_target,
)
if img_buffer:
img = Image(img_buffer, width=400, height=250)
story.append(img)