Finding True Value in Fantasy Baseball — Implementation Blueprint (5/6)

January 13, 2026

34 views
Finding True Value in Fantasy Baseball — Implementation Blueprint (5/6)

Part 5: The Implementation Blueprint

TRP — True Relative Price: True Value. Market Pricing. No Noise.

Parts 1–4 established the theory: replacement-level baselines, Z-score normalization, iterative tier refinement, and production-weighted dollar conversion. This part translates that theory into a buildable system.

What follows is a language-agnostic architectural blueprint. The data structures, core functions, and control flow are specified precisely enough that you could code this yourself or hand this document to an LLM and get a working implementation in one shot.

Photo by Mohammad Rahmani on Unsplash

Inputs

The system requires three input files:

1. Projections File (projections.csv)

Player-level projections with the following schema:

1player_id: string (unique identifier)
2name: string
3team: string
4positions: string[] (e.g., ["C", "1B"] or ["SP"])
5pa: integer (plate appearances, hitters only)
6ab: integer (at-bats, hitters only)
7r: float (runs)
8hr: float (home runs)
9rbi: float (runs batted in)
10sb: float (stolen bases)
11obp: float (on-base percentage)
12slg: float (slugging percentage)
13ip: float (innings pitched, pitchers only)
14era: float (earned run average)
15whip: float (walks + hits per inning pitched)
16k9: float (strikeouts per 9 innings)
17qs: integer (quality starts, SP only)
18svhd: integer (saves + holds, RP only)
19role: string ("HITTER" | "SP" | "RP")
20wrc_plus: float (hitters only, pre-calculated)
21fip: float (pitchers only, pre-calculated)

Note: wrc_plus and fip are pre-calculated composite metrics used for initial player sorting. These come from your projection source or are calculated upstream.

2. League Settings File (league_settings.json)

1{
2 "num_teams": 12,
3 "budget_per_team": 260,
4 "bench_reserve": 5,
5 "roster_slots": {
6 "C": 1,
7 "1B": 1,
8 "2B": 1,
9 "3B": 1,
10 "SS": 1,
11 "OF": 3,
12 "UTIL": 1,
13 "SP": 5,
14 "RP": 2
15 },
16 "hitter_categories": ["R", "HR", "RBI", "SB", "OBP", "SLG"],
17 "sp_categories": ["IP", "QS", "ERA", "WHIP", "K9"],
18 "rp_categories": ["IP", "SVHD", "ERA", "WHIP", "K9"]
19}

3. Budget Configuration File (budget_config.json)

1{
2 "hitter_pitcher_split": [0.70, 0.30],
3 "sp_rp_split": [0.50, 0.50],
4 "hitter_category_weights": {
5 "OBP": 0.25,
6 "SLG": 0.25,
7 "R": 0.125,
8 "HR": 0.125,
9 "RBI": 0.125,
10 "SB": 0.125
11 },
12 "sp_category_weights": {
13 "K9": 0.40,
14 "IP": 0.15,
15 "QS": 0.15,
16 "ERA": 0.15,
17 "WHIP": 0.15
18 },
19 "rp_category_weights": {
20 "K9": 0.40,
21 "IP": 0.15,
22 "SVHD": 0.15,
23 "ERA": 0.15,
24 "WHIP": 0.15
25 },
26 "pa_weights": {
27 "C": 500,
28 "default": 600
29 },
30 "replacement_tier_pct": 0.03,
31 "min_replacement_tier_size": 3,
32 "max_iterations": 10,
33 "convergence_threshold": 0
34}

Outputs

The system produces two output files:

1. Player Valuations File (valuations.csv)

1player_id: string
2name: string
3position: string (primary position used for valuation)
4role: string ("HITTER" | "SP" | "RP")
5total_z: float
6dollar_value: float
7z_R: float (hitters only)
8z_HR: float (hitters only)
9z_RBI: float (hitters only)
10z_SB: float (hitters only)
11z_OBP: float (hitters only)
12z_SLG: float (hitters only)
13z_IP: float (pitchers only)
14z_ERA: float (pitchers only)
15z_WHIP: float (pitchers only)
16z_K9: float (pitchers only)
17z_QS: float (SP only)
18z_SVHD: float (RP only)
19dollar_R: float (hitters only)
20dollar_HR: float (hitters only)
21... (dollar value per category)
22tier: string ("ROSTERED" | "REPLACEMENT" | "BELOW_REPLACEMENT")

2. Position Summary File (position_summary.csv)

1position: string
2role: string
3rostered_count: integer
4replacement_tier_count: integer
5total_budget: float
6dollars_per_z_R: float
7dollars_per_z_HR: float
8... ($/Z for each category)
9replacement_baseline_R: float
10replacement_baseline_HR: float
11... (RLP archetype stats)

Core Data Structures

Player

