File size: 9,072 Bytes
013874c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Build the trifecta comparison PDF for 2026-08-10 from _rerun_summary.json."""
from __future__ import annotations

import json
from datetime import datetime, timezone
from pathlib import Path

from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm
from reportlab.platypus import (
    SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, HRFlowable,
)

BASE = Path(__file__).resolve().parent
S = json.loads((BASE / "data" / "predictions" / "_rerun_summary.json").read_text())

MODELS = ["Model A (default)", "Variant B (form-heavy)", "Variant C (place-heavy)"]
NEON = colors.HexColor("#16C47F")
DARK = colors.HexColor("#0F172A")
MUTED = colors.HexColor("#64748B")
LIGHT = colors.HexColor("#F1F5F9")

styles = getSampleStyleSheet()
H1 = ParagraphStyle("H1", parent=styles["Title"], fontSize=22, textColor=DARK, spaceAfter=2, leading=26)
SUB = ParagraphStyle("SUB", parent=styles["Normal"], fontSize=10, textColor=MUTED, spaceAfter=10)
H2 = ParagraphStyle("H2", parent=styles["Heading2"], fontSize=14, textColor=DARK, spaceBefore=10, spaceAfter=6)
BODY = ParagraphStyle("BODY", parent=styles["Normal"], fontSize=9.5, textColor=DARK, leading=13)
SMALL = ParagraphStyle("SMALL", parent=styles["Normal"], fontSize=8, textColor=MUTED, leading=10)
CELL = ParagraphStyle("CELL", parent=styles["Normal"], fontSize=8.5, textColor=DARK, leading=11)
CELLH = ParagraphStyle("CELLH", parent=styles["Normal"], fontSize=8.5, textColor=colors.white, leading=11)


def pct(a, b):
    return f"{100*a/b:.0f}%" if b else "0%"


def build():
    out = BASE / "data" / "pdfs" / "trifecta-comparison-2026-08-10.pdf"
    doc = SimpleDocTemplate(str(out), pagesize=A4, title="Trifecta-Bro Comparison 2026-08-10",
                            author="Trifecta-Bro", leftMargin=16*mm, rightMargin=16*mm,
                            topMargin=16*mm, bottomMargin=14*mm)
    story = []

    story.append(Paragraph("Trifecta-Bro — Prediction vs Result Comparison", H1))
    story.append(Paragraph(
        f"Date 2026-08-10  |  Tracks: Dubbo, Kilcoy, Nowra  |  "
        f"Generated {datetime.now(timezone.utc):%Y-%m-%d %H:%M UTC}  |  "
        f"Engine: open-source TrifectaPredictor (LM Studio model v1 has no weights loaded)", SUB))
    story.append(HRFlowable(width="100%", thickness=1.2, color=NEON, spaceAfter=8))

    # Scorecard table
    story.append(Paragraph("Scorecard — 20 scorable races (Dubbo R7 excluded: race time only)", H2))
    head = ["Metric", "Model A (default)", "Variant B (form)", "Variant C (place)"]
    rows = [head]
    def r(metric, key, denom_key):
        def g(m):
            d = S["results"][m]
            return f"{d[key]}/{d[denom_key]} ({pct(d[key], d[denom_key])})"
        rows.append([metric, g(MODELS[0]), g(MODELS[1]), g(MODELS[2])])
    def r1(metric, key):
        rows.append([metric, str(S["results"][MODELS[0]][key]),
                     str(S["results"][MODELS[1]][key]), str(S["results"][MODELS[2]][key])])
    r("Exact trifecta hit", "exact", "races")
    r("Any box-of-3 (any order)", "box", "races")
    r("Winner picked", "winner", "races")
    r("Winner inside predicted top-3", "win_in_top3", "n_all")
    r("Full 3 in predicted top-3", "full3_in_top3", "races3")
    r("2-of-3 in predicted top-3", "two3_in_top3", "races3")
    r1("Quinella hits (2-horse results)", "quinella")

    t = Table(rows, colWidths=[58*mm, 38*mm, 38*mm, 38*mm])
    t.setStyle(TableStyle([
        ("BACKGROUND", (0,0), (-1,0), DARK),
        ("TEXTCOLOR", (0,0), (-1,0), colors.white),
        ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
        ("FONTSIZE", (0,0), (-1,-1), 8.5),
        ("ROWBACKGROUNDS", (0,1), (-1,-1), [colors.white, LIGHT]),
        ("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#CBD5E1")),
        ("ALIGN", (1,0), (-1,-1), "CENTER"),
        ("VALIGN", (0,0), (-1,-1), "MIDDLE"),
        ("TOPPADDING", (0,0), (-1,-1), 3),
        ("BOTTOMPADDING", (0,0), (-1,-1), 3),
        # highlight Model A column winner/exact rows
        ("BACKGROUND", (1,1), (1,3), colors.HexColor("#ECFDF5")),
    ]))
    story.append(t)
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "Model A (default weights) posted the best result: 4 winners and the lone exact trifecta. "
        "Neither alternative weighting improved the strike rate — Variant B and C each picked 3 winners.", SMALL))

    # Race-by-race
    story.append(Paragraph("Race-by-Race — actual vs each model's primary pick", H2))
    det = {m: {f"{d[0]}_R{d[1]}": d[3]["primary"] for d in S["details"][m] if d[3]} for m in MODELS}
    actual = {
        ("Dubbo","1"):"1-7-9",("Dubbo","2"):"8-3-6",("Dubbo","3"):"5-1-4",("Dubbo","4"):"4-9-6",
        ("Dubbo","5"):"2-8",("Dubbo","6"):"13-11-9",("Dubbo","7"):"(time)",
        ("Kilcoy","1"):"7-8",("Kilcoy","2"):"6-9-4",("Kilcoy","3"):"2-4",("Kilcoy","4"):"6-1-4",
        ("Kilcoy","5"):"5-11-9",("Kilcoy","6"):"10-4",("Kilcoy","7"):"15-9-1",
        ("Nowra","1"):"6-8-1",("Nowra","2"):"9-11-1",("Nowra","3"):"3-5-1",("Nowra","4"):"4-11-2",
        ("Nowra","5"):"8-11-3",("Nowra","6"):"7-1-6",("Nowra","7"):"2-3-9",
    }
    keys = sorted(actual.keys(), key=lambda k: (k[0], int(k[1])))
    head2 = ["Track", "R", "Actual", "A", "B", "C", "Hit"]
    rdata = [head2]
    for trk, rn in keys:
        a = actual[(trk, rn)]
        pa = det[MODELS[0]].get(f"{trk}_R{rn}", "--")
        pb = det[MODELS[1]].get(f"{trk}_R{rn}", "--")
        pc = det[MODELS[2]].get(f"{trk}_R{rn}", "--")
        hit = ""
        if a != "(time)":
            av = [int(x) for x in a.split("-")]
            for m, pk in ((MODELS[0],pa),(MODELS[1],pb),(MODELS[2],pc)):
                if pk != "--":
                    pv = [int(x) for x in pk.split("-")]
                    if pv[0] == av[0]:
                        hit += "W" if m==MODELS[0] else ("B" if m==MODELS[1] else "C")
        rdata.append([trk, rn, a, pa, pb, pc, hit or "-"])
    t2 = Table(rdata, colWidths=[20*mm, 8*mm, 20*mm, 22*mm, 22*mm, 22*mm, 22*mm], repeatRows=1)
    ts = [
        ("BACKGROUND", (0,0), (-1,0), DARK),
        ("TEXTCOLOR", (0,0), (-1,0), colors.white),
        ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
        ("FONTSIZE", (0,0), (-1,-1), 8),
        ("ROWBACKGROUNDS", (0,1), (-1,-1), [colors.white, LIGHT]),
        ("GRID", (0,0), (-1,-1), 0.3, colors.HexColor("#CBD5E1")),
        ("ALIGN", (1,0), (-1,-1), "CENTER"),
        ("TOPPADDING", (0,0), (-1,-1), 2.2),
        ("BOTTOMPADDING", (0,0), (-1,-1), 2.2),
    ]
    # shade hit cells
    for i, (trk, rn) in enumerate(keys, start=1):
        a = actual[(trk, rn)]
        if a == "(time)":
            ts.append(("TEXTCOLOR", (6,i), (6,i), MUTED))
    t2.setStyle(TableStyle(ts))
    story.append(t2)
    story.append(Spacer(1, 8))

    # Read-through
    story.append(Paragraph("Read-Through", H2))
    for line in [
        "1. Model A nailed Dubbo R1 outright (exact trifecta 1-7-9) and found 4 winners overall — "
        "the strongest single-model run of the day.",
        "2. Re-weighting the scorer toward recent form (Variant B) or toward place%+prize (Variant C) "
        "did NOT lift the strike rate. The default weights already performed best.",
        "3. The shared weakness is the SAME across all three: across the 16 complete 3-place results, "
        "ZERO had all three actual trifecta runners sitting inside the predicted top-3, and only 4/16 "
        "had even two of three. The models rarely select the correct SET of runners — they are "
        "better at the front (35-45% of winners in the top-3) than at nailing the exotics.",
        "4. This is a data/feature limitation of the open-source heuristic (no weights loaded for the "
        "LM Studio model v1), not a tuning problem. Meaningful gains need either the trained "
        "Brettapps/trifecta-bro/v1 weights, or richer inputs (speed ratings, sectional times, "
        "jockey/trainer strike rates, track bias).",
        "5. Caveats: several pasted results are 2 numbers only (Dubbo R5; Kilcoy R1/R3/R6) and were "
        "judged as quinella/winner only. Dubbo R7 carried a race time with no finishing order.",
    ]:
        story.append(Paragraph("• " + line, BODY))
        story.append(Spacer(1, 3))

    story.append(Spacer(1, 8))
    story.append(HRFlowable(width="100%", thickness=0.6, color=MUTED, spaceAfter=4))
    story.append(Paragraph(
        "Method: predictions regenerated from the same FormFav runner data in predictions-2026-08-10.json. "
        "Variant B = form x1.6, place x0.8, distance x1.2, barrier x0.3, prize x0.5. "
        "Variant C = place x1.7, prize x1.3, track x1.2, condition x1.2, form x0.7, barrier x0.5. "
        "Honest backtest — only the scoring weights differ.", SMALL))

    doc.build(story)
    print(f"wrote {out} ({out.stat().st_size} bytes)")


if __name__ == "__main__":
    build()