OSM
CoachKnow what to do next.

Evidence guide

Evaluation models

Structured methods for tactic and transfer evaluation.
01

Overview

These models are for manual observations and offline analysis. They must not be connected to unauthorized scraping, APIs, bots, or automatic purchases. Their lookup values are calibration priors from the reviewed material—not discovered OSM probabilities.

02

16.1 Transfer efficiency model

Simple gross-efficiency score

Gross TE = (target resale − acquisition cost)
           / (acquisition cost × adjusted holding days)

Where:

Target resale = min(live in-game maximum, player value × target multiple)
Adjusted holding days = baseline holding days / (age factor × event factor)

This score ranks capital velocity but assumes the target sale occurs. It is therefore useful for screening, not sufficient for a final purchase.

Provisional lookup table

Rating Target multiple Baseline hold
≤80 2.45×; 2.50× if age ≤21 1.5 days
81–90 2.25× 2.0 days
91–99 2.10× 3.5 days
100–109 1.90× 6.0 days
110+ 1.70× 6.0+ days; calibrate separately
Age Provisional liquidity factor
≤21 1.15
22–24 1.10
25–29 1.00
30+ 0.90
Event Provisional factor
Transfer Madness 1.40
No relevant sale event 1.00

The event factor must be replaced with observed results. An event confirms improved conditions, not a universal 40% reduction in hold time.

Risk-adjusted score

The preferred model includes failed-sale downside:

Expected profit = p × target resale
                + (1 − p) × fallback value
                − acquisition cost
                − Boss Coin cost

Risk-adjusted TE = Expected profit
                   / (acquisition cost × adjusted holding days)

Use your own 48/72-hour sale rate for p. Until enough data exists, use a conservative range and calculate pessimistic, base, and optimistic scenarios.

Provisional decision bands

Gross TE Screening label Decision rule
≥0.25 Exceptional candidate Never “auto-buy”; verify cash, floor, live cap, and risk-adjusted EV
0.15–0.249 Strong candidate Buy when squad and list capacity are safe
0.08–0.149 Acceptable Use only with spare capacity or real sporting utility
<0.08 Reject as pure flip Capital/slot time is probably better used elsewhere

These thresholds are unvalidated heuristics. A negative risk-adjusted TE always overrides a positive gross score.

Offline Python reference

class OSMTradeEvaluator:
    @staticmethod
    def resale_multiple(ovr: int, age: int) -> float:
        if ovr <= 80:
            return 2.50 if age <= 21 else 2.45
        if ovr <= 90:
            return 2.25
        if ovr <= 99:
            return 2.10
        if ovr <= 109:
            return 1.90
        return 1.70

    @staticmethod
    def baseline_hold_days(ovr: int) -> float:
        if ovr <= 80:
            return 1.5
        if ovr <= 90:
            return 2.0
        if ovr <= 99:
            return 3.5
        return 6.0

    @staticmethod
    def age_factor(age: int) -> float:
        if age <= 21:
            return 1.15
        if age <= 24:
            return 1.10
        if age <= 29:
            return 1.00
        return 0.90

    @classmethod
    def evaluate(
        cls,
        ovr: int,
        age: int,
        player_value: float,
        buy_price: float,
        live_max_price: float,
        sale_probability: float,
        fallback_value: float,
        boss_coin_cost: float = 0.0,
        transfer_madness: bool = False,
    ) -> dict:
        if buy_price <= 0 or player_value < 0 or live_max_price < 0:
            raise ValueError("Prices must be valid and buy_price must be positive")
        if not 0.0 <= sale_probability <= 1.0:
            raise ValueError("sale_probability must be between 0 and 1")

        target = min(
            live_max_price,
            player_value * cls.resale_multiple(ovr, age),
        )
        event_factor = 1.40 if transfer_madness else 1.00
        hold_days = cls.baseline_hold_days(ovr) / (
            cls.age_factor(age) * event_factor
        )

        gross_profit = target - buy_price
        gross_te = gross_profit / (buy_price * hold_days)
        expected_proceeds = (
            sale_probability * target
            + (1.0 - sale_probability) * fallback_value
        )
        expected_profit = expected_proceeds - buy_price - boss_coin_cost
        risk_te = expected_profit / (buy_price * hold_days)

        if expected_profit <= 0 or risk_te < 0.08:
            action = "PASS"
        elif risk_te < 0.15:
            action = "ACCEPTABLE"
        elif risk_te < 0.25:
            action = "STRONG CANDIDATE"
        else:
            action = "EXCEPTIONAL CANDIDATE — VERIFY MANUALLY"

        return {
            "target_resale": round(target, 2),
            "adjusted_hold_days": round(hold_days, 2),
            "gross_profit": round(gross_profit, 2),
            "expected_profit": round(expected_profit, 2),
            "gross_te": round(gross_te, 4),
            "risk_adjusted_te": round(risk_te, 4),
            "action": action,
        }
