DEV Community

Propfirmkey
Propfirmkey

Posted on

How to Evaluate Prop Trading Firms: A Data-Driven Framework in Python

Choosing a prop trading firm is a decision that should be based on data, not marketing. Let's build a scoring framework in Python that objectively evaluates and ranks prop firms based on measurable criteria.

The Evaluation Criteria

After analyzing dozens of firms, these are the metrics that matter most:

from dataclasses import dataclass
from typing import Optional
import json

@dataclass
class PropFirmProfile:
    name: str
    # Cost
    eval_price_50k: float       # Price for $50K evaluation
    monthly_fee: float          # Recurring monthly fee
    reset_fee: float            # Fee to reset after failure

    # Rules
    profit_target_pct: float    # Phase 1 profit target %
    max_daily_dd_pct: float     # Max daily drawdown %
    max_total_dd_pct: float     # Max trailing/total drawdown %
    drawdown_type: str          # "eod_trailing", "realtime_trailing", "static"
    min_trading_days: int       # Minimum days to pass

    # Payout
    profit_split_pct: float     # Your share of profits
    payout_frequency: str       # "weekly", "biweekly", "monthly"
    first_payout_days: int      # Days until first payout eligible

    # Platform
    platforms: list[str]
    instruments: list[str]

    # Trust
    years_operating: float
    trustpilot_rating: float
    payout_proof_count: int     # Verified payout proofs


class PropFirmScorer:
    """Weighted scoring system for prop firm evaluation."""

    WEIGHTS = {
        "cost": 0.20,
        "rules": 0.25,
        "payout": 0.25,
        "platform": 0.10,
        "trust": 0.20,
    }

    def __init__(self, firms: list[PropFirmProfile]):
        self.firms = firms

    def _normalize(self, values: list[float], invert: bool = False) -> list[float]:
        min_v, max_v = min(values), max(values)
        if max_v == min_v:
            return [0.5] * len(values)
        normalized = [(v - min_v) / (max_v - min_v) for v in values]
        return [1 - n for n in normalized] if invert else normalized

    def score_cost(self) -> list[float]:
        # Lower cost = better score
        total_costs = [f.eval_price_50k + f.monthly_fee * 3 for f in self.firms]
        return self._normalize(total_costs, invert=True)

    def score_rules(self) -> list[float]:
        scores = []
        for f in self.firms:
            s = 0
            # Higher drawdown allowance = better
            s += f.max_total_dd_pct / 12 * 30
            s += f.max_daily_dd_pct / 5 * 20
            # EOD trailing is more forgiving
            s += {"eod_trailing": 25, "static": 30, "realtime_trailing": 10}[f.drawdown_type]
            # Lower profit target = easier
            s += (15 - f.profit_target_pct) * 2
            # Fewer min days = more flexible
            s += max(0, 10 - f.min_trading_days) * 2
            scores.append(s)
        return self._normalize(scores)

    def score_payout(self) -> list[float]:
        scores = []
        for f in self.firms:
            s = f.profit_split_pct  # Direct percentage
            s += {"weekly": 15, "biweekly": 10, "monthly": 5}[f.payout_frequency]
            s -= f.first_payout_days * 0.3
            scores.append(s)
        return self._normalize(scores)

    def score_platform(self) -> list[float]:
        scores = [len(f.platforms) * 10 + len(f.instruments) * 5 for f in self.firms]
        return self._normalize(scores)

    def score_trust(self) -> list[float]:
        scores = []
        for f in self.firms:
            s = f.trustpilot_rating * 20
            s += min(f.years_operating * 10, 50)
            s += min(f.payout_proof_count * 0.1, 30)
            scores.append(s)
        return self._normalize(scores)

    def rank(self) -> list[dict]:
        cost_scores = self.score_cost()
        rules_scores = self.score_rules()
        payout_scores = self.score_payout()
        platform_scores = self.score_platform()
        trust_scores = self.score_trust()

        results = []
        for i, firm in enumerate(self.firms):
            total = (
                cost_scores[i] * self.WEIGHTS["cost"] +
                rules_scores[i] * self.WEIGHTS["rules"] +
                payout_scores[i] * self.WEIGHTS["payout"] +
                platform_scores[i] * self.WEIGHTS["platform"] +
                trust_scores[i] * self.WEIGHTS["trust"]
            )
            results.append({
                "name": firm.name,
                "total_score": round(total * 100, 1),
                "cost": round(cost_scores[i] * 100, 1),
                "rules": round(rules_scores[i] * 100, 1),
                "payout": round(payout_scores[i] * 100, 1),
                "platform": round(platform_scores[i] * 100, 1),
                "trust": round(trust_scores[i] * 100, 1),
            })

        return sorted(results, key=lambda x: x["total_score"], reverse=True)