1{
2 id: string
3 name: string
4 team: string
5 positions: string[]
6 role: "HITTER" | "SP" | "RP"
7 stats: {
8 // Raw projection stats
9 pa: float
10 ab: float
11 r: float
12 hr: float
13 rbi: float
14 sb: float
15 obp: float
16 slg: float
17 ip: float
18 era: float
19 whip: float
20 k9: float
21 qs: float
22 svhd: float
23 // Pre-calculated composite metrics
24 wrc_plus: float (hitters)
25 fip: float (pitchers)
26 }
27 computed: {
28 primary_position: string
29 raw_z: { [category]: float }
30 normalized_z: { [category]: float }
31 total_z: float
32 dollar_values: { [category]: float }
33 total_dollars: float
34 tier: "ROSTERED" | "REPLACEMENT" | "BELOW_REPLACEMENT"
35 }
36}

PositionPool

1{
2 position: string
3 role: "HITTER" | "SP" | "RP"
4 roster_slots: integer
5 rostered_players: Player[]
6 replacement_players: Player[]
7 rostered_tier_means: { [category]: float }
8 rostered_tier_stdevs: { [category]: float }
9 rlp_archetype: { [category]: float }
10 rlp_raw_z_avg: { [category]: float }
11 category_budgets: { [category]: float }
12 dollars_per_z: { [category]: float }
13 total_pool_z: { [category]: float }
14 production_share: { [category]: float }
15}

LeagueBudget

1{
2 total: float
3 hitter_budget: float
4 pitcher_budget: float
5 sp_budget: float
6 rp_budget: float
7 category_budgets: {
8 hitter: { [category]: float }
9 sp: { [category]: float }
10 rp: { [category]: float }
11 }
12}

Core Functions

ASSIGN_PRIMARY_POSITIONS(players, settings)

Multi-position players create a challenge: where do you value them? A player eligible at 2B and SS could anchor either position. TRP assigns each player to their most valuable position — the scarcest one where they’d be rostered.

This function processes positions from scarcest to deepest, assigning players to maximize positional value.

1ASSIGN_PRIMARY_POSITIONS(players, settings):
2 // Sort positions by scarcity (fewest roster slots first)
3 position_order = SORT_BY(settings.roster_slots, ascending)
4
5 assigned = {}
6
7 FOR each position IN position_order:
8 eligible = FILTER(players, position IN player.positions AND player.id NOT IN assigned)
9 slots = settings.roster_slots[position] * settings.num_teams
10
11 // Sort by composite metric (wRC+ for hitters, FIP for pitchers)
12 eligible = SORT_BY(eligible, composite_metric, descending)
13
14 // Assign top N players to this position
15 FOR i = 0 TO slots + (slots * 0.5): // Include replacement tier buffer
16 IF i = threshold
17 )
18
19 // Enforce minimum tier size
20 IF LENGTH(replacement_candidates) 0:
21 last_rostered_metric = pool.rostered_players[-1].composite_metric
22 threshold = last_rostered_metric * (1 - budget_config.replacement_tier_pct)
23
24 replacement_candidates = FILTER(
25 util_candidates[pool.roster_slots :],
26 composite_metric >= threshold
27 )
28
29 IF LENGTH(replacement_candidates) 0:
30 pool.dollars_per_z[category] = pool.category_budgets[category] / pool.total_pool_z[category]
31 ELSE:
32 pool.dollars_per_z[category] = 0
33
34 RETURN pools

CALC_PLAYER_DOLLARS(player, pool)

The final step: multiply each player’s normalized Z by the $/Z rate for their position-category. Sum across categories for total dollar value.

Negative Z-scores produce negative dollar contributions — a player who hurts you in a category is penalized accordingly.

1CALC_PLAYER_DOLLARS(player, pool):
2 dollar_values = {}
3
4 FOR each category IN player.computed.normalized_z:
5 z = player.computed.normalized_z[category]
6 rate = pool.dollars_per_z[category]
7 dollar_values[category] = z * rate
8
9 RETURN dollar_values

Helper Functions

IS_INVERTED(category)

1RETURN category IN ["ERA", "WHIP"]

GET_CATEGORIES(role, settings)

1IF role == "HITTER":
2 RETURN settings.hitter_categories
3ELSE IF role == "SP":
4 RETURN settings.sp_categories
5ELSE IF role == "RP":
6 RETURN settings.rp_categories

CALC_MEANS(players, field)

1values = [player.stats[field] OR player.computed[field] FOR player IN players]
2RETURN SUM(values) / LENGTH(values)

CALC_STDEVS(players, field)

1values = [player.stats[field] OR player.computed[field] FOR player IN players]
2mean = CALC_MEANS(players, field)
3variance = SUM((v - mean)^2 FOR v IN values) / LENGTH(values)
4RETURN SQRT(variance)

Validation Checks