03

16.2 Tactical experiment analyzer

The following offline model calculates basic KPIs and applies sample-size confidence caps. Its confidence score measures the strength of your evidence, not match win probability.

class OSMTacticalAnalyzer:
    def __init__(self, tactic_name: str):
        self.tactic_name = tactic_name
        self.matches = []

    def log_match(
        self,
        gf: int,
        ga: int,
        shots_for: int,
        shots_against: int,
        sot_for: int,
        sot_against: int,
        possession_for: float,
        distorted: bool = False,
    ) -> None:
        self.matches.append({
            "gf": gf,
            "ga": ga,
            "shots_for": shots_for,
            "shots_against": shots_against,
            "sot_for": sot_for,
            "sot_against": sot_against,
            "possession_for": possession_for,
            "distorted": distorted,
        })

    def valid_matches(self) -> list[dict]:
        return [m for m in self.matches if not m["distorted"]]

    def kpis(self) -> dict:
        matches = self.valid_matches()
        n = len(matches)
        if n == 0:
            return {"status": "No valid matches recorded"}

        wins = sum(m["gf"] > m["ga"] for m in matches)
        draws = sum(m["gf"] == m["ga"] for m in matches)
        average = lambda key: sum(m[key] for m in matches) / n

        return {
            "sample_size": n,
            "points_per_match": round((3 * wins + draws) / n, 2),
            "goal_difference": round(
                average("gf") - average("ga"), 2
            ),
            "shot_difference": round(
                average("shots_for") - average("shots_against"), 2
            ),
            "sot_difference": round(
                average("sot_for") - average("sot_against"), 2
            ),
            "average_possession": round(average("possession_for"), 1),
        }

    def evidence_confidence(self, external_evidence: float = 15.0) -> float:
        matches = self.valid_matches()
        n = len(matches)
        if n == 0:
            return 0.0

        score = max(0.0, min(external_evidence, 20.0))
        for m in matches:
            process_edge = (
                m["shots_for"] > m["shots_against"]
                and m["sot_for"] >= m["sot_against"]
            )
            if m["gf"] > m["ga"] and process_edge:
                score += 3.0
            elif m["gf"] < m["ga"] and process_edge:
                score += 0.5
            elif m["gf"] > m["ga"] and not process_edge:
                score += 0.0
            elif m["gf"] < m["ga"] and not process_edge:
                score -= 5.0

        if n == 1:
            cap = 25.0
        elif n < 5:
            cap = 40.0
        elif n < 10:
            cap = 55.0
        elif n < 20:
            cap = 70.0
        elif n <= 30:
            cap = 85.0
        else:
            cap = 95.0

        return round(min(cap, max(0.0, score)), 1)
04

16.3 AI diagnostic rules

Use suggested adjustments as the next variable to test, not proof of the cause:

Repeated pattern Working hypothesis Next controlled test
High possession, few shots Sterile circulation Raise Tempo by 5 or alter one midfield instruction—not both
Many shots, low SOT Weak shot selection/finishing Compare lower Style or a non-SOS plan; review striker quality
Few shots conceded, many goals Finishing/GK variance or unusually clear chances Retest unchanged before restructuring
Low possession, high shot/SOT output Counter/direct transition is functioning Preserve setup and expand the sample
Repeated high shots conceded Block is structurally too open Lower Pressing or Style, or add one midfielder/defender

Every automated output should return:

  • the inputs used;
  • whether the match/trade was distorted;
  • the calculation and threshold;
  • the evidence confidence;
  • the recommended action;
  • the assumptions still requiring manual validation.