Enter fullscreen mode Exit fullscreen mode

Sample Comparison

firms = [
    PropFirmProfile(
        name="Firm A", eval_price_50k=299, monthly_fee=0, reset_fee=99,
        profit_target_pct=8, max_daily_dd_pct=3, max_total_dd_pct=6,
        drawdown_type="eod_trailing", min_trading_days=1,
        profit_split_pct=90, payout_frequency="weekly", first_payout_days=14,
        platforms=["NinjaTrader", "Tradovate", "TradingView"],
        instruments=["ES", "NQ", "YM", "RTY", "CL", "GC"],
        years_operating=3.0, trustpilot_rating=4.5, payout_proof_count=500
    ),
    PropFirmProfile(
        name="Firm B", eval_price_50k=165, monthly_fee=0, reset_fee=80,
        profit_target_pct=6, max_daily_dd_pct=2, max_total_dd_pct=4,
        drawdown_type="realtime_trailing", min_trading_days=3,
        profit_split_pct=80, payout_frequency="biweekly", first_payout_days=30,
        platforms=["NinjaTrader", "Tradovate"],
        instruments=["ES", "NQ", "CL"],
        years_operating=5.0, trustpilot_rating=4.2, payout_proof_count=1200
    ),
    PropFirmProfile(
        name="Firm C", eval_price_50k=375, monthly_fee=0, reset_fee=150,
        profit_target_pct=10, max_daily_dd_pct=4, max_total_dd_pct=8,
        drawdown_type="static", min_trading_days=0,
        profit_split_pct=95, payout_frequency="weekly", first_payout_days=7,
        platforms=["NinjaTrader", "Tradovate", "Rithmic", "TradingView"],
        instruments=["ES", "NQ", "YM", "RTY", "CL", "GC", "ZB", "ZN"],
        years_operating=2.0, trustpilot_rating=4.7, payout_proof_count=300
    ),
]

scorer = PropFirmScorer(firms)
rankings = scorer.rank()

print(f"{'Rank':<5} {'Firm':<10} {'Score':<8} {'Cost':<8} {'Rules':<8} {'Payout':<8} {'Trust':<8}")
print("-" * 55)
for i, r in enumerate(rankings):
    print(f"{i+1:<5} {r['name']:<10} {r['total_score']:<8} {r['cost']:<8} {r['rules']:<8} {r['payout']:<8} {r['trust']:<8}")
Enter fullscreen mode Exit fullscreen mode

Making It Your Own

The weights are customizable. If cost matters most to you, increase that weight. If you're experienced and want maximum payouts, weight payout higher.

# Custom weights for different trader profiles
CONSERVATIVE = {"cost": 0.10, "rules": 0.35, "payout": 0.15, "platform": 0.10, "trust": 0.30}
AGGRESSIVE = {"cost": 0.15, "rules": 0.15, "payout": 0.40, "platform": 0.10, "trust": 0.20}
BUDGET = {"cost": 0.40, "rules": 0.20, "payout": 0.15, "platform": 0.05, "trust": 0.20}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Don't choose a prop firm based on social media hype. Run the numbers. This framework gives you a systematic way to evaluate any firm on objective criteria.

For real-world data to feed into this model, PropFirmKey maintains up-to-date comparisons of futures prop firms including Alpha Futures, complete with pricing, rules, and user reviews that map directly to our scoring criteria.

Fork this code and customize it for your needs!

Top comments (0)