feat(calibration): finalize Step 2 Risk & Stops with inline PDF reports and visual validation
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user