Before outputting, validate:

  • Budget Balance: Sum of all rostered player dollars ≈ total league budget (±$1)
  • No Orphan Players: Every player with projections is assigned to exactly one position pool
  • Tier Consistency: Rostered tier size equals roster slots × num teams for each position
  • Z-Score Sanity: RLP players should have total normalized Z near 0
  • Dollar Sanity: No rostered player should have negative total dollars (below replacement should be rare)

Control Flow

Putting it all together — the complete pipeline from raw projections to dollar values:

1MAIN():
2 // Phase 1: Initialize
3 projections = LOAD_PROJECTIONS("projections.csv")
4 settings = LOAD_SETTINGS("league_settings.json")
5 budget_config = LOAD_BUDGET_CONFIG("budget_config.json")
6
7 // Phase 2: Assign primary positions (scarcity-first allocation)
8 players = ASSIGN_PRIMARY_POSITIONS(projections, settings)
9
10 // Phase 3: Split by role
11 hitters = FILTER(players, role == "HITTER")
12 pure_dh_players = FILTER(hitters, positions == ["DH"])
13 starters = FILTER(players, role == "SP")
14 relievers = FILTER(players, role == "RP")
15
16 // Phase 4: Build position pools and iterate to convergence
17 hitter_pools = BUILD_POSITION_POOLS(hitters, settings, "HITTER")
18 hitter_pools = ITERATE_TO_CONVERGENCE(hitter_pools, budget_config)
19
20 // Phase 5: Build UTIL pool from replacement-tier players + pure DHs
21 // This must happen AFTER position pools converge so we know who's below replacement
22 util_pool = BUILD_UTIL_POOL(hitter_pools, pure_dh_players, settings)
23 util_pool = ITERATE_TO_CONVERGENCE([util_pool], budget_config)[0]
24 hitter_pools.APPEND(util_pool)
25
26 // Phase 6: Build pitcher pools
27 sp_pool = BUILD_SINGLE_POOL(starters, settings, "SP")
28 sp_pool = ITERATE_TO_CONVERGENCE([sp_pool], budget_config)[0]
29
30 rp_pool = BUILD_SINGLE_POOL(relievers, settings, "RP")
31 rp_pool = ITERATE_TO_CONVERGENCE([rp_pool], budget_config)[0]
32
33 // Phase 7: Calculate league budget structure
34 league_budget = CALC_LEAGUE_BUDGET(settings, budget_config)
35
36 // Phase 8: Allocate category budgets to positions
37 hitter_pools = ALLOCATE_POSITION_BUDGETS(hitter_pools, league_budget, budget_config)
38 sp_pool = ALLOCATE_POOL_BUDGET(sp_pool, league_budget.sp_budget, budget_config.sp_category_weights)
39 rp_pool = ALLOCATE_POOL_BUDGET(rp_pool, league_budget.rp_budget, budget_config.rp_category_weights)
40
41 // Phase 9: Convert Z-scores to dollars
42 hitter_pools = CALC_DOLLARS_PER_Z(hitter_pools)
43 sp_pool = CALC_DOLLARS_PER_Z([sp_pool])[0]
44 rp_pool = CALC_DOLLARS_PER_Z([rp_pool])[0]
45
46 // Phase 10: Value each player
47 FOR each pool IN [hitter_pools..., sp_pool, rp_pool]:
48 FOR each player IN pool.rostered_players + pool.replacement_players:
49 player.computed.dollar_values = CALC_PLAYER_DOLLARS(player, pool)
50 player.computed.total_dollars = SUM(player.computed.dollar_values)
51
52 // Phase 11: Validate and normalize
53 total_allocated = SUM(all player.computed.total_dollars WHERE tier == "ROSTERED")
54 IF total_allocated != league_budget.total:
55 NORMALIZE_TO_BUDGET(all_players, league_budget.total)
56
57 // Phase 12: Output
58 WRITE_VALUATIONS("valuations.csv", all_players)
59 WRITE_POSITION_SUMMARY("position_summary.csv", all_pools)

What We’ve Built

This blueprint specifies:

  • Input/Output contracts — exactly what data goes in and comes out
  • Data structures — Player, PositionPool, and LeagueBudget objects
  • Core algorithms — iteration, Z-score calculation, budget allocation, dollar conversion
  • UTIL pool construction — collecting replacement-tier players to fill the flex slot
  • Validation checks — sanity tests before output
  • Control flow — the 12-phase pipeline from raw projections to dollar values

Hand this document to any competent developer or LLM, and they can build a working TRP implementation in their language of choice.

Part 6 extends this foundation to in-season analysis: combining current stats with rest-of-season projections to identify buy-low, sell-high, and waiver opportunities.

TRP is a valuation framework developed within the MTBL (Metaball) ecosystem. It consumes projections from any source and outputs market-calibrated player values for fantasy baseball.

0 claps