
Strategy Framework
- 229 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
strategy-framework is a Claude Code skill providing a standardized template for defining trading strategies with entry rules, exit rules, position sizing, risk parameters, and performance criteria.
About
strategy-framework is a Claude Code skill that provides a standardized template and lifecycle for defining, documenting, and managing trading strategies. It enforces a falsifiable edge hypothesis, machine-testable entry and exit rules, position sizing, portfolio risk parameters, filters, and performance criteria, then walks a strategy through hypothesis, backtest, paper trade, and small-live stages. A developer uses it to make trading strategies reproducible, testable, and version-controlled.
- Standard template for entry/exit rules, sizing, and risk params
- Five-stage strategy lifecycle from hypothesis to live trading
- Performance criteria for continue/review/retire decisions
Strategy Framework by the numbers
- 229 all-time installs (skills.sh)
- Ranked #417 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
strategy-framework capabilities & compatibility
- Capabilities
- strategy definition · backtesting · risk parameters · position sizing
- Use cases
- trading · planning
- Pricing
- Free
What strategy-framework says it does
A standardized system for defining, documenting, testing, and managing trading strategies.
Trading without a written strategy framework leads to:
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill strategy-frameworkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 229 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Define, document, and manage trading strategies with standardized entry/exit rules, sizing, and risk parameters.
Who is it for?
Turning trading ideas into documented, backtestable, version-controlled strategy definitions.
Skip if: Position-sizing math or risk-management internals, which it defers to the position-sizing and risk-management skills.
When should I use this skill?
You are defining or documenting a trading strategy and need a disciplined, testable template.
What you get
A fully specified, falsifiable, version-controlled strategy document with entry/exit, sizing, risk, and retirement criteria.
- A completed strategy definition document
- A staged plan from hypothesis to small live trading
By the numbers
- Backtest requires minimum 100 trades
- Paper trade at least 2 weeks or 30 trades
- 5 documented exit-rule types
Files
Strategy Framework
A standardized system for defining, documenting, testing, and managing trading strategies. This skill provides templates and tools that enforce discipline, enable reproducibility, and make strategies testable.
Why a Strategy Framework Matters
Trading without a written strategy framework leads to:
- Inconsistency: ad-hoc decisions driven by emotion rather than rules
- Untestability: vague ideas that cannot be backtested or evaluated
- Scope creep: strategies that drift without version-controlled definitions
- Unmanaged risk: missing stop losses, position limits, or drawdown halts
A strategy framework forces you to: 1. State a falsifiable hypothesis about a market inefficiency 2. Define precise, machine-testable entry and exit rules 3. Specify position sizing and risk parameters before trading 4. Set minimum performance criteria for continuation or retirement 5. Track changes through versioned strategy documents
Strategy Definition Template
Every strategy must be documented using the standard template. The full copy-paste template is in references/strategy_template.md.
Core Sections
Identity
Name: SOL-EMA-Cross v1.0
Asset class: Solana tokens (top 50 by 24h volume)
Timeframe: Primary 1H, confirmation 4H
Style: Trend followingEdge Hypothesis: State what market inefficiency you are exploiting and why it exists.
Hypothesis: Solana mid-cap tokens exhibit momentum persistence
on the 1H timeframe due to retail herding behavior and low
institutional participation. EMA crossovers capture the
initiation of these trends.Entry Rules: Specific, testable conditions combined with AND/OR logic.
def entry_signal(data: pd.DataFrame) -> bool:
"""All conditions must be True (AND logic)."""
ema_cross = data["ema_12"] > data["ema_26"] # EMA 12 crossed above 26
ema_rising = data["ema_26"].diff(3) > 0 # 26 EMA trending up
volume_ok = data["volume"] > data["vol_sma_20"] * 1.5 # Volume confirmation
regime_ok = data["adx"] > 20 # Trending regime
return ema_cross & ema_rising & volume_ok & regime_okExit Rules: Every strategy needs multiple exit mechanisms.
| Exit Type | Method | Parameters |
|---|---|---|
| Stop Loss | ATR-based | 2.0 × ATR(14) below entry |
| Take Profit | Risk multiple | 3.0 × risk (3:1 R:R) |
| Trailing Stop | Chandelier | 3.0 × ATR(14) from highest high |
| Time Stop | Bar count | Close if flat after 20 bars |
| Signal Exit | EMA reversal | EMA 12 crosses below EMA 26 |
Position Sizing: Method and parameters. See the position-sizing skill for details.
risk_per_trade = 0.02 # 2% of portfolio
stop_distance_pct = 0.05 # 5% from entry (ATR-derived)
position_size = (portfolio * risk_per_trade) / stop_distance_pctRisk Parameters: Portfolio-level guardrails. See the risk-management skill.
Max concurrent positions: 5
Risk per trade: 2% of portfolio
Daily loss limit: 5% of portfolio
Max drawdown halt: 15% — stop trading, review strategy
Correlated exposure limit: 10% (e.g., meme tokens combined)Filters: Conditions that prevent entry even if signals fire.
def filters_pass(token: dict, market: dict) -> bool:
"""All filters must pass before entry is allowed."""
volume_ok = token["volume_24h"] > 500_000 # Min $500K volume
liquidity_ok = token["liquidity"] > 100_000 # Min $100K liquidity
age_ok = token["age_days"] > 7 # Not brand new
holders_ok = token["holder_count"] > 500 # Sufficient distribution
regime_ok = market["regime"] != "crisis" # No crisis regime
return all([volume_ok, liquidity_ok, age_ok, holders_ok, regime_ok])Performance Criteria: When to continue, review, or retire.
Continue: Sharpe > 1.0, PF > 1.5, Win Rate > 40%, MDD < 20%
Review: Any metric degrades 25% from baseline
Retire: Rolling 30-day Sharpe < 0, or 3 consecutive losing monthsStrategy Lifecycle
1. Hypothesis
Identify a market inefficiency and explain why it exists and why it might persist.
Good hypothesis: "New PumpFun tokens that reach 80+ SOL in bonding curve within 10 minutes have a 65% probability of graduating to Raydium, creating a predictable price spike at graduation."
Bad hypothesis: "SOL will go up." (Not specific, not testable, no edge identified.)
2. Definition
Write the full strategy document using the template in references/strategy_template.md. Every field must be filled. If you cannot fill a field, the strategy is not ready.
3. Backtest
Test on historical data using vectorbt or equivalent. Requirements:
- Minimum 100 trades in the test period
- Use walk-forward validation (train on 70%, test on 30%)
- Account for slippage and fees (see
slippage-modelingskill) - Report both in-sample and out-of-sample metrics
4. Paper Trade
Run the strategy in simulation for at least 2 weeks (or 30 trades, whichever is longer).
- Compare paper results to backtest expectations
- If results differ by more than 25%, investigate before proceeding
5. Small Live
Trade with minimum viable size (enough to cover fees, small enough to be inconsequential).
- Run for at least 30 trades
- Compare to paper trade results
6. Scale
If small-live metrics match expectations (within 25% of backtest):
- Increase position size gradually (25% increments per week)
- Monitor metrics continuously
7. Monitor
Ongoing performance tracking:
- Daily: P&L, trade count, win rate
- Weekly: Sharpe ratio, profit factor, drawdown
- Monthly: Full strategy review against performance criteria
8. Retire
Stop using a strategy when:
- Rolling 30-day Sharpe drops below 0
- Three consecutive losing months
- Market regime permanently shifts (e.g., regulatory change)
- A better strategy replaces it for the same edge
Strategy Evaluation Criteria
Minimum thresholds before a strategy should be traded live:
| Metric | Trend Following | Mean Reversion | Scalping |
|---|---|---|---|
| Min Trades | 100 | 100 | 500 |
| Sharpe (OOS) | > 1.0 | > 1.0 | > 1.5 |
| Profit Factor | > 1.5 | > 1.5 | > 1.3 |
| Max Drawdown | < 20% | < 15% | < 10% |
| Win Rate | > 35% | > 55% | > 55% |
| Avg Win/Avg Loss | > 2.0 | > 1.0 | > 1.0 |
Strategy Types for Crypto
Detailed descriptions of each strategy type are in references/strategy_types.md.
Momentum / Trend Following
- Edge: Price trends persist due to behavioral biases and information asymmetry
- Indicators: EMA crossovers, SuperTrend, ADX, MACD
- Win rate: 35-45%, relies on large winners
- Best regime: Trending markets with moderate volatility
Mean Reversion
- Edge: Price oscillates around equilibrium due to overreaction
- Indicators: RSI, Bollinger Bands, z-score, VWAP deviation
- Win rate: 55-65%, relies on high win rate with smaller gains
- Best regime: Ranging markets with low-moderate volatility
Breakout
- Edge: Compressed volatility leads to directional expansion
- Indicators: Bollinger Band squeeze, Donchian channels, volume breakout
- Win rate: 30-40%, relies on catching large moves
- Best regime: Transitioning from low to high volatility
Copy Trading / Wallet Following
- Edge: Skilled wallets have informational or analytical advantages
- Indicators: Wallet PnL history, trade frequency, token selection
- Win rate: Depends on followed wallet quality
- Best regime: Any (depends on followed wallet's strategy)
PumpFun Sniping
- Edge: Predictable price dynamics around token creation and graduation
- Strategies: Creation snipe, volume confirmation, graduation play
- Win rate: Highly variable (20-60% depending on approach)
- Best regime: High retail activity periods
Arbitrage
- Edge: Price discrepancies across DEXs or between spot and perpetuals
- Indicators: Price feeds from multiple venues, funding rates
- Win rate: > 80% when executed correctly
- Best regime: High volatility, fragmented liquidity
Market Making
- Edge: Capturing bid-ask spread while managing inventory risk
- Indicators: Order book depth, volatility, inventory position
- Win rate: > 60%, relies on volume and spread capture
- Best regime: Stable markets with consistent volume
Common Strategy Mistakes
1. No written rules: Trading on intuition, unable to backtest or reproduce 2. Curve fitting: Optimizing parameters until backtest looks perfect, fails live 3. Missing stops: "I'll exit when it feels right" leads to catastrophic losses 4. Ignoring regime: Using a trend strategy in a ranging market (or vice versa) 5. Survivorship bias: Only backtesting tokens that still exist 6. Lookahead bias: Using future information in backtest signals 7. Ignoring costs: Not accounting for slippage, fees, and market impact 8. Over-trading: Entering on marginal signals to "stay active" 9. Strategy hopping: Abandoning strategies after normal losing streaks 10. No retirement plan: Continuing to trade a broken strategy out of attachment
Integration with Other Skills
| Skill | Integration |
|---|---|
vectorbt | Backtest strategy definitions programmatically |
pandas-ta | Compute technical indicators for entry/exit signals |
regime-detection | Market regime filters for strategy activation |
exit-strategies | Detailed exit rule implementation |
position-sizing | Position size calculation methods |
risk-management | Portfolio-level risk parameter enforcement |
slippage-modeling | Realistic execution cost estimation |
feature-engineering | ML feature computation from strategy signals |
Files
References
references/strategy_template.md— Complete copy-paste strategy definition templatereferences/strategy_types.md— Detailed guide to each strategy type with parameters and examples
Scripts
scripts/define_strategy.py— Interactive strategy definition tool with--demomodescripts/strategy_scorecard.py— Strategy evaluation scorecard with GO/REVIEW/NO-GO recommendations
Strategy Definition Template
Copy this template and fill in every section. If you cannot fill a section, the strategy is not ready for testing.
Template
# Strategy: [Name] v[X.Y]
## Overview
- **Asset class**: [e.g., Solana tokens, top 50 by 24h volume]
- **Timeframe**: Primary [1H], Confirmation [4H]
- **Style**: [Trend following / Mean reversion / Breakout / Scalping / Other]
- **Edge hypothesis**: [One paragraph: what inefficiency exists and why it persists]
## Entry Rules
All conditions use AND logic unless noted otherwise.
- Condition 1: [specific, testable — e.g., EMA(12) > EMA(26)]
- Condition 2: [specific, testable — e.g., ADX > 20]
- Condition 3: [specific, testable — e.g., Volume > 1.5x 20-period SMA]
- Condition 4 (optional): [additional confirmation]
**Entry logic**: ALL of conditions 1-3 must be True (AND)
**Entry execution**:
- Order type: [Market / Limit at bid+X bps]
- Slippage tolerance: [X bps]
- Max entry time: [fill within N seconds or cancel]
## Exit Rules
### Stop Loss
- **Method**: [Fixed % / ATR-based / Support level / Volatility-adjusted]
- **Parameters**: [e.g., 2.0 × ATR(14) below entry]
- **Hard stop**: [absolute max loss per trade, e.g., 3% of portfolio]
### Take Profit
- **Method**: [Fixed R:R / Resistance level / Indicator target]
- **Parameters**: [e.g., 3.0 × risk distance above entry]
- **Partial exits**: [e.g., 50% at 2R, 50% at 3R]
### Trailing Stop
- **Method**: [Chandelier / Percentage / Parabolic SAR / ATR trail]
- **Parameters**: [e.g., 3.0 × ATR(14) from highest high since entry]
- **Activation**: [e.g., activate after 1.5R profit reached]
### Time Stop
- **Trigger**: [e.g., close position if flat (< 0.5R move) after 20 bars]
- **Rationale**: [capital is better deployed elsewhere if no movement]
### Signal Exit
- **Condition**: [e.g., EMA(12) crosses below EMA(26)]
- **Overrides**: [does this override trailing stop? take profit?]
### Exit Priority
1. Hard stop loss (always honored)
2. Signal exit
3. Trailing stop
4. Take profit
5. Time stop
## Position Sizing
- **Method**: [Fixed fractional / Volatility-adjusted / Kelly criterion]
- **Risk per trade**: [X% of portfolio]
- **Calculation**:position_size = (portfolio_value × risk_per_trade) / stop_distance
- **Max position**: [Y% of portfolio in any single trade]
- **Min position**: [Z SOL or $W — must cover fees]
## Risk Parameters
- **Max concurrent positions**: [N]
- **Max correlated positions**: [M positions in same sector/narrative]
- **Daily loss limit**: [X% — stop trading for the day if hit]
- **Weekly loss limit**: [Y% — reduce size by 50% if hit]
- **Max drawdown halt**: [Z% — stop trading entirely, review strategy]
- **Correlated exposure limit**: [W% of portfolio in correlated assets]
## Filters
Conditions that prevent entry even when signals fire.
### Market Filters
- **Regime filter**: [e.g., only trade when ADX > 20 (trending)]
- **Volatility filter**: [e.g., skip if ATR/price > 15% (too volatile)]
- **Correlation filter**: [e.g., skip if BTC correlation > 0.9 during BTC downtrend]
### Token Filters
- **Volume filter**: [minimum 24h volume, e.g., > $500K]
- **Liquidity filter**: [minimum pool liquidity, e.g., > $100K]
- **Age filter**: [minimum token age, e.g., > 7 days]
- **Holder filter**: [minimum holder count, e.g., > 500]
- **Concentration filter**: [top 10 holders < 50% of supply]
### Time Filters
- **Active hours**: [e.g., 08:00-22:00 UTC only]
- **Day filter**: [e.g., avoid weekends for low-liquidity tokens]
- **Event filter**: [e.g., avoid 1H before/after major announcements]
## Performance Criteria
### Continue Trading
- Rolling 30d Sharpe > [1.0]
- Rolling 30d Profit Factor > [1.5]
- Rolling 30d Win Rate > [40%]
- Max drawdown from peak < [20%]
### Review Required
- Any metric degrades [25%] from baseline
- Three consecutive losing weeks
- Significant market regime change detected
### Retire Strategy
- Rolling 30d Sharpe < [0] for [2 consecutive weeks]
- Three consecutive losing months
- Max drawdown halt triggered [twice in 30 days]
- Fundamental edge no longer exists (e.g., protocol change)
## Backtest Results
### In-Sample (Training Period)
- **Period**: [YYYY-MM-DD to YYYY-MM-DD]
- **Total return**: [X%]
- **Sharpe ratio**: [X.XX]
- **Max drawdown**: [X%]
- **Win rate**: [X%]
- **Profit factor**: [X.XX]
- **Trade count**: [N]
- **Avg trade duration**: [N bars]
### Out-of-Sample (Test Period)
- **Period**: [YYYY-MM-DD to YYYY-MM-DD]
- **Total return**: [X%]
- **Sharpe ratio**: [X.XX]
- **Max drawdown**: [X%]
- **Win rate**: [X%]
- **Profit factor**: [X.XX]
- **Trade count**: [N]
- **Degradation from IS**: [X% Sharpe drop, X% PF drop]
### Paper Trade Results
- **Period**: [YYYY-MM-DD to YYYY-MM-DD]
- **Total return**: [X%]
- **Comparison to OOS**: [within X% — acceptable / needs investigation]
## Dependencies
- Data source: [e.g., Birdeye API, DexScreener]
- Indicators: [e.g., pandas-ta EMA, ADX, ATR]
- Execution: [e.g., Jupiter API via jupiter-swap skill]
- Risk: [e.g., position-sizing skill, risk-management skill]
## Notes
- [Any additional context, known limitations, or observations]
## Change Log
- **v1.0** [YYYY-MM-DD]: Initial strategy definition
- **v1.1** [YYYY-MM-DD]: [Description of changes and reason]Usage Notes
1. Fill every field: Empty fields indicate an incomplete strategy. Do not trade an incomplete strategy. 2. Be specific: "Buy when RSI is low" is not testable. "Buy when RSI(14) < 30 AND price > EMA(200)" is testable. 3. Version your changes: Every parameter change gets a new version number and changelog entry. 4. Keep it updated: After backtesting and paper trading, fill in the results sections. 5. One strategy per document: Do not combine multiple strategies in one template.
Strategy Types for Crypto Trading
Detailed guide to each strategy archetype with entry/exit logic, expected metrics, and parameter suggestions.
Momentum / Trend Following
Edge: Price trends persist due to behavioral biases (herding, anchoring) and information diffusion lag. In crypto, low institutional participation amplifies trend persistence.
Entry signals:
- EMA(12) crosses above EMA(26) with ADX > 20
- SuperTrend flips bullish on primary timeframe
- Price breaks above N-period high with volume confirmation (> 1.5x average)
- MACD histogram turns positive after bearish-to-bullish crossover
Exit signals:
- Trailing stop: Chandelier exit at 3.0 × ATR(14) from highest high
- Signal reversal: EMA(12) crosses below EMA(26)
- Trend exhaustion: ADX peaks and turns down from above 40
Expected metrics:
- Win rate: 35-45%
- Avg win / avg loss: 2.0-4.0x
- Profit factor: 1.5-2.5
- Best Sharpe: 1.0-2.0 in trending regimes
Parameter suggestions by timeframe:
| Timeframe | Fast EMA | Slow EMA | ATR Period | ATR Multiplier |
|---|---|---|---|---|
| 5m | 8 | 21 | 10 | 1.5 |
| 15m | 9 | 21 | 14 | 2.0 |
| 1H | 12 | 26 | 14 | 2.5 |
| 4H | 12 | 26 | 14 | 3.0 |
| 1D | 20 | 50 | 14 | 3.0 |
Risk notes: Trend following suffers in ranging markets. Use ADX > 20 or regime detection as a filter. Expect 5-8 consecutive losers during chop — size accordingly.
Mean Reversion
Edge: Price oscillates around a fair value equilibrium. Overreactions (panic selling, FOMO buying) create temporary dislocations that correct. Works best for established tokens with consistent liquidity.
Entry signals:
- RSI(14) < 30 AND price above EMA(200) (oversold in uptrend)
- Price below lower Bollinger Band(20, 2.0) with band width > 0.05
- Z-score of price relative to 20-period mean < -2.0
- Price deviates > 2% below VWAP with increasing volume
Exit signals:
- Return to mean: price crosses back above EMA(20) or VWAP
- RSI(14) > 50 (neutral zone)
- Bollinger Band midline touch
- Time stop: 10-20 bars if no mean reversion occurs
Expected metrics:
- Win rate: 55-65%
- Avg win / avg loss: 0.8-1.5x
- Profit factor: 1.3-2.0
- Best Sharpe: 1.0-1.5 in ranging regimes
Parameter suggestions by timeframe:
| Timeframe | RSI Period | BB Period | BB Std | Z-Score Lookback |
|---|---|---|---|---|
| 5m | 10 | 15 | 2.0 | 20 |
| 15m | 14 | 20 | 2.0 | 30 |
| 1H | 14 | 20 | 2.0 | 30 |
| 4H | 14 | 20 | 2.5 | 40 |
| 1D | 14 | 20 | 2.5 | 50 |
Risk notes: Mean reversion fails catastrophically in trending markets. A "cheap" token can get much cheaper. Always use a hard stop loss. Never mean-revert tokens with broken fundamentals.
Breakout
Edge: Periods of low volatility (consolidation) build potential energy that resolves in directional moves. Volume confirmation distinguishes real breakouts from fakeouts.
Entry signals:
- Bollinger Band width contracts to lowest 20% of 100-period range, then expands
- Price breaks above/below Donchian channel (20-period high/low)
- Volume spike > 2.5x 20-period average on breakout bar
- Range contraction (inside bars or narrowing ATR) followed by expansion
Exit signals:
- ATR trailing stop: 2.5 × ATR(14) from entry direction
- Time stop: close if no follow-through within 5-10 bars
- Failed breakout: price returns inside prior range within 3 bars
Expected metrics:
- Win rate: 30-40%
- Avg win / avg loss: 3.0-5.0x
- Profit factor: 1.5-2.5
- Many false breakouts — accept low win rate for large winners
Risk notes: False breakouts are common (50-60% of breakout signals fail). Volume confirmation is critical. Avoid breakout strategies on low-liquidity tokens where a single large order can fake a breakout.
Copy Trading / Wallet Following
Edge: Skilled wallets (early token discoverers, profitable traders) have informational or analytical advantages. Following their trades captures some of their edge.
Entry signals:
- Tracked wallet buys token matching your criteria
- Wallet has historical PnL > 50% over 30 days
- Wallet has > 60% win rate on similar tokens
- Multiple tracked wallets buy same token within short window (consensus signal)
Exit signals:
- Tracked wallet sells (follow their exit)
- Independent stop loss (do not rely solely on wallet exit)
- Time stop: wallet has not exited after N hours
- Your own technical stop loss triggers
Implementation considerations:
- Monitor wallets via Helius webhooks or polling (see
helius-apiskill) - Latency matters: detect and execute within seconds of wallet transaction
- Wallet quality degrades over time — continuously re-evaluate tracked wallets
- Filter by wallet style: sniper, swing trader, accumulator
Risk notes: Wallet following has significant latency risk — by the time you buy, the price may have moved. Smart wallets may front-run followers. Diversify across multiple wallets. Re-evaluate wallet quality monthly.
PumpFun Strategies
Edge: PumpFun token lifecycle creates predictable price dynamics at known milestones.
Sniper Strategy
- Entry: Buy within first 10 transactions of token creation
- Exit: Sell within 1-5 minutes (quick flip)
- Win rate: 20-30% (most new tokens fail immediately)
- Risk: Extremely high. Most tokens go to zero. Size very small.
- Key metric: The 20-30% of winners must return 5-10x to compensate
Volume Confirmation Strategy
- Entry: Buy after token shows sustained buying volume for 5+ minutes
- Exit: Sell on first volume decline or reversal signal
- Win rate: 35-45%
- Risk: Lower than sniping but still high. Volume can evaporate instantly.
Graduation Play
- Entry: Buy when bonding curve approaches 85 SOL (near graduation threshold)
- Exit: Sell during post-graduation pump on Raydium
- Win rate: 50-65% (tokens that reach 85 SOL often graduate)
- Risk: If graduation fails or dumps immediately post-graduation
Risk notes: PumpFun strategies involve extremely high-risk tokens. Never allocate more than 1-2% of portfolio to any single PumpFun trade. Most tokens go to zero.
Arbitrage
Edge: Price discrepancies across DEXs or between spot and perpetual markets.
Types:
- DEX-to-DEX: Same token priced differently on Raydium vs Orca
- CEX-DEX: Price difference between centralized and decentralized exchanges
- Funding rate: Long spot, short perp when funding is positive (or vice versa)
- Triangle: A→B→C→A across three token pairs
Entry: Price discrepancy exceeds transaction costs + slippage + margin of safety
Exit: Both legs of the trade execute or unwind immediately
Expected metrics:
- Win rate: > 80% (if costs are modeled correctly)
- Avg profit per trade: 0.1-1.0% (thin margins, high frequency)
- Profit factor: > 3.0
- Risk: execution risk (one leg fills, other does not)
Risk notes: Arbitrage requires speed (sub-second execution), accurate cost modeling, and capital efficiency. MEV bots compete for the same opportunities. See mev-analysis skill.
Market Making
Edge: Capturing bid-ask spread while managing inventory risk. Profitable when spread exceeds adverse selection cost.
Entry: Place limit orders on both sides of the market (bid and ask)
Exit: Opposing order fills, or inventory limit reached
Parameters:
- Spread width: function of volatility and inventory
- Order size: small relative to book depth
- Inventory limits: maximum long/short exposure
- Rebalancing: frequency of quote updates
Expected metrics:
- Win rate: > 60%
- Profit per trade: very small (captured spread)
- Volume: high (many trades per day)
- Risk: inventory risk during sharp moves
Risk notes: Market making on Solana DEXs is complex due to AMM mechanics. Traditional limit-order market making is mainly on CLOBs (e.g., Phoenix, OpenBook). LP provision on AMMs is a form of passive market making. See lp-math and impermanent-loss skills.
Choosing the Right Strategy Type
| Factor | Best Strategy Type |
|---|---|
| Trending market | Momentum / Trend Following |
| Ranging market | Mean Reversion |
| Low volatility transitioning to high | Breakout |
| Access to skilled wallet data | Copy Trading |
| High retail activity on PumpFun | PumpFun Strategies |
| Multi-venue infrastructure | Arbitrage |
| High capital, low-vol stable pairs | Market Making |
#!/usr/bin/env python3
"""Interactive strategy definition tool.
Prompts the user through each section of the strategy template and
generates a formatted strategy document. Use --demo mode to see an
example EMA crossover strategy definition without interactive input.
Usage:
python scripts/define_strategy.py # Interactive mode
python scripts/define_strategy.py --demo # Generate example strategy
Dependencies:
None (standard library only)
"""
import sys
import textwrap
from dataclasses import dataclass, field
from typing import Optional
# ── Data Structures ─────────────────────────────────────────────────
@dataclass
class ExitRule:
"""A single exit rule with method and parameters."""
exit_type: str
method: str
parameters: str
@dataclass
class PerformanceCriteria:
"""Thresholds for continue / review / retire decisions."""
continue_sharpe: float = 1.0
continue_pf: float = 1.5
continue_win_rate: float = 0.40
continue_max_dd: float = 0.20
review_degradation: float = 0.25
retire_sharpe: float = 0.0
@dataclass
class StrategyDefinition:
"""Complete strategy definition."""
name: str = ""
version: str = "1.0"
asset_class: str = ""
timeframe_primary: str = ""
timeframe_confirmation: str = ""
style: str = ""
edge_hypothesis: str = ""
entry_conditions: list[str] = field(default_factory=list)
entry_logic: str = "AND"
exit_rules: list[ExitRule] = field(default_factory=list)
sizing_method: str = ""
risk_per_trade: float = 0.02
max_position_pct: float = 0.10
max_concurrent: int = 5
daily_loss_limit: float = 0.05
max_drawdown_halt: float = 0.15
correlated_limit: float = 0.10
regime_filter: str = ""
volume_filter: str = ""
time_filter: str = ""
token_filter: str = ""
performance: PerformanceCriteria = field(default_factory=PerformanceCriteria)
notes: str = ""
# ── Validation ──────────────────────────────────────────────────────
def validate_strategy(strategy: StrategyDefinition) -> list[str]:
"""Validate strategy definition completeness.
Args:
strategy: The strategy definition to validate.
Returns:
List of warning messages for missing or incomplete sections.
"""
warnings: list[str] = []
if not strategy.name:
warnings.append("MISSING: Strategy name")
if not strategy.asset_class:
warnings.append("MISSING: Asset class")
if not strategy.timeframe_primary:
warnings.append("MISSING: Primary timeframe")
if not strategy.style:
warnings.append("MISSING: Strategy style")
if not strategy.edge_hypothesis:
warnings.append("MISSING: Edge hypothesis — you must articulate your edge")
if len(strategy.entry_conditions) == 0:
warnings.append("MISSING: Entry conditions — need at least one")
if len(strategy.entry_conditions) < 2:
warnings.append("WARNING: Only one entry condition — consider adding confirmation")
if len(strategy.exit_rules) == 0:
warnings.append("MISSING: Exit rules — need at least a stop loss")
else:
has_stop = any(r.exit_type.lower() == "stop loss" for r in strategy.exit_rules)
if not has_stop:
warnings.append("CRITICAL: No stop loss defined — every strategy needs a stop loss")
if not strategy.sizing_method:
warnings.append("MISSING: Position sizing method")
if strategy.risk_per_trade > 0.05:
warnings.append(
f"WARNING: Risk per trade is {strategy.risk_per_trade:.1%} — "
"consider keeping under 5%"
)
if strategy.max_drawdown_halt > 0.25:
warnings.append(
f"WARNING: Max drawdown halt is {strategy.max_drawdown_halt:.0%} — "
"consider a tighter limit"
)
if not strategy.regime_filter:
warnings.append("WARNING: No regime filter — strategy may underperform in wrong regime")
return warnings
# ── Formatting ──────────────────────────────────────────────────────
def format_strategy(strategy: StrategyDefinition) -> str:
"""Format a strategy definition as a markdown document.
Args:
strategy: The strategy definition to format.
Returns:
Formatted markdown string.
"""
lines: list[str] = []
lines.append(f"# Strategy: {strategy.name} v{strategy.version}")
lines.append("")
# Overview
lines.append("## Overview")
lines.append(f"- **Asset class**: {strategy.asset_class}")
lines.append(
f"- **Timeframe**: Primary {strategy.timeframe_primary}"
+ (f", Confirmation {strategy.timeframe_confirmation}"
if strategy.timeframe_confirmation else "")
)
lines.append(f"- **Style**: {strategy.style}")
lines.append(f"- **Edge hypothesis**: {strategy.edge_hypothesis}")
lines.append("")
# Entry Rules
lines.append("## Entry Rules")
lines.append("")
for i, condition in enumerate(strategy.entry_conditions, 1):
lines.append(f"- Condition {i}: {condition}")
lines.append("")
lines.append(f"**Entry logic**: {strategy.entry_logic} — "
f"{'all' if strategy.entry_logic == 'AND' else 'any'} "
f"conditions must be true")
lines.append("")
# Exit Rules
lines.append("## Exit Rules")
lines.append("")
if strategy.exit_rules:
for rule in strategy.exit_rules:
lines.append(f"### {rule.exit_type}")
lines.append(f"- **Method**: {rule.method}")
lines.append(f"- **Parameters**: {rule.parameters}")
lines.append("")
else:
lines.append("*No exit rules defined — INCOMPLETE*")
lines.append("")
# Position Sizing
lines.append("## Position Sizing")
lines.append(f"- **Method**: {strategy.sizing_method}")
lines.append(f"- **Risk per trade**: {strategy.risk_per_trade:.1%}")
lines.append(f"- **Max position**: {strategy.max_position_pct:.0%} of portfolio")
lines.append("")
# Risk Parameters
lines.append("## Risk Parameters")
lines.append(f"- **Max concurrent positions**: {strategy.max_concurrent}")
lines.append(f"- **Daily loss limit**: {strategy.daily_loss_limit:.1%}")
lines.append(f"- **Max drawdown halt**: {strategy.max_drawdown_halt:.0%}")
lines.append(f"- **Correlated exposure limit**: {strategy.correlated_limit:.0%}")
lines.append("")
# Filters
lines.append("## Filters")
lines.append(f"- **Regime filter**: {strategy.regime_filter or 'None specified'}")
lines.append(f"- **Volume filter**: {strategy.volume_filter or 'None specified'}")
lines.append(f"- **Time filter**: {strategy.time_filter or 'None specified'}")
lines.append(f"- **Token filter**: {strategy.token_filter or 'None specified'}")
lines.append("")
# Performance Criteria
perf = strategy.performance
lines.append("## Performance Criteria")
lines.append(
f"- **Continue if**: Sharpe > {perf.continue_sharpe}, "
f"PF > {perf.continue_pf}, "
f"Win Rate > {perf.continue_win_rate:.0%}, "
f"MDD < {perf.continue_max_dd:.0%}"
)
lines.append(
f"- **Review if**: Any metric degrades {perf.review_degradation:.0%} from baseline"
)
lines.append(
f"- **Retire if**: Rolling 30d Sharpe < {perf.retire_sharpe}"
)
lines.append("")
# Backtest Results (placeholder)
lines.append("## Backtest Results")
lines.append("")
lines.append("*Fill in after backtesting*")
lines.append("")
lines.append("### In-Sample")
lines.append("- Period: ")
lines.append("- Sharpe: ")
lines.append("- Max Drawdown: ")
lines.append("- Win Rate: ")
lines.append("- Profit Factor: ")
lines.append("- Trade Count: ")
lines.append("")
lines.append("### Out-of-Sample")
lines.append("- Period: ")
lines.append("- Sharpe: ")
lines.append("- Max Drawdown: ")
lines.append("- Win Rate: ")
lines.append("- Profit Factor: ")
lines.append("- Trade Count: ")
lines.append("")
# Notes
if strategy.notes:
lines.append("## Notes")
lines.append(strategy.notes)
lines.append("")
# Change Log
lines.append("## Change Log")
lines.append(f"- **v{strategy.version}**: Initial strategy definition")
lines.append("")
return "\n".join(lines)
# ── Interactive Mode ────────────────────────────────────────────────
def prompt(label: str, default: str = "") -> str:
"""Prompt the user for input with an optional default.
Args:
label: The prompt label to display.
default: Default value if user presses Enter.
Returns:
User input or default value.
"""
suffix = f" [{default}]" if default else ""
try:
value = input(f" {label}{suffix}: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nAborted.")
sys.exit(0)
return value if value else default
def prompt_float(label: str, default: float) -> float:
"""Prompt for a float value with a default.
Args:
label: The prompt label.
default: Default float value.
Returns:
Parsed float from user input.
"""
raw = prompt(label, str(default))
try:
return float(raw)
except ValueError:
print(f" Invalid number, using default: {default}")
return default
def prompt_int(label: str, default: int) -> int:
"""Prompt for an integer value with a default.
Args:
label: The prompt label.
default: Default integer value.
Returns:
Parsed integer from user input.
"""
raw = prompt(label, str(default))
try:
return int(raw)
except ValueError:
print(f" Invalid number, using default: {default}")
return default
def interactive_define() -> StrategyDefinition:
"""Walk the user through defining a strategy interactively.
Returns:
Completed StrategyDefinition.
"""
s = StrategyDefinition()
print("\n=== Strategy Definition Tool ===\n")
print("Fill in each section. Press Enter to skip optional fields.\n")
# Identity
print("── Identity ──")
s.name = prompt("Strategy name", "My-Strategy")
s.version = prompt("Version", "1.0")
s.asset_class = prompt("Asset class (e.g., Solana tokens top 50 by volume)")
s.timeframe_primary = prompt("Primary timeframe (e.g., 1H)")
s.timeframe_confirmation = prompt("Confirmation timeframe (e.g., 4H, optional)")
s.style = prompt("Style (Trend/MeanReversion/Breakout/Scalping/Other)")
print()
# Edge
print("── Edge Hypothesis ──")
s.edge_hypothesis = prompt("What market inefficiency are you exploiting?")
print()
# Entry
print("── Entry Rules ──")
print(" Enter conditions one per line. Empty line to finish.")
while True:
condition = prompt(f"Condition {len(s.entry_conditions) + 1} (empty to finish)")
if not condition:
break
s.entry_conditions.append(condition)
s.entry_logic = prompt("Logic (AND/OR)", "AND").upper()
print()
# Exit
print("── Exit Rules ──")
for exit_type in ["Stop Loss", "Take Profit", "Trailing Stop", "Time Stop", "Signal Exit"]:
method = prompt(f"{exit_type} method (empty to skip)")
if method:
params = prompt(f"{exit_type} parameters")
s.exit_rules.append(ExitRule(exit_type=exit_type, method=method, parameters=params))
print()
# Position Sizing
print("── Position Sizing ──")
s.sizing_method = prompt("Method (FixedFractional/VolatilityAdjusted/Kelly)", "FixedFractional")
s.risk_per_trade = prompt_float("Risk per trade (decimal, e.g., 0.02 = 2%)", 0.02)
s.max_position_pct = prompt_float("Max position % of portfolio (decimal)", 0.10)
print()
# Risk
print("── Risk Parameters ──")
s.max_concurrent = prompt_int("Max concurrent positions", 5)
s.daily_loss_limit = prompt_float("Daily loss limit (decimal)", 0.05)
s.max_drawdown_halt = prompt_float("Max drawdown halt (decimal)", 0.15)
s.correlated_limit = prompt_float("Correlated exposure limit (decimal)", 0.10)
print()
# Filters
print("── Filters ──")
s.regime_filter = prompt("Regime filter (e.g., only trade when ADX > 20)")
s.volume_filter = prompt("Volume filter (e.g., 24h volume > $500K)")
s.time_filter = prompt("Time filter (e.g., 08:00-22:00 UTC)")
s.token_filter = prompt("Token filter (e.g., age > 7d, holders > 500)")
print()
# Performance
print("── Performance Criteria ──")
s.performance.continue_sharpe = prompt_float("Min Sharpe to continue", 1.0)
s.performance.continue_pf = prompt_float("Min Profit Factor to continue", 1.5)
s.performance.continue_win_rate = prompt_float("Min Win Rate to continue (decimal)", 0.40)
s.performance.continue_max_dd = prompt_float("Max drawdown to continue (decimal)", 0.20)
print()
# Notes
s.notes = prompt("Additional notes (optional)")
return s
# ── Demo Mode ───────────────────────────────────────────────────────
def demo_strategy() -> StrategyDefinition:
"""Generate an example EMA crossover strategy definition.
Returns:
A fully populated StrategyDefinition for demonstration.
"""
return StrategyDefinition(
name="SOL-EMA-Cross",
version="1.0",
asset_class="Solana tokens, top 50 by 24h volume on Birdeye",
timeframe_primary="1H",
timeframe_confirmation="4H",
style="Trend Following",
edge_hypothesis=(
"Solana mid-cap tokens exhibit momentum persistence on the 1H timeframe "
"due to retail herding behavior and low institutional participation. "
"EMA crossovers capture trend initiation with volume confirmation "
"filtering out false signals."
),
entry_conditions=[
"EMA(12) crosses above EMA(26) on 1H chart",
"EMA(26) slope is positive over last 3 bars (trend confirmation)",
"Volume > 1.5x 20-period volume SMA (volume confirmation)",
"ADX(14) > 20 (trending regime filter)",
"4H EMA(12) > 4H EMA(26) (higher timeframe alignment)",
],
entry_logic="AND",
exit_rules=[
ExitRule("Stop Loss", "ATR-based", "2.0 x ATR(14) below entry price"),
ExitRule("Take Profit", "Risk multiple", "3.0 x risk distance (3:1 R:R)"),
ExitRule(
"Trailing Stop", "Chandelier Exit",
"3.0 x ATR(14) from highest high since entry, "
"activated after 1.5R profit"
),
ExitRule("Time Stop", "Bar count", "Close if < 0.5R move after 20 bars"),
ExitRule("Signal Exit", "EMA reversal", "EMA(12) crosses below EMA(26) on 1H"),
],
sizing_method="Fixed Fractional",
risk_per_trade=0.02,
max_position_pct=0.10,
max_concurrent=5,
daily_loss_limit=0.05,
max_drawdown_halt=0.15,
correlated_limit=0.10,
regime_filter="Only trade when ADX(14) > 20 (trending regime)",
volume_filter="24h volume > $500K, pool liquidity > $100K",
time_filter="08:00-22:00 UTC (peak Solana activity)",
token_filter="Token age > 7 days, holder count > 500, top 10 holders < 50% supply",
performance=PerformanceCriteria(
continue_sharpe=1.0,
continue_pf=1.5,
continue_win_rate=0.40,
continue_max_dd=0.20,
review_degradation=0.25,
retire_sharpe=0.0,
),
notes=(
"This strategy works best during moderate trending conditions. "
"Avoid during high-volatility regime changes (e.g., major protocol updates, "
"regulatory announcements). Consider pausing during BTC dominance spikes "
"as altcoin trends become unreliable."
),
)
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run the strategy definition tool."""
demo_mode = "--demo" in sys.argv
if demo_mode:
print("=== Demo Mode: EMA Crossover Strategy ===\n")
strategy = demo_strategy()
else:
strategy = interactive_define()
# Validate
warnings = validate_strategy(strategy)
# Output
document = format_strategy(strategy)
print("\n" + "=" * 60)
print(document)
print("=" * 60)
# Validation report
if warnings:
print(f"\n⚠ Validation ({len(warnings)} issues):\n")
for w in warnings:
print(f" - {w}")
else:
print("\nValidation: All sections complete.")
print("\nTo save this strategy definition:")
print(f" python scripts/define_strategy.py {'--demo ' if demo_mode else ''}"
f"> strategies/{strategy.name.lower().replace(' ', '-')}-v{strategy.version}.md")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Strategy evaluation scorecard.
Takes backtest results and scores a strategy across multiple dimensions,
providing an overall GO / REVIEW / NO-GO recommendation with specific
concerns and suggestions.
Usage:
python scripts/strategy_scorecard.py --demo # Run with 3 example strategies
python scripts/strategy_scorecard.py # Enter metrics interactively
Dependencies:
None (standard library only)
"""
import math
import sys
from dataclasses import dataclass
from typing import Optional
# ── Data Structures ─────────────────────────────────────────────────
@dataclass
class BacktestResults:
"""Backtest results for a strategy."""
strategy_name: str
style: str # "trend", "mean_reversion", "scalping", "breakout"
total_return_pct: float
sharpe_ratio: float
max_drawdown_pct: float
win_rate: float # as decimal (0.45 = 45%)
profit_factor: float
trade_count: int
avg_win_loss_ratio: float
avg_trade_duration_bars: float
is_oos: bool = False # out-of-sample results?
is_sharpe_degradation: Optional[float] = None # OOS vs IS Sharpe drop
@dataclass
class ScoreComponent:
"""A single scored dimension."""
name: str
score: float # 0 to 100
weight: float
grade: str # A, B, C, D, F
notes: list[str]
@dataclass
class Scorecard:
"""Complete strategy scorecard."""
strategy_name: str
components: list[ScoreComponent]
overall_score: float
recommendation: str # GO, REVIEW, NO-GO
concerns: list[str]
suggestions: list[str]
# ── Scoring Functions ───────────────────────────────────────────────
def score_edge_quality(results: BacktestResults) -> ScoreComponent:
"""Score the strategy's edge quality based on Sharpe ratio.
Args:
results: Backtest results to evaluate.
Returns:
ScoreComponent with edge quality assessment.
"""
sharpe = results.sharpe_ratio
notes: list[str] = []
if sharpe >= 2.0:
score = 95.0
notes.append(f"Sharpe {sharpe:.2f} — excellent risk-adjusted returns")
elif sharpe >= 1.5:
score = 80.0
notes.append(f"Sharpe {sharpe:.2f} — strong risk-adjusted returns")
elif sharpe >= 1.0:
score = 65.0
notes.append(f"Sharpe {sharpe:.2f} — acceptable risk-adjusted returns")
elif sharpe >= 0.5:
score = 40.0
notes.append(f"Sharpe {sharpe:.2f} — marginal edge, may not survive costs")
elif sharpe >= 0.0:
score = 20.0
notes.append(f"Sharpe {sharpe:.2f} — minimal edge detected")
else:
score = 0.0
notes.append(f"Sharpe {sharpe:.2f} — negative risk-adjusted returns")
# Bonus/penalty for profit factor
if results.profit_factor >= 2.0:
score = min(100, score + 10)
notes.append(f"Profit factor {results.profit_factor:.2f} — strong")
elif results.profit_factor < 1.2:
score = max(0, score - 10)
notes.append(f"Profit factor {results.profit_factor:.2f} — thin edge")
grade = _score_to_grade(score)
return ScoreComponent("Edge Quality", score, 0.30, grade, notes)
def score_risk_management(results: BacktestResults) -> ScoreComponent:
"""Score risk management based on drawdown characteristics.
Args:
results: Backtest results to evaluate.
Returns:
ScoreComponent with risk management assessment.
"""
mdd = results.max_drawdown_pct
notes: list[str] = []
if mdd <= 10:
score = 95.0
notes.append(f"Max drawdown {mdd:.1f}% — excellent capital preservation")
elif mdd <= 15:
score = 80.0
notes.append(f"Max drawdown {mdd:.1f}% — good capital preservation")
elif mdd <= 20:
score = 65.0
notes.append(f"Max drawdown {mdd:.1f}% — acceptable but monitor closely")
elif mdd <= 30:
score = 40.0
notes.append(f"Max drawdown {mdd:.1f}% — concerning, tighten risk controls")
elif mdd <= 50:
score = 20.0
notes.append(f"Max drawdown {mdd:.1f}% — severe, strategy needs redesign")
else:
score = 0.0
notes.append(f"Max drawdown {mdd:.1f}% — catastrophic, do not trade")
# Return-to-drawdown ratio
ret_dd = results.total_return_pct / mdd if mdd > 0 else 0
if ret_dd >= 3.0:
score = min(100, score + 10)
notes.append(f"Return/MDD ratio {ret_dd:.1f} — excellent compensation for risk")
elif ret_dd < 1.0:
score = max(0, score - 10)
notes.append(f"Return/MDD ratio {ret_dd:.1f} — returns don't justify the drawdown")
grade = _score_to_grade(score)
return ScoreComponent("Risk Management", score, 0.25, grade, notes)
def score_consistency(results: BacktestResults) -> ScoreComponent:
"""Score strategy consistency based on win rate and profit factor stability.
Args:
results: Backtest results to evaluate.
Returns:
ScoreComponent with consistency assessment.
"""
notes: list[str] = []
score = 50.0 # Start neutral
style = results.style.lower()
wr = results.win_rate
awl = results.avg_win_loss_ratio
# Win rate evaluation depends on style
if style in ("trend", "trend_following", "breakout"):
if wr >= 0.45:
score += 20
notes.append(f"Win rate {wr:.0%} — above average for trend/breakout")
elif wr >= 0.35:
score += 10
notes.append(f"Win rate {wr:.0%} — typical for trend/breakout")
elif wr >= 0.25:
notes.append(f"Win rate {wr:.0%} — low but acceptable if avg win/loss high")
else:
score -= 20
notes.append(f"Win rate {wr:.0%} — too low even for trend following")
else: # mean reversion, scalping
if wr >= 0.60:
score += 20
notes.append(f"Win rate {wr:.0%} — strong for {style}")
elif wr >= 0.50:
score += 10
notes.append(f"Win rate {wr:.0%} — adequate for {style}")
elif wr >= 0.40:
notes.append(f"Win rate {wr:.0%} — below expectations for {style}")
score -= 10
else:
score -= 20
notes.append(f"Win rate {wr:.0%} — too low for {style}")
# Avg win/loss ratio
if awl >= 3.0:
score += 15
notes.append(f"Avg win/loss {awl:.2f} — large winners compensate for losses")
elif awl >= 2.0:
score += 10
notes.append(f"Avg win/loss {awl:.2f} — good asymmetry")
elif awl >= 1.0:
notes.append(f"Avg win/loss {awl:.2f} — balanced")
else:
score -= 15
notes.append(f"Avg win/loss {awl:.2f} — losses larger than wins on average")
# Expectancy check: (win_rate * avg_win) - ((1-win_rate) * 1.0) > 0
expectancy = (wr * awl) - (1 - wr)
if expectancy > 0.5:
score += 10
notes.append(f"Expectancy per trade {expectancy:.2f}R — positive edge confirmed")
elif expectancy > 0:
notes.append(f"Expectancy per trade {expectancy:.2f}R — thin but positive")
else:
score -= 20
notes.append(f"Expectancy per trade {expectancy:.2f}R — NEGATIVE expected value")
score = max(0, min(100, score))
grade = _score_to_grade(score)
return ScoreComponent("Consistency", score, 0.20, grade, notes)
def score_sample_size(results: BacktestResults) -> ScoreComponent:
"""Score the statistical adequacy of the trade sample.
Args:
results: Backtest results to evaluate.
Returns:
ScoreComponent with sample size assessment.
"""
n = results.trade_count
notes: list[str] = []
if n >= 500:
score = 95.0
notes.append(f"{n} trades — large sample, high statistical confidence")
elif n >= 200:
score = 80.0
notes.append(f"{n} trades — good sample size")
elif n >= 100:
score = 65.0
notes.append(f"{n} trades — minimum acceptable sample")
elif n >= 50:
score = 40.0
notes.append(f"{n} trades — borderline, results may not be reliable")
elif n >= 20:
score = 20.0
notes.append(f"{n} trades — insufficient for statistical significance")
else:
score = 5.0
notes.append(f"{n} trades — far too few trades, backtest is meaningless")
# Margin of error estimate (simplified)
if n > 0:
wr = results.win_rate
margin = 1.96 * math.sqrt(wr * (1 - wr) / n)
notes.append(
f"Win rate 95% CI: {max(0, wr - margin):.0%} to {min(1, wr + margin):.0%} "
f"(±{margin:.1%})"
)
if margin > 0.10:
score = max(0, score - 10)
notes.append("Wide confidence interval — need more trades")
grade = _score_to_grade(score)
return ScoreComponent("Sample Size", score, 0.10, grade, notes)
def score_robustness(results: BacktestResults) -> ScoreComponent:
"""Score strategy robustness based on OOS degradation.
Args:
results: Backtest results to evaluate.
Returns:
ScoreComponent with robustness assessment.
"""
notes: list[str] = []
if not results.is_oos:
score = 50.0
notes.append("In-sample only — cannot assess robustness without OOS results")
notes.append("Run walk-forward validation to get out-of-sample metrics")
grade = _score_to_grade(score)
return ScoreComponent("Robustness", score, 0.15, grade, notes)
degradation = results.is_sharpe_degradation
if degradation is None:
score = 50.0
notes.append("OOS results present but no IS comparison provided")
elif degradation <= 0:
score = 95.0
notes.append(f"OOS Sharpe improved by {abs(degradation):.0%} — rare, verify data")
elif degradation <= 0.15:
score = 85.0
notes.append(f"OOS Sharpe degraded {degradation:.0%} — minimal, strategy is robust")
elif degradation <= 0.30:
score = 65.0
notes.append(f"OOS Sharpe degraded {degradation:.0%} — moderate, acceptable")
elif degradation <= 0.50:
score = 40.0
notes.append(f"OOS Sharpe degraded {degradation:.0%} — significant overfitting risk")
else:
score = 15.0
notes.append(f"OOS Sharpe degraded {degradation:.0%} — severe overfitting detected")
grade = _score_to_grade(score)
return ScoreComponent("Robustness", score, 0.15, grade, notes)
# ── Scorecard Generation ───────────────────────────────────────────
def generate_scorecard(results: BacktestResults) -> Scorecard:
"""Generate a complete strategy scorecard.
Args:
results: Backtest results to evaluate.
Returns:
Complete Scorecard with recommendation.
"""
components = [
score_edge_quality(results),
score_risk_management(results),
score_consistency(results),
score_sample_size(results),
score_robustness(results),
]
# Weighted overall score
overall = sum(c.score * c.weight for c in components)
# Recommendation
concerns: list[str] = []
suggestions: list[str] = []
if results.sharpe_ratio < 0:
concerns.append("Negative Sharpe ratio — strategy loses money on risk-adjusted basis")
if results.max_drawdown_pct > 25:
concerns.append(f"Max drawdown {results.max_drawdown_pct:.1f}% exceeds 25% threshold")
if results.trade_count < 50:
concerns.append(f"Only {results.trade_count} trades — insufficient for statistical significance")
if results.profit_factor < 1.2:
concerns.append(f"Profit factor {results.profit_factor:.2f} — thin edge, likely consumed by real-world costs")
expectancy = (results.win_rate * results.avg_win_loss_ratio) - (1 - results.win_rate)
if expectancy < 0:
concerns.append(f"Negative expectancy ({expectancy:.2f}R) — strategy has no edge")
if results.max_drawdown_pct > 15:
suggestions.append("Tighten stop losses or reduce position sizes to lower max drawdown")
if results.trade_count < 100:
suggestions.append("Extend backtest period or lower timeframe to increase trade count")
if not results.is_oos:
suggestions.append("Run walk-forward validation to get out-of-sample metrics")
if results.sharpe_ratio > 3.0:
suggestions.append("Sharpe > 3.0 is suspicious — check for lookahead bias or data errors")
if results.win_rate < 0.35 and results.avg_win_loss_ratio < 2.0:
suggestions.append("Low win rate with low avg win/loss — improve entries or widen targets")
# Determine recommendation
critical_fail = (
results.sharpe_ratio < 0
or results.profit_factor < 1.0
or expectancy < 0
or results.max_drawdown_pct > 50
)
if critical_fail:
recommendation = "NO-GO"
elif overall >= 70 and len(concerns) == 0:
recommendation = "GO"
elif overall >= 55:
recommendation = "REVIEW"
else:
recommendation = "NO-GO"
return Scorecard(
strategy_name=results.strategy_name,
components=components,
overall_score=overall,
recommendation=recommendation,
concerns=concerns,
suggestions=suggestions,
)
# ── Display ─────────────────────────────────────────────────────────
def display_scorecard(sc: Scorecard) -> None:
"""Print a formatted scorecard to stdout.
Args:
sc: The scorecard to display.
"""
rec_display = {
"GO": "GO — Strategy meets minimum criteria for live trading",
"REVIEW": "REVIEW — Strategy shows potential but has issues to address",
"NO-GO": "NO-GO — Strategy does not meet minimum criteria",
}
print(f"\n{'=' * 60}")
print(f" STRATEGY SCORECARD: {sc.strategy_name}")
print(f"{'=' * 60}\n")
# Component scores
print(f" {'Dimension':<20} {'Score':>6} {'Grade':>6} {'Weight':>8}")
print(f" {'-' * 20} {'-' * 6} {'-' * 6} {'-' * 8}")
for c in sc.components:
print(f" {c.name:<20} {c.score:>5.0f}% {c.grade:>5} {c.weight:>7.0%}")
print(f" {'-' * 20} {'-' * 6}")
print(f" {'OVERALL':<20} {sc.overall_score:>5.0f}%")
print()
# Recommendation
print(f" RECOMMENDATION: {rec_display.get(sc.recommendation, sc.recommendation)}")
print()
# Details per component
for c in sc.components:
print(f" [{c.grade}] {c.name}:")
for note in c.notes:
print(f" {note}")
print()
# Concerns
if sc.concerns:
print(" CONCERNS:")
for concern in sc.concerns:
print(f" - {concern}")
print()
# Suggestions
if sc.suggestions:
print(" SUGGESTIONS:")
for suggestion in sc.suggestions:
print(f" - {suggestion}")
print()
print(f"{'=' * 60}")
# ── Helper ──────────────────────────────────────────────────────────
def _score_to_grade(score: float) -> str:
"""Convert numeric score to letter grade.
Args:
score: Score from 0 to 100.
Returns:
Letter grade string.
"""
if score >= 90:
return "A"
elif score >= 75:
return "B"
elif score >= 60:
return "C"
elif score >= 40:
return "D"
else:
return "F"
# ── Interactive Input ───────────────────────────────────────────────
def prompt_float(label: str, default: float) -> float:
"""Prompt user for a float value.
Args:
label: Prompt text.
default: Default value.
Returns:
User-entered float or default.
"""
try:
raw = input(f" {label} [{default}]: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nAborted.")
sys.exit(0)
if not raw:
return default
try:
return float(raw)
except ValueError:
print(f" Invalid, using default: {default}")
return default
def prompt_int(label: str, default: int) -> int:
"""Prompt user for an integer value.
Args:
label: Prompt text.
default: Default value.
Returns:
User-entered int or default.
"""
try:
raw = input(f" {label} [{default}]: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nAborted.")
sys.exit(0)
if not raw:
return default
try:
return int(raw)
except ValueError:
print(f" Invalid, using default: {default}")
return default
def prompt_str(label: str, default: str = "") -> str:
"""Prompt user for a string value.
Args:
label: Prompt text.
default: Default value.
Returns:
User-entered string or default.
"""
suffix = f" [{default}]" if default else ""
try:
raw = input(f" {label}{suffix}: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nAborted.")
sys.exit(0)
return raw if raw else default
def interactive_input() -> BacktestResults:
"""Collect backtest results interactively.
Returns:
BacktestResults from user input.
"""
print("\n=== Strategy Scorecard — Enter Backtest Results ===\n")
name = prompt_str("Strategy name", "My-Strategy")
style = prompt_str("Style (trend/mean_reversion/breakout/scalping)", "trend")
total_return = prompt_float("Total return %", 50.0)
sharpe = prompt_float("Sharpe ratio", 1.0)
mdd = prompt_float("Max drawdown %", 15.0)
wr = prompt_float("Win rate (decimal, e.g., 0.45)", 0.45)
pf = prompt_float("Profit factor", 1.5)
trades = prompt_int("Trade count", 100)
awl = prompt_float("Avg win / avg loss ratio", 2.0)
duration = prompt_float("Avg trade duration (bars)", 10.0)
is_oos_str = prompt_str("Is this out-of-sample? (y/n)", "n")
is_oos = is_oos_str.lower().startswith("y")
degradation: Optional[float] = None
if is_oos:
deg = prompt_float("Sharpe degradation from IS (decimal, e.g., 0.2 = 20%)", 0.2)
degradation = deg
return BacktestResults(
strategy_name=name,
style=style,
total_return_pct=total_return,
sharpe_ratio=sharpe,
max_drawdown_pct=mdd,
win_rate=wr,
profit_factor=pf,
trade_count=trades,
avg_win_loss_ratio=awl,
avg_trade_duration_bars=duration,
is_oos=is_oos,
is_sharpe_degradation=degradation,
)
# ── Demo Data ───────────────────────────────────────────────────────
def demo_strategies() -> list[BacktestResults]:
"""Generate three example strategies for demonstration.
Returns:
List of BacktestResults: good, mediocre, and bad strategy examples.
"""
return [
BacktestResults(
strategy_name="SOL-Momentum-Pro",
style="trend",
total_return_pct=85.0,
sharpe_ratio=1.8,
max_drawdown_pct=12.0,
win_rate=0.42,
profit_factor=2.1,
trade_count=230,
avg_win_loss_ratio=2.8,
avg_trade_duration_bars=15.0,
is_oos=True,
is_sharpe_degradation=0.15,
),
BacktestResults(
strategy_name="Meme-Mean-Revert",
style="mean_reversion",
total_return_pct=22.0,
sharpe_ratio=0.8,
max_drawdown_pct=18.0,
win_rate=0.52,
profit_factor=1.3,
trade_count=85,
avg_win_loss_ratio=1.2,
avg_trade_duration_bars=8.0,
is_oos=True,
is_sharpe_degradation=0.35,
),
BacktestResults(
strategy_name="YOLO-Breakout",
style="breakout",
total_return_pct=-5.0,
sharpe_ratio=-0.3,
max_drawdown_pct=42.0,
win_rate=0.28,
profit_factor=0.85,
trade_count=35,
avg_win_loss_ratio=1.5,
avg_trade_duration_bars=5.0,
is_oos=False,
is_sharpe_degradation=None,
),
]
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run the strategy scorecard tool."""
demo_mode = "--demo" in sys.argv
if demo_mode:
print("=== Demo Mode: Evaluating 3 Example Strategies ===")
strategies = demo_strategies()
for results in strategies:
scorecard = generate_scorecard(results)
display_scorecard(scorecard)
else:
results = interactive_input()
scorecard = generate_scorecard(results)
display_scorecard(scorecard)
if __name__ == "__main__":
main()
Related skills
FAQ
What must a strategy definition include?
Identity, a falsifiable edge hypothesis, testable entry rules, multiple exit mechanisms, position sizing, portfolio risk parameters, filters, and performance criteria for continuation or retirement.
What is the strategy lifecycle?
Hypothesis, definition, backtest (minimum 100 trades with walk-forward validation), paper trade for at least two weeks, then small live trading for at least 30 trades.