
Exit Strategies
- 225 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
exit-strategies is a Claude Code skill providing systematic exit rules, stop-loss methods, take-profit strategies, and trailing stop implementations for crypto trading.
About
exit-strategies is a Claude Code skill for systematic, rule-based trade exits in crypto and Solana token trading. It covers stop-loss methods (fixed, ATR, support), take-profit frameworks (R:R targets, scaled tranches, market-cap milestones), and trailing stops (percentage, Chandelier, EMA), plus time-, signal-, and liquidity-based exits. A developer uses it when building disciplined exit logic into a trading strategy.
- Six exit categories: stop loss, take profit, trailing stop, time, signal, and liquidity
- ATR-based stops, scaled take-profit tranches, and Chandelier/EMA trailing implementations
- Market-cap milestone exits tuned for PumpFun and meme tokens
Exit Strategies by the numbers
- 225 all-time installs (skills.sh)
- Ranked #433 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
exit-strategies capabilities & compatibility
Free; runs locally on Python, no API keys.
- Capabilities
- exit strategies · stop loss · take profit · trailing stops · risk management
- Use cases
- trading · data analysis
- Runs
- Runs locally
- Pricing
- Free
What exit-strategies says it does
Systematic exit rules, stop-loss methods, take-profit strategies, and trailing stop implementations for crypto trading
Every trade should have **three exits defined before entry**: stop loss, take profit, and trailing stop.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill exit-strategiesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 225 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Define systematic stop-loss, take-profit, and trailing-stop exit rules for crypto trades.
Who is it for?
Defining disciplined stop-loss, take-profit, and trailing-stop exit rules before entering a crypto trade.
Skip if: Entry-signal generation; the skill deliberately focuses on exits over entries.
When should I use this skill?
You need systematic exit rules (stop loss, take profit, trailing stop) for a trading strategy.
What you get
Defined stop-loss, take-profit, and trailing-stop rules set before entry.
By the numbers
- Six exit categories
- Default ATR stop at 2.0x below entry
- Four-tranche scaled take-profit framework (25% each)
Files
Exit Strategies
Entries are easy, exits are everything. A mediocre entry with a disciplined exit will outperform a perfect entry with no exit plan. This skill covers systematic, rule-based exit methods for crypto and Solana token trading.
Why Exits Matter
- Entries determine _if_ you participate. Exits determine _how much_ you keep.
- Most traders spend 90% of effort on entries and 10% on exits — invert this.
- Without defined exits you rely on emotion, which guarantees inconsistency.
- Every trade should have three exits defined before entry: stop loss, take profit,
and trailing stop.
Exit Categories
1. Stop Loss — Risk Management Exits
Predefined price level where you close the position to cap downside.
| Method | Description | Best For |
|---|---|---|
| Fixed percentage | Exit at entry − X% | Simple setups, beginners |
| ATR-based | Entry − ATR(14) × multiplier | Volatility-adaptive |
| Support level | Below nearest swing low | Technically defined risk |
| Maximum loss | Absolute SOL/USD cap | Account protection |
ATR-based stop (recommended default):
import pandas_ta as ta
atr = df.ta.atr(length=14)
stop_loss = entry_price - (atr.iloc[-1] * 2.0) # 2x ATR below entryMultiplier guide:
- 1.5× — Tight. High win rate needed. Good for scalps.
- 2.0× — Standard. Balances noise filtering with risk.
- 3.0× — Wide. For swing trades in volatile conditions.
See references/stop_loss_methods.md for complete methodology.
2. Take Profit — Target Exits
Predefined levels where you lock in gains.
Fixed risk/reward targets:
risk = entry_price - stop_loss_price
tp_2r = entry_price + (risk * 2) # 2:1 R:R
tp_3r = entry_price + (risk * 3) # 3:1 R:R
tp_5r = entry_price + (risk * 5) # 5:1 R:RScaled exit framework (recommended for meme/PumpFun tokens):
| Tranche | Size | Target | Action After |
|---|---|---|---|
| 1 | 25% | 2× risk | Move stop to breakeven |
| 2 | 25% | 3–5× risk | Trail remainder |
| 3 | 25% | 5–10× risk | Tighten trail |
| 4 | 25% | Trailing stop | Moonbag — let it ride |
Market cap milestone exits:
For PumpFun and meme tokens where R:R ratios are less meaningful:
milestones = [
{"mcap": 50_000, "sell_pct": 0.25, "label": "Cover cost"},
{"mcap": 100_000, "sell_pct": 0.25, "label": "Lock profit"},
{"mcap": 500_000, "sell_pct": 0.25, "label": "Major profit"},
# Hold 25% as moonbag with trailing stop
]See references/take_profit_strategies.md for full methodology including Fibonacci extension targets and volume-based exits.
3. Trailing Stop — Trend-Following Exits
Dynamic stops that follow price upward but never move down.
Percentage trailing:
def percentage_trailing_stop(
current_price: float,
highest_since_entry: float,
trail_pct: float = 0.10,
) -> tuple[float, bool]:
"""Return (stop_level, triggered)."""
highest = max(highest_since_entry, current_price)
stop = highest * (1 - trail_pct)
return stop, current_price <= stopATR trailing (Chandelier Exit):
def chandelier_exit(
highs: list[float],
atr_value: float,
multiplier: float = 2.5,
lookback: int = 22,
) -> float:
"""Highest high over lookback minus ATR * multiplier."""
highest_high = max(highs[-lookback:])
return highest_high - (atr_value * multiplier)EMA trailing:
# Exit when close < EMA for M consecutive bars
ema = df.ta.ema(length=20)
below_ema = df["close"] < ema
consecutive_below = below_ema.rolling(3).sum() == 3 # 3 bars belowTypical EMA periods: 10 (scalp), 20 (day trade), 50 (swing).
See references/trailing_stops.md for Parabolic SAR, SuperTrend, and step trailing.
4. Time-Based Exits
Exit if the trade hasn't moved in your favor within a defined window.
bars_since_entry = current_bar - entry_bar
if bars_since_entry > max_hold_bars and current_pnl <= 0:
exit_reason = "time_stop"Guidelines:
- Scalp: 5–15 minutes
- Day trade: 4–8 hours
- Swing: 3–5 days
- PumpFun snipe: 2–10 minutes (token-specific)
Time stops prevent capital from sitting in dead trades.
5. Signal-Based Exits
Exit when the indicator that generated the entry signal reverses.
# RSI reversal exit
rsi = df.ta.rsi(length=14)
if position == "long" and rsi.iloc[-1] > 70:
exit_reason = "rsi_overbought"
# MACD crossover exit
macd = df.ta.macd()
if macd["MACDs_12_26_9"].iloc[-1] < macd["MACDh_12_26_9"].iloc[-1]:
exit_reason = "macd_bearish_cross"Signal exits work well when combined with trailing stops — the signal triggers tightening the trail rather than an immediate full exit.
6. Liquidity-Based Exits
Exit when volume or liquidity deteriorates, signaling reduced ability to exit cleanly.
recent_vol = df["volume"].rolling(10).mean().iloc[-1]
baseline_vol = df["volume"].rolling(50).mean().iloc[-1]
if recent_vol < baseline_vol * 0.3: # Volume dropped to 30% of baseline
exit_reason = "liquidity_deterioration"Critical for low-cap Solana tokens where liquidity can evaporate rapidly.
PumpFun-Specific Exit Rules
PumpFun tokens have unique dynamics requiring specialized exit logic.
Pre-Graduation Exits
Tokens on the bonding curve before reaching 85 SOL fill:
bonding_fill_pct = current_fill_sol / 85.0
if bonding_fill_pct > 0.90:
# Near graduation — decide: hold through or exit before
# Graduation creates volatility spike, both up and down
pass
if bonding_fill_pct < 0.50 and time_since_entry > 300: # 5 min
exit_reason = "stalled_bonding_curve"Volume Decay Exits
buy_vol_1m = get_buy_volume(token, "1m")
buy_vol_5m = get_buy_volume(token, "5m") / 5 # Normalize to per-minute
if buy_vol_1m < buy_vol_5m * 0.3:
exit_reason = "buy_volume_decay"Time Decay for PumpFun
Most PumpFun tokens that will succeed show momentum within the first few minutes:
| Timeframe | Action |
|---|---|
| 0–2 min | Hold — too early to judge |
| 2–5 min | Exit if no 2× from entry |
| 5–10 min | Exit if no 3× from entry |
| 10+ min | Should be trailing, not hoping |
Combining Exit Rules
A complete exit plan layers multiple rules. Here is a recommended template:
exit_plan = {
"hard_stop": {
"type": "fixed_percentage",
"value": 0.20, # -20% max loss
"priority": 1, # Checked first, always honored
},
"atr_stop": {
"type": "atr_trailing",
"multiplier": 2.5,
"atr_length": 14,
"priority": 2,
},
"take_profit": {
"type": "scaled",
"tranches": [
{"at_rr": 2, "sell_pct": 0.25},
{"at_rr": 4, "sell_pct": 0.25},
{"at_rr": 8, "sell_pct": 0.25},
],
"priority": 3,
},
"time_stop": {
"type": "max_bars",
"value": 50,
"condition": "if_not_profitable",
"priority": 4,
},
}Priority hierarchy: Hard stop > ATR trailing > Take profit > Time stop.
The hard stop is always active and never overridden. The ATR trailing stop activates after the first take-profit tranche fills. The time stop only fires if the trade is not yet profitable.
Common Exit Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| No stop loss | Unlimited downside | Always define max loss before entry |
| Moving stops wider | Increases risk after the fact | Never move stops away from price |
| Not taking profits | Winners become losers | Use scaled exits |
| All-or-nothing exits | Leaves money on the table or exits too early | Scale out in tranches |
| Round-number stops | Cluster with other traders, get hunted | Offset by small random amount |
| Too-tight stops | Stopped out by normal volatility | Use ATR-based stops |
| Hoping instead of trailing | Gives back profits | Activate trail after first TP |
| Ignoring liquidity | Cannot exit at intended price | Check spread and depth before sizing |
Integration with Other Skills
- `position-sizing` — Size the position based on the stop loss distance.
position_size = (account_risk * account_balance) / (entry - stop_loss)
- `risk-management` — Exits are the mechanism that enforces risk limits.
- `pandas-ta` — Use ATR, EMA, RSI, MACD for signal-based and trailing exits.
- `slippage-modeling` — Estimate execution cost of the exit to set realistic targets.
- `liquidity-analysis` — Verify exit liquidity before entering a position.
Files
References
references/stop_loss_methods.md— Complete stop loss methodology and anti-patternsreferences/take_profit_strategies.md— Scaled exits, R:R targets, Fibonacci extensionsreferences/trailing_stops.md— Trailing stop implementations and parameter guidance
Scripts
scripts/exit_simulator.py— Simulate and compare exit strategies on synthetic price datascripts/stop_loss_calculator.py— Calculate stop levels, position sizes, and R:R targets
Stop Loss Methods — Complete Reference
Overview
A stop loss is a predefined price level at which a position is closed to limit downside. Every trade must have a stop loss defined before entry. This reference covers all major stop loss methods, when to use each, and common anti-patterns.
Fixed Percentage Stops
Exit when the position declines by a fixed percentage from entry.
| Stop % | Use Case | Notes |
|---|---|---|
| 5% | Tight scalps on liquid pairs | Requires high win rate (>60%) |
| 10% | Day trades on mid-cap tokens | Good balance for moderate volatility |
| 15% | Swing trades on volatile tokens | Allows room for crypto noise |
| 20% | Wide stops for high-conviction plays | Max recommended for single trade |
Calculation:
def fixed_pct_stop(entry_price: float, stop_pct: float) -> float:
"""Calculate fixed percentage stop loss price.
Args:
entry_price: Price at entry.
stop_pct: Stop percentage as decimal (0.10 = 10%).
Returns:
Stop loss price.
"""
return entry_price * (1 - stop_pct)When to use: Simple setups where volatility analysis is not available or when you need a quick, predictable risk cap.
Limitation: Not adaptive to market conditions. A 10% stop may be too tight in high-volatility environments and too wide in low-volatility ones.
ATR-Based Stops
Use Average True Range to set stops that adapt to current volatility.
Core calculation:
def atr_stop(
entry_price: float,
atr_value: float,
multiplier: float = 2.0,
) -> float:
"""Calculate ATR-based stop loss.
Args:
entry_price: Price at entry.
atr_value: Current ATR(14) value.
multiplier: ATR multiplier (1.5 tight, 2.0 standard, 3.0 wide).
Returns:
Stop loss price.
"""
return entry_price - (atr_value * multiplier)Multiplier Guide
| Multiplier | Style | Win Rate Needed | Typical R:R |
|---|---|---|---|
| 1.0× | Very tight | >65% | 1:1 – 2:1 |
| 1.5× | Tight | >55% | 2:1 – 3:1 |
| 2.0× | Standard | >45% | 3:1 – 5:1 |
| 2.5× | Moderate | >40% | 4:1 – 6:1 |
| 3.0× | Wide | >35% | 5:1 – 10:1 |
ATR Period by Timeframe
| Timeframe | ATR Period | Rationale |
|---|---|---|
| 1-minute | ATR(7) | Fast adaptation to micro-volatility |
| 5-minute | ATR(10) | Balance of speed and stability |
| 15-minute | ATR(14) | Standard, most widely tested |
| 1-hour | ATR(14) | Standard |
| 4-hour | ATR(14–20) | Smoother, fewer whipsaws |
| Daily | ATR(14–20) | Standard swing trading period |
Example
Entry at $1.00, ATR(14) = $0.08:
- 1.5× ATR stop: $1.00 − ($0.08 × 1.5) = $0.88 (12% risk)
- 2.0× ATR stop: $1.00 − ($0.08 × 2.0) = $0.84 (16% risk)
- 3.0× ATR stop: $1.00 − ($0.08 × 3.0) = $0.76 (24% risk)
Volatility-Adjusted Stops
Adapt stop distance based on the _current volatility regime_ rather than a fixed multiplier.
import numpy as np
def volatility_adjusted_stop(
entry_price: float,
atr_value: float,
atr_history: list[float],
base_multiplier: float = 2.0,
) -> float:
"""Widen/tighten stop based on volatility percentile.
High volatility → wider stop. Low volatility → tighter stop.
"""
percentile = np.searchsorted(
np.sort(atr_history), atr_value
) / len(atr_history)
# Scale multiplier: 0.7× at low vol, 1.3× at high vol
vol_scale = 0.7 + (percentile * 0.6)
adjusted_mult = base_multiplier * vol_scale
return entry_price - (atr_value * adjusted_mult)This prevents being stopped out during volatility spikes while keeping stops tight during quiet markets.
Support-Based Stops
Place stop below the nearest significant support level.
Identification methods: 1. Swing low: Lowest low in the last N bars 2. Volume profile: Below the nearest high-volume node 3. Round numbers: Below psychological levels (with offset) 4. Previous resistance turned support: Below the flip level
def support_stop(
swing_low: float,
offset_pct: float = 0.02,
) -> float:
"""Place stop below swing low with offset to avoid stop hunts.
Args:
swing_low: Nearest swing low price.
offset_pct: Buffer below swing low (default 2%).
Returns:
Stop loss price.
"""
return swing_low * (1 - offset_pct)Important: Always add an offset below the support level. Placing stops exactly at support is where everyone else puts them — market makers and bots target these clusters.
Time Stops
Exit if the trade has not moved favorably within a defined number of bars.
def check_time_stop(
bars_held: int,
max_hold: int,
current_pnl_pct: float,
threshold: float = 0.0,
) -> bool:
"""Return True if time stop is triggered.
Only triggers if the trade is not meaningfully profitable.
"""
return bars_held >= max_hold and current_pnl_pct <= threshold| Style | Max Hold | Threshold |
|---|---|---|
| PumpFun snipe | 5–20 bars (1m) | Must be >+50% |
| Scalp | 15–30 bars (1m) | Must be >0% |
| Day trade | 20–50 bars (15m) | Must be >0% |
| Swing | 10–20 bars (4h) | Must be >breakeven |
Combining Stop Methods
Use a layered approach:
1. Hard stop (always active): Maximum acceptable loss — never violated. Typically 20% for crypto. 2. Indicator stop (primary): ATR-based or support-based. This is your actual stop for most trades. 3. Time stop (secondary): Eject dead trades that neither hit stop nor profit target.
def evaluate_stops(
entry_price: float,
current_price: float,
hard_stop: float,
indicator_stop: float,
bars_held: int,
max_hold: int,
) -> str | None:
"""Check all stop conditions in priority order."""
if current_price <= hard_stop:
return "hard_stop"
if current_price <= indicator_stop:
return "indicator_stop"
pnl_pct = (current_price - entry_price) / entry_price
if bars_held >= max_hold and pnl_pct <= 0:
return "time_stop"
return None # No stop triggeredStop Placement Anti-Patterns
Round Numbers
Placing stops at $1.00, $0.50, $10.00 etc. These are the most common stop levels and frequently targeted by large players.
Fix: Offset by 1–3% below the round number.
Too Tight
Stops inside the normal noise range of the asset. If ATR is 8% and your stop is 3%, you will be stopped out by random fluctuation.
Fix: Stop should be at minimum 1× ATR from entry. Ideally 1.5–2×.
Too Wide
Stops so far from entry that a single loss wipes out many winners.
Fix: If the required stop distance implies risk >5% of account, reduce position size rather than widening the stop.
Moving Stops Away from Price
Widening a stop after entry because the trade is going against you. This violates the original risk assessment.
Fix: The stop defined at entry is final. If it is hit, accept the loss.
No Stop at All
Hoping the trade recovers. In crypto, tokens can go to zero. A -50% loss requires a +100% gain to recover.
Fix: Every trade gets a stop. No exceptions.
Quick Reference
| Method | Formula | Adaptive | Complexity |
|---|---|---|---|
| Fixed % | entry × (1 − pct) | No | Low |
| ATR-based | entry − ATR × mult | Yes | Medium |
| Vol-adjusted | ATR + percentile scaling | Yes | Medium |
| Support-based | swing_low × (1 − offset) | Partially | Medium |
| Time stop | Bars held > max | N/A | Low |
| Combined | Priority stack | Yes | High |
Take Profit Strategies — Complete Reference
Overview
Take profit (TP) rules define where and how you lock in gains. Without TP rules, winning trades reverse into losses. This reference covers fixed R:R targets, scaled exits, market cap milestones, Fibonacci extensions, and volume-based exits.
Fixed Risk/Reward Targets
Methodology
1. Define risk: risk = entry_price - stop_loss_price 2. Set TP: tp = entry_price + (risk × R_ratio)
def calculate_rr_targets(
entry: float,
stop: float,
ratios: list[float] = [2.0, 3.0, 5.0],
) -> list[dict]:
"""Calculate take profit levels at specified R:R ratios.
Args:
entry: Entry price.
stop: Stop loss price.
ratios: List of risk/reward ratios.
Returns:
List of dicts with ratio, price, and gain percentage.
"""
risk = entry - stop
targets = []
for r in ratios:
tp_price = entry + (risk * r)
gain_pct = (tp_price - entry) / entry * 100
targets.append({
"ratio": f"{r:.1f}:1",
"price": round(tp_price, 6),
"gain_pct": round(gain_pct, 2),
})
return targetsRecommended Ratios by Style
| Trading Style | Min R:R | Target R:R | Notes |
|---|---|---|---|
| Scalp | 1.5:1 | 2:1 | High win rate compensates for lower R:R |
| Day trade | 2:1 | 3:1 | Standard for intraday setups |
| Swing trade | 3:1 | 5:1 | Wider stops need bigger targets |
| PumpFun snipe | 5:1 | 10:1+ | Low win rate, need big winners |
Break-Even Analysis
Minimum win rate needed to be profitable at each R:R (excluding fees):
| R:R | Required Win Rate |
|---|---|
| 1:1 | >50% |
| 2:1 | >33% |
| 3:1 | >25% |
| 5:1 | >17% |
| 10:1 | >9% |
This is why high R:R strategies can be profitable with low win rates.
Scaled Exit Framework
Selling the entire position at one level is suboptimal. Scaled exits capture profits progressively while keeping exposure to further upside.
Standard 4-Tranche Model
SCALED_EXIT_PLAN = [
{
"tranche": 1,
"sell_pct": 0.25,
"target_rr": 2.0,
"label": "Cover cost + small profit",
"post_action": "Move stop to breakeven",
},
{
"tranche": 2,
"sell_pct": 0.25,
"target_rr": 4.0,
"label": "Meaningful profit locked",
"post_action": "Trail stop at 2× entry",
},
{
"tranche": 3,
"sell_pct": 0.25,
"target_rr": 8.0,
"label": "Major profit captured",
"post_action": "Tighten trail to 10% from peak",
},
{
"tranche": 4,
"sell_pct": 0.25,
"target_rr": None,
"label": "Moonbag — trailing stop only",
"post_action": "Trail with EMA(20) or 15% from peak",
},
]Why Scaled Exits Work
- Tranche 1 removes psychological pressure — you have locked in some profit.
- Moving to breakeven after Tranche 1 makes the trade risk-free.
- Tranches 2–3 capture the majority of the move.
- Tranche 4 (moonbag) captures outlier moves at zero additional risk.
Aggressive Variant (PumpFun / Meme Tokens)
For extremely volatile tokens where moves are fast and violent:
AGGRESSIVE_PLAN = [
{"sell_pct": 0.50, "target_rr": 2.0, "label": "Half out, secure base"},
{"sell_pct": 0.25, "target_rr": 5.0, "label": "Quarter at 5×"},
{"sell_pct": 0.25, "target_rr": None, "label": "Trail with 20% stop"},
]Taking 50% off early is appropriate when the median outcome is a loss and you need to capitalize aggressively on winners.
Market Cap Milestone Exits
For PumpFun and meme tokens, absolute price targets are less meaningful than market cap milestones since these tokens start from near-zero.
MCAP_MILESTONES = [
{"mcap": 30_000, "sell_pct": 0.25, "note": "Cover entry cost"},
{"mcap": 100_000, "sell_pct": 0.25, "note": "Lock solid profit"},
{"mcap": 500_000, "sell_pct": 0.25, "note": "Major milestone"},
{"mcap": 1_000_000, "sell_pct": 0.25, "note": "Moonbag exit or trail"},
]Market Cap to Price Conversion
def mcap_to_price(target_mcap: float, total_supply: float) -> float:
"""Convert market cap target to token price."""
return target_mcap / total_supplyEnsure you use circulating supply, not total supply, for accurate mcap calculations. On PumpFun, total supply is typically 1 billion tokens.
Fibonacci Extension Targets
Use Fibonacci extensions from the initial impulse move to project take profit levels during continuation.
Key Extension Levels
| Level | Use |
|---|---|
| 1.000 | Measured move (100% extension) |
| 1.272 | Conservative TP |
| 1.618 | Standard TP — most commonly respected |
| 2.000 | Aggressive TP |
| 2.618 | Strong trend TP |
| 4.236 | Euphoric extension — rare but happens in crypto |
Calculation
def fibonacci_extensions(
swing_low: float,
swing_high: float,
pullback_low: float,
levels: list[float] = [1.0, 1.272, 1.618, 2.618, 4.236],
) -> list[dict]:
"""Calculate Fibonacci extension levels.
Args:
swing_low: Start of the impulse move.
swing_high: End of the impulse move.
pullback_low: Low of the retracement.
levels: Extension ratios to calculate.
Returns:
List of dicts with level and price.
"""
impulse = swing_high - swing_low
results = []
for lvl in levels:
price = pullback_low + (impulse * lvl)
results.append({"level": lvl, "price": round(price, 6)})
return resultsExample
Swing low: $0.001, Swing high: $0.010, Pullback: $0.006
- 1.000 ext: $0.006 + $0.009 × 1.000 = $0.015
- 1.618 ext: $0.006 + $0.009 × 1.618 = $0.0206
- 2.618 ext: $0.006 + $0.009 × 2.618 = $0.0296
Volume-Based Exits
Exit when buying pressure fades, regardless of price level.
def check_volume_exit(
recent_volume: float,
baseline_volume: float,
threshold: float = 0.3,
) -> bool:
"""Signal exit when recent volume drops below threshold of baseline.
Args:
recent_volume: Average volume over last N bars (e.g., 5).
baseline_volume: Average volume over longer period (e.g., 50).
threshold: Ratio below which volume is considered weak.
Returns:
True if volume exit should trigger.
"""
if baseline_volume == 0:
return False
return recent_volume < (baseline_volume * threshold)Volume exits are especially important for:
- Meme tokens where volume drives 100% of price action
- Low-float tokens where a few large sellers can crash price
- Tokens post-pump where volume naturally decays
Combining Take Profit Methods
The most robust approach combines multiple methods:
1. Primary TP: Scaled R:R exits (Tranches 1–3) 2. Moonbag TP: Trailing stop on final tranche 3. Override: Volume-based exit if volume collapses before TP is reached 4. Ceiling: Fibonacci 2.618 or 4.236 as maximum expectation
The scaled exit handles the common case. The volume exit protects against momentum death. The Fibonacci extension provides a reality check on targets.
Trailing Stops — Complete Reference
Overview
A trailing stop follows price in the favorable direction and never moves against the position. When price reverses by the trail amount, the position is closed. Trailing stops let winners run while protecting accumulated profit.
Core rule: Trailing stops only move _up_ for longs (and _down_ for shorts). They never widen.
Percentage Trailing
The simplest trailing stop. Trail a fixed percentage below the highest price since entry.
def percentage_trail(
prices: list[float],
entry_price: float,
trail_pct: float = 0.10,
) -> dict:
"""Simulate a percentage trailing stop.
Args:
prices: List of close prices after entry.
entry_price: Price at entry.
trail_pct: Trail distance as decimal (0.10 = 10%).
Returns:
Dict with exit_bar, exit_price, peak_price, pnl_pct.
"""
peak = entry_price
for i, price in enumerate(prices):
peak = max(peak, price)
stop_level = peak * (1 - trail_pct)
if price <= stop_level:
pnl = (price - entry_price) / entry_price
return {
"exit_bar": i,
"exit_price": price,
"peak_price": peak,
"pnl_pct": round(pnl * 100, 2),
}
pnl = (prices[-1] - entry_price) / entry_price
return {
"exit_bar": len(prices) - 1,
"exit_price": prices[-1],
"peak_price": peak,
"pnl_pct": round(pnl * 100, 2),
}Recommended Trail Percentages
| Style | Trail % | Rationale |
|---|---|---|
| Scalp | 3–5% | Tight, locks quick gains |
| Day trade | 7–12% | Room for intraday noise |
| Swing trade | 15–25% | Accommodates multi-day swings |
| Meme/PumpFun | 20–35% | Extreme volatility needs wide trail |
Tip: If the trail is tighter than 1× ATR, you will be stopped by noise.
ATR Trailing (Chandelier Exit)
Uses ATR to set a volatility-adaptive trailing distance. Named "Chandelier" because it hangs from the highest high.
def chandelier_exit(
highs: list[float],
closes: list[float],
atr_values: list[float],
multiplier: float = 2.5,
lookback: int = 22,
) -> dict:
"""Calculate Chandelier Exit trailing stop.
Args:
highs: High prices.
closes: Close prices.
atr_values: ATR values aligned with price bars.
multiplier: ATR multiplier.
lookback: Period for highest high.
Returns:
Dict with exit info or None if not triggered.
"""
for i in range(lookback, len(closes)):
highest_high = max(highs[i - lookback : i + 1])
stop = highest_high - (atr_values[i] * multiplier)
if closes[i] <= stop:
entry_approx = closes[lookback]
pnl = (closes[i] - entry_approx) / entry_approx
return {
"exit_bar": i,
"exit_price": closes[i],
"stop_level": stop,
"pnl_pct": round(pnl * 100, 2),
}
return NoneRecommended Parameters
| Parameter | Default | Crypto Adjustment |
|---|---|---|
| ATR period | 14 | 10 (faster adaptation) |
| Multiplier | 3.0 | 2.5 (tighter for 24/7 markets) |
| Lookback | 22 | 20 (fewer trading days concept) |
Why ATR trailing is superior to fixed %: A 10% trail might be tight for a token with 15% daily swings but excessively wide for a stablecoin. ATR adapts automatically.
Parabolic SAR
The Parabolic Stop and Reverse (SAR) uses an acceleration factor that tightens the stop as the trend progresses.
Parameters
| Parameter | Standard | Crypto Recommended |
|---|---|---|
| AF start | 0.02 | 0.01 |
| AF increment | 0.02 | 0.01 |
| AF max | 0.20 | 0.15 |
Lower AF values for crypto because:
- Crypto has higher noise-to-signal ratio
- Standard parameters trigger too frequently
- The slower acceleration gives trends more room
Usage with pandas-ta
import pandas_ta as ta
# Standard parameters
sar_standard = df.ta.psar(af0=0.02, af=0.02, max_af=0.2)
# Crypto-adjusted parameters
sar_crypto = df.ta.psar(af0=0.01, af=0.01, max_af=0.15)
# Exit when price crosses below the SAR (for longs)
exit_signal = df["close"] < sar_crypto["PSARl_0.01_0.15"]Strengths and Weaknesses
- Strength: Self-tightening — the longer the trend, the closer the stop.
- Strength: No lookback period needed.
- Weakness: Whipsaws in ranging markets.
- Weakness: Cannot be used in isolation — combine with trend filter.
EMA Trailing
Exit when price closes below an EMA for M consecutive bars.
def ema_trailing_stop(
closes: list[float],
ema_values: list[float],
consecutive_required: int = 2,
) -> dict | None:
"""Exit on M consecutive closes below EMA.
Args:
closes: Close prices.
ema_values: EMA values aligned with closes.
consecutive_required: Number of consecutive closes below EMA.
Returns:
Exit info dict or None.
"""
below_count = 0
for i in range(len(closes)):
if closes[i] < ema_values[i]:
below_count += 1
if below_count >= consecutive_required:
return {
"exit_bar": i,
"exit_price": closes[i],
"ema_value": ema_values[i],
}
else:
below_count = 0
return NoneEMA Period Selection
| Style | EMA Period | Consecutive Bars | Rationale |
|---|---|---|---|
| Scalp | EMA(8–10) | 1–2 | Fast reaction |
| Day trade | EMA(20) | 2–3 | Filters intrabar noise |
| Swing | EMA(50) | 2–3 | Major trend following |
| Position | EMA(100–200) | 3–5 | Very slow, big picture |
Requiring consecutive bars is critical. A single close below the EMA in a strong uptrend is normal — requiring 2–3 confirms the trend has actually changed.
SuperTrend as Trailing Stop
SuperTrend combines ATR with a directional component. It stays below price in uptrends and above price in downtrends.
Calculation
def supertrend(
highs: list[float],
lows: list[float],
closes: list[float],
atr_values: list[float],
multiplier: float = 3.0,
) -> list[float]:
"""Calculate SuperTrend values.
Args:
highs, lows, closes: OHLC data.
atr_values: ATR values.
multiplier: ATR multiplier.
Returns:
List of SuperTrend stop levels.
"""
st = [0.0] * len(closes)
for i in range(1, len(closes)):
mid = (highs[i] + lows[i]) / 2
upper = mid + (atr_values[i] * multiplier)
lower = mid - (atr_values[i] * multiplier)
# Lower band only moves up
if closes[i - 1] > st[i - 1]:
st[i] = max(lower, st[i - 1]) if closes[i] > lower else upper
else:
st[i] = min(upper, st[i - 1]) if closes[i] < upper else lower
return stRecommended Parameters
| Volatility Regime | ATR Period | Multiplier |
|---|---|---|
| Low | 10 | 2.0 |
| Normal | 10 | 3.0 |
| High | 10 | 4.0 |
Step Trailing (Ratchet Stop)
Move the stop up in discrete steps as the trade hits milestones.
RATCHET_LEVELS = [
{"trigger_rr": 2.0, "stop_to": "breakeven"},
{"trigger_rr": 3.0, "stop_to": "1r_profit"},
{"trigger_rr": 5.0, "stop_to": "3r_profit"},
{"trigger_rr": 10.0, "stop_to": "7r_profit"},
]| When Price Reaches | Move Stop To | Locked Profit |
|---|---|---|
| 2× risk | Breakeven (entry) | 0 (risk-free) |
| 3× risk | 1× risk above entry | 1R locked |
| 5× risk | 3× risk above entry | 3R locked |
| 10× risk | 7× risk above entry | 7R locked |
Advantage: Simple mental model. You always know exactly where your stop is. Disadvantage: Leaves more on the table than smooth trailing in strong trends.
Implementation Notes
- Check on bar close, not intra-bar. Wicks frequently violate stop levels
then recover. Use closing price to avoid false triggers.
- Activation delay. Wait until 1R profit before enabling the trail. This
prevents being stopped by initial noise after entry.
- Combine fast + slow trails. Use a fast trail (8% or ATR×1.5) for partial
exit (50%) and a slow trail (20% or ATR×3.0) for the moonbag remainder.
#!/usr/bin/env python3
"""Simulate and compare multiple exit strategies on synthetic price data.
Generates synthetic trade scenarios (winning and losing) and applies five
different exit strategies to each. Prints a comparison table showing which
strategy captured the most profit or minimized loss.
This script is for informational analysis only — not financial advice.
Usage:
python scripts/exit_simulator.py
Dependencies:
uv pip install pandas numpy
"""
import sys
from typing import Optional
import numpy as np
import pandas as pd
# ── Configuration ───────────────────────────────────────────────────
ENTRY_PRICE = 1.0
NUM_BARS = 200
SEED = 42
# Strategy parameters
FIXED_STOP_PCT = 0.10 # 10% fixed stop loss
ATR_TRAIL_MULT = 2.5 # ATR multiplier for trailing
ATR_PERIOD = 14 # ATR lookback
EMA_PERIOD = 20 # EMA trailing period
EMA_CONSEC = 2 # Consecutive closes below EMA
TIME_STOP_BARS = 50 # Max bars before time stop
SCALED_TARGETS_RR = [2.0, 3.0, 5.0] # R:R for scaled exits
SCALED_SELL_PCTS = [0.25, 0.25, 0.25] # 25% at each target, 25% trails
# ── Price Generation ────────────────────────────────────────────────
def generate_price_series(
entry: float,
n_bars: int,
trend: float = 0.0,
volatility: float = 0.03,
seed: Optional[int] = None,
) -> pd.DataFrame:
"""Generate synthetic OHLC price data for simulation.
Args:
entry: Starting price.
n_bars: Number of bars to generate.
trend: Drift per bar (positive = uptrend).
volatility: Per-bar volatility (std dev of returns).
seed: Random seed for reproducibility.
Returns:
DataFrame with open, high, low, close columns.
"""
rng = np.random.default_rng(seed)
returns = rng.normal(trend, volatility, n_bars)
closes = entry * np.cumprod(1 + returns)
# Generate OHLC from closes
highs = closes * (1 + rng.uniform(0, volatility, n_bars))
lows = closes * (1 - rng.uniform(0, volatility, n_bars))
opens = np.roll(closes, 1)
opens[0] = entry
df = pd.DataFrame({
"open": opens,
"high": highs,
"low": lows,
"close": closes,
})
# Calculate ATR
tr1 = df["high"] - df["low"]
tr2 = (df["high"] - df["close"].shift(1)).abs()
tr3 = (df["low"] - df["close"].shift(1)).abs()
df["tr"] = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
df["atr"] = df["tr"].rolling(ATR_PERIOD).mean()
df["atr"] = df["atr"].bfill()
# Calculate EMA
df["ema"] = df["close"].ewm(span=EMA_PERIOD, adjust=False).mean()
return df
# ── Exit Strategy Implementations ───────────────────────────────────
def fixed_stop_loss(
df: pd.DataFrame,
entry: float,
stop_pct: float,
) -> dict:
"""Fixed percentage stop loss — exit when price drops X% from entry.
Args:
df: OHLC DataFrame.
entry: Entry price.
stop_pct: Stop loss percentage as decimal.
Returns:
Dict with strategy results.
"""
stop_price = entry * (1 - stop_pct)
for i, row in df.iterrows():
if row["close"] <= stop_price:
pnl = (row["close"] - entry) / entry * 100
return {
"strategy": f"Fixed Stop ({stop_pct:.0%})",
"exit_bar": i,
"exit_price": round(row["close"], 6),
"exit_reason": "stop_loss",
"pnl_pct": round(pnl, 2),
"peak_price": round(df["close"].iloc[: i + 1].max(), 6),
}
# Never triggered — return final bar
final = df["close"].iloc[-1]
pnl = (final - entry) / entry * 100
return {
"strategy": f"Fixed Stop ({stop_pct:.0%})",
"exit_bar": len(df) - 1,
"exit_price": round(final, 6),
"exit_reason": "end_of_data",
"pnl_pct": round(pnl, 2),
"peak_price": round(df["close"].max(), 6),
}
def atr_trailing_stop(
df: pd.DataFrame,
entry: float,
multiplier: float,
) -> dict:
"""ATR-based trailing stop.
Trail = highest_close - ATR * multiplier. Only moves up.
Args:
df: OHLC DataFrame with 'atr' column.
entry: Entry price.
multiplier: ATR multiplier.
Returns:
Dict with strategy results.
"""
peak = entry
trail_stop = entry - (df["atr"].iloc[0] * multiplier)
for i, row in df.iterrows():
peak = max(peak, row["close"])
new_stop = peak - (row["atr"] * multiplier)
trail_stop = max(trail_stop, new_stop) # Only moves up
if row["close"] <= trail_stop:
pnl = (row["close"] - entry) / entry * 100
return {
"strategy": f"ATR Trail ({multiplier}x)",
"exit_bar": i,
"exit_price": round(row["close"], 6),
"exit_reason": "trailing_stop",
"pnl_pct": round(pnl, 2),
"peak_price": round(peak, 6),
}
final = df["close"].iloc[-1]
pnl = (final - entry) / entry * 100
return {
"strategy": f"ATR Trail ({multiplier}x)",
"exit_bar": len(df) - 1,
"exit_price": round(final, 6),
"exit_reason": "end_of_data",
"pnl_pct": round(pnl, 2),
"peak_price": round(peak, 6),
}
def scaled_exits(
df: pd.DataFrame,
entry: float,
stop_loss_price: float,
targets_rr: list[float],
sell_pcts: list[float],
trail_pct: float = 0.15,
) -> dict:
"""Scaled exit strategy — sell tranches at R:R targets, trail remainder.
Args:
df: OHLC DataFrame.
entry: Entry price.
stop_loss_price: Initial stop loss price.
targets_rr: R:R ratios for each tranche.
sell_pcts: Fraction to sell at each target.
trail_pct: Trail percentage for the final moonbag.
Returns:
Dict with strategy results.
"""
risk = entry - stop_loss_price
tp_prices = [entry + (risk * rr) for rr in targets_rr]
remaining = 1.0
realized_pnl = 0.0
current_stop = stop_loss_price
tranche_idx = 0
peak = entry
exits_log: list[str] = []
for i, row in df.iterrows():
price = row["close"]
peak = max(peak, price)
# Check stop loss on remaining position
if price <= current_stop and remaining > 0:
exit_pnl = (price - entry) / entry * remaining * 100
realized_pnl += exit_pnl
exits_log.append(f"Bar {i}: Stop hit, sold {remaining:.0%} at {price:.4f}")
remaining = 0.0
return {
"strategy": "Scaled Exits",
"exit_bar": i,
"exit_price": round(price, 6),
"exit_reason": "stop_loss" if not exits_log[:-1] else "trailing_stop",
"pnl_pct": round(realized_pnl, 2),
"peak_price": round(peak, 6),
"detail": "; ".join(exits_log),
}
# Check take profit tranches
if tranche_idx < len(tp_prices) and price >= tp_prices[tranche_idx]:
sell_frac = sell_pcts[tranche_idx]
tranche_pnl = (price - entry) / entry * sell_frac * 100
realized_pnl += tranche_pnl
remaining -= sell_frac
exits_log.append(
f"Bar {i}: TP{tranche_idx + 1} hit ({targets_rr[tranche_idx]}R), "
f"sold {sell_frac:.0%} at {price:.4f}"
)
# Move stop up after each tranche
if tranche_idx == 0:
current_stop = entry # Breakeven
else:
current_stop = entry + (risk * targets_rr[tranche_idx - 1])
tranche_idx += 1
# Trail the moonbag
if tranche_idx >= len(tp_prices) and remaining > 0:
trail_stop = peak * (1 - trail_pct)
current_stop = max(current_stop, trail_stop)
# End of data — close remaining
if remaining > 0:
final = df["close"].iloc[-1]
exit_pnl = (final - entry) / entry * remaining * 100
realized_pnl += exit_pnl
exits_log.append(f"Bar {len(df) - 1}: EOD, sold {remaining:.0%} at {final:.4f}")
return {
"strategy": "Scaled Exits",
"exit_bar": len(df) - 1,
"exit_price": round(df["close"].iloc[-1], 6),
"exit_reason": "end_of_data",
"pnl_pct": round(realized_pnl, 2),
"peak_price": round(peak, 6),
"detail": "; ".join(exits_log),
}
def ema_trailing(
df: pd.DataFrame,
entry: float,
consec_bars: int = 2,
) -> dict:
"""EMA trailing stop — exit on M consecutive closes below EMA.
Args:
df: OHLC DataFrame with 'ema' column.
entry: Entry price.
consec_bars: Required consecutive closes below EMA.
Returns:
Dict with strategy results.
"""
below_count = 0
peak = entry
for i, row in df.iterrows():
peak = max(peak, row["close"])
if row["close"] < row["ema"]:
below_count += 1
if below_count >= consec_bars:
pnl = (row["close"] - entry) / entry * 100
return {
"strategy": f"EMA({EMA_PERIOD}) Trail",
"exit_bar": i,
"exit_price": round(row["close"], 6),
"exit_reason": "ema_cross",
"pnl_pct": round(pnl, 2),
"peak_price": round(peak, 6),
}
else:
below_count = 0
final = df["close"].iloc[-1]
pnl = (final - entry) / entry * 100
return {
"strategy": f"EMA({EMA_PERIOD}) Trail",
"exit_bar": len(df) - 1,
"exit_price": round(final, 6),
"exit_reason": "end_of_data",
"pnl_pct": round(pnl, 2),
"peak_price": round(peak, 6),
}
def time_stop(
df: pd.DataFrame,
entry: float,
max_bars: int,
) -> dict:
"""Time-based stop — exit after N bars if not profitable.
Args:
df: OHLC DataFrame.
entry: Entry price.
max_bars: Maximum bars to hold.
Returns:
Dict with strategy results.
"""
for i, row in df.iterrows():
if i >= max_bars:
pnl_pct = (row["close"] - entry) / entry * 100
if pnl_pct <= 0:
return {
"strategy": f"Time Stop ({max_bars} bars)",
"exit_bar": i,
"exit_price": round(row["close"], 6),
"exit_reason": "time_stop",
"pnl_pct": round(pnl_pct, 2),
"peak_price": round(df["close"].iloc[: i + 1].max(), 6),
}
final = df["close"].iloc[-1]
pnl = (final - entry) / entry * 100
return {
"strategy": f"Time Stop ({max_bars} bars)",
"exit_bar": len(df) - 1,
"exit_price": round(final, 6),
"exit_reason": "profitable_hold",
"pnl_pct": round(pnl, 2),
"peak_price": round(df["close"].max(), 6),
}
# ── Scenario Runner ─────────────────────────────────────────────────
def run_scenario(
name: str,
trend: float,
volatility: float,
seed: int,
) -> list[dict]:
"""Run all exit strategies on a generated price scenario.
Args:
name: Scenario name for display.
trend: Per-bar drift.
volatility: Per-bar volatility.
seed: Random seed.
Returns:
List of result dicts from each strategy.
"""
df = generate_price_series(
entry=ENTRY_PRICE,
n_bars=NUM_BARS,
trend=trend,
volatility=volatility,
seed=seed,
)
stop_for_scaled = ENTRY_PRICE * (1 - FIXED_STOP_PCT)
results = [
fixed_stop_loss(df, ENTRY_PRICE, FIXED_STOP_PCT),
atr_trailing_stop(df, ENTRY_PRICE, ATR_TRAIL_MULT),
scaled_exits(
df, ENTRY_PRICE, stop_for_scaled,
SCALED_TARGETS_RR, SCALED_SELL_PCTS,
),
ema_trailing(df, ENTRY_PRICE, EMA_CONSEC),
time_stop(df, ENTRY_PRICE, TIME_STOP_BARS),
]
return results
def print_results(scenario_name: str, results: list[dict]) -> None:
"""Print formatted comparison table for a scenario.
Args:
scenario_name: Name of the scenario.
results: List of strategy result dicts.
"""
print(f"\n{'=' * 80}")
print(f" SCENARIO: {scenario_name}")
print(f" Entry Price: {ENTRY_PRICE}")
print(f"{'=' * 80}")
print(
f" {'Strategy':<22} {'Exit Bar':>8} {'Exit Price':>11} "
f"{'P&L %':>8} {'Peak':>11} {'Reason':<18}"
)
print(f" {'-' * 78}")
for r in results:
print(
f" {r['strategy']:<22} {r['exit_bar']:>8} "
f"{r['exit_price']:>11.6f} {r['pnl_pct']:>+8.2f}% "
f"{r['peak_price']:>11.6f} {r['exit_reason']:<18}"
)
# Summary
best = max(results, key=lambda x: x["pnl_pct"])
worst = min(results, key=lambda x: x["pnl_pct"])
print(f"\n Best: {best['strategy']} ({best['pnl_pct']:+.2f}%)")
print(f" Worst: {worst['strategy']} ({worst['pnl_pct']:+.2f}%)")
print(f" Spread: {best['pnl_pct'] - worst['pnl_pct']:.2f} percentage points")
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run all scenarios and print comparison tables."""
scenarios = [
{
"name": "Strong Uptrend (easy winner)",
"trend": 0.003,
"volatility": 0.025,
"seed": 42,
},
{
"name": "Choppy Uptrend (noisy winner)",
"trend": 0.001,
"volatility": 0.04,
"seed": 123,
},
{
"name": "Pump and Dump (spike then crash)",
"trend": 0.008,
"volatility": 0.05,
"seed": 77,
},
{
"name": "Slow Bleed (gradual loser)",
"trend": -0.002,
"volatility": 0.02,
"seed": 99,
},
{
"name": "Sideways Chop (no trend)",
"trend": 0.0,
"volatility": 0.03,
"seed": 55,
},
]
print("\n" + "=" * 80)
print(" EXIT STRATEGY SIMULATOR")
print(" Comparing 5 exit strategies across 5 market scenarios")
print(" For informational analysis only — not financial advice")
print("=" * 80)
all_results: dict[str, list[dict]] = {}
for scenario in scenarios:
results = run_scenario(
scenario["name"],
scenario["trend"],
scenario["volatility"],
scenario["seed"],
)
print_results(scenario["name"], results)
all_results[scenario["name"]] = results
# ── Cross-Scenario Summary ──────────────────────────────────────
print(f"\n{'=' * 80}")
print(" CROSS-SCENARIO SUMMARY")
print(f"{'=' * 80}")
strategy_names = [r["strategy"] for r in list(all_results.values())[0]]
print(f"\n {'Strategy':<22}", end="")
for s in scenarios:
short_name = s["name"].split("(")[0].strip()[:12]
print(f" {short_name:>12}", end="")
print(f" {'Avg P&L':>10}")
print(f" {'-' * (22 + 12 * len(scenarios) + 10)}")
for idx, strat in enumerate(strategy_names):
pnls = [all_results[s["name"]][idx]["pnl_pct"] for s in scenarios]
avg_pnl = sum(pnls) / len(pnls)
print(f" {strat:<22}", end="")
for pnl in pnls:
print(f" {pnl:>+11.2f}%", end="")
print(f" {avg_pnl:>+9.2f}%")
print(f"\n Note: Results depend on synthetic data parameters and random seeds.")
print(f" Real-world performance will vary based on market conditions.\n")
if __name__ == "__main__":
try:
main()
except ImportError as e:
print(f"Missing dependency: {e}")
print("Install with: uv pip install pandas numpy")
sys.exit(1)
#!/usr/bin/env python3
"""Calculate stop loss levels, position sizes, and R:R targets for a trade.
Takes entry price, ATR value, and account size as inputs and computes
stop loss levels using multiple methods. For each stop level, shows the
risk amount, position size for a 2% account risk, and R:R target prices.
This script is for informational analysis only — not financial advice.
Usage:
python scripts/stop_loss_calculator.py
Or with custom values:
ENTRY_PRICE=0.05 ATR_VALUE=0.004 ACCOUNT_SIZE=100 python scripts/stop_loss_calculator.py
Dependencies:
None (pure math, no external packages)
Environment Variables:
ENTRY_PRICE: Token price at entry (default: 0.001 SOL)
ATR_VALUE: Current ATR(14) value (default: 0.00008)
ACCOUNT_SIZE: Account size in SOL (default: 50.0)
RISK_PCT: Account risk percentage as decimal (default: 0.02 = 2%)
"""
import os
import sys
from typing import Optional
# ── Configuration ───────────────────────────────────────────────────
ENTRY_PRICE = float(os.getenv("ENTRY_PRICE", "0.001"))
ATR_VALUE = float(os.getenv("ATR_VALUE", "0.00008"))
ACCOUNT_SIZE = float(os.getenv("ACCOUNT_SIZE", "50.0"))
RISK_PCT = float(os.getenv("RISK_PCT", "0.02"))
# Fixed percentage stop levels
FIXED_STOP_PCTS = [0.05, 0.10, 0.15, 0.20]
# ATR multipliers
ATR_MULTIPLIERS = [1.5, 2.0, 2.5, 3.0]
# R:R ratios for target calculation
RR_RATIOS = [2.0, 3.0, 5.0, 10.0]
# ── Core Functions ──────────────────────────────────────────────────
def calculate_fixed_stop(
entry_price: float,
stop_pct: float,
) -> dict:
"""Calculate a fixed percentage stop loss.
Args:
entry_price: Price at entry.
stop_pct: Stop percentage as decimal (0.10 = 10%).
Returns:
Dict with stop details.
"""
stop_price = entry_price * (1 - stop_pct)
risk_per_unit = entry_price - stop_price
return {
"method": f"Fixed {stop_pct:.0%}",
"stop_price": stop_price,
"risk_per_unit": risk_per_unit,
"risk_pct": stop_pct * 100,
}
def calculate_atr_stop(
entry_price: float,
atr_value: float,
multiplier: float,
) -> dict:
"""Calculate an ATR-based stop loss.
Args:
entry_price: Price at entry.
atr_value: Current ATR value.
multiplier: ATR multiplier.
Returns:
Dict with stop details.
"""
risk_per_unit = atr_value * multiplier
stop_price = entry_price - risk_per_unit
risk_pct = (risk_per_unit / entry_price) * 100
return {
"method": f"ATR {multiplier:.1f}x",
"stop_price": max(stop_price, 0), # Price cannot be negative
"risk_per_unit": risk_per_unit,
"risk_pct": risk_pct,
}
def calculate_position_size(
account_size: float,
risk_pct: float,
risk_per_unit: float,
) -> dict:
"""Calculate position size to risk exactly X% of account.
Args:
account_size: Total account value in SOL.
risk_pct: Fraction of account to risk (0.02 = 2%).
risk_per_unit: Price difference between entry and stop.
Returns:
Dict with sizing details.
"""
if risk_per_unit <= 0:
return {
"risk_amount_sol": 0,
"position_size_units": 0,
"position_size_sol": 0,
"position_pct_of_account": 0,
}
risk_amount = account_size * risk_pct
position_units = risk_amount / risk_per_unit
position_sol = position_units * ENTRY_PRICE
position_pct = (position_sol / account_size) * 100
return {
"risk_amount_sol": round(risk_amount, 6),
"position_size_units": round(position_units, 2),
"position_size_sol": round(position_sol, 6),
"position_pct_of_account": round(position_pct, 2),
}
def calculate_rr_targets(
entry_price: float,
risk_per_unit: float,
ratios: list[float],
) -> list[dict]:
"""Calculate take profit prices at given R:R ratios.
Args:
entry_price: Price at entry.
risk_per_unit: Distance from entry to stop.
ratios: List of R:R ratios.
Returns:
List of dicts with target price and gain percentage.
"""
targets = []
for ratio in ratios:
tp_price = entry_price + (risk_per_unit * ratio)
gain_pct = (tp_price - entry_price) / entry_price * 100
targets.append({
"ratio": f"{ratio:.0f}:1",
"price": tp_price,
"gain_pct": round(gain_pct, 2),
})
return targets
# ── Display Functions ───────────────────────────────────────────────
def print_header() -> None:
"""Print the calculator header with input values."""
print("\n" + "=" * 80)
print(" STOP LOSS & POSITION SIZE CALCULATOR")
print(" For informational analysis only — not financial advice")
print("=" * 80)
print(f"\n Entry Price: {ENTRY_PRICE:.8f} SOL")
print(f" ATR(14) Value: {ATR_VALUE:.8f}")
print(f" Account Size: {ACCOUNT_SIZE:.2f} SOL")
print(f" Risk Per Trade: {RISK_PCT:.0%} ({ACCOUNT_SIZE * RISK_PCT:.4f} SOL)")
print()
def print_stop_table(stops: list[dict]) -> None:
"""Print formatted stop loss comparison table.
Args:
stops: List of stop detail dicts.
"""
print(f" {'Method':<14} {'Stop Price':>14} {'Risk/Unit':>14} {'Risk %':>8}")
print(f" {'-' * 52}")
for s in stops:
print(
f" {s['method']:<14} "
f"{s['stop_price']:>14.8f} "
f"{s['risk_per_unit']:>14.8f} "
f"{s['risk_pct']:>7.2f}%"
)
def print_sizing_table(stops: list[dict]) -> None:
"""Print position sizing for each stop method.
Args:
stops: List of stop detail dicts.
"""
print(f"\n POSITION SIZING (to risk exactly {RISK_PCT:.0%} of account)")
print(f" {'-' * 72}")
print(
f" {'Method':<14} {'Risk (SOL)':>11} {'Units':>14} "
f"{'Size (SOL)':>12} {'% of Acct':>10}"
)
print(f" {'-' * 72}")
for s in stops:
sizing = calculate_position_size(
ACCOUNT_SIZE, RISK_PCT, s["risk_per_unit"]
)
print(
f" {s['method']:<14} "
f"{sizing['risk_amount_sol']:>11.6f} "
f"{sizing['position_size_units']:>14.2f} "
f"{sizing['position_size_sol']:>12.6f} "
f"{sizing['position_pct_of_account']:>9.2f}%"
)
def print_rr_table(stops: list[dict]) -> None:
"""Print R:R target prices for each stop method.
Args:
stops: List of stop detail dicts.
"""
print(f"\n R:R TARGET PRICES")
print(f" {'-' * 72}")
header = f" {'Method':<14}"
for ratio in RR_RATIOS:
header += f" {ratio:.0f}:1 Target "
print(header)
print(f" {'-' * 72}")
for s in stops:
targets = calculate_rr_targets(
ENTRY_PRICE, s["risk_per_unit"], RR_RATIOS
)
line = f" {s['method']:<14}"
for t in targets:
line += f" {t['price']:>10.8f} "
print(line)
# Print gain percentages for the first stop method as reference
print(f"\n Gain % at each R:R (using {stops[0]['method']}):")
ref_targets = calculate_rr_targets(
ENTRY_PRICE, stops[0]["risk_per_unit"], RR_RATIOS
)
for t in ref_targets:
print(f" {t['ratio']:>5} → {t['price']:.8f} ({t['gain_pct']:>+.2f}%)")
def print_quick_reference() -> None:
"""Print quick reference guide."""
print(f"\n QUICK REFERENCE")
print(f" {'-' * 60}")
print(f" Rule of thumb for stop distance:")
print(f" Scalp: 1.0-1.5× ATR = {ATR_VALUE * 1.0:.8f} - {ATR_VALUE * 1.5:.8f}")
print(f" Day trade: 1.5-2.0× ATR = {ATR_VALUE * 1.5:.8f} - {ATR_VALUE * 2.0:.8f}")
print(f" Swing: 2.0-3.0× ATR = {ATR_VALUE * 2.0:.8f} - {ATR_VALUE * 3.0:.8f}")
print()
print(f" Maximum position size guidelines:")
print(f" Conservative: risk 1% = {ACCOUNT_SIZE * 0.01:.4f} SOL")
print(f" Standard: risk 2% = {ACCOUNT_SIZE * 0.02:.4f} SOL")
print(f" Aggressive: risk 5% = {ACCOUNT_SIZE * 0.05:.4f} SOL")
print()
print(f" Note: If position size exceeds 20% of account, consider")
print(f" widening the stop or reducing the position.")
print()
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run the stop loss calculator and print all tables."""
# Validate inputs
if ENTRY_PRICE <= 0:
print("Error: ENTRY_PRICE must be positive.")
sys.exit(1)
if ATR_VALUE <= 0:
print("Error: ATR_VALUE must be positive.")
sys.exit(1)
if ACCOUNT_SIZE <= 0:
print("Error: ACCOUNT_SIZE must be positive.")
sys.exit(1)
if not 0 < RISK_PCT < 1:
print("Error: RISK_PCT must be between 0 and 1.")
sys.exit(1)
print_header()
# Calculate all stops
fixed_stops = [
calculate_fixed_stop(ENTRY_PRICE, pct)
for pct in FIXED_STOP_PCTS
]
atr_stops = [
calculate_atr_stop(ENTRY_PRICE, ATR_VALUE, mult)
for mult in ATR_MULTIPLIERS
]
all_stops = fixed_stops + atr_stops
# Print tables
print(" FIXED PERCENTAGE STOPS")
print_stop_table(fixed_stops)
print(f"\n ATR-BASED STOPS (ATR = {ATR_VALUE:.8f})")
print_stop_table(atr_stops)
print_sizing_table(all_stops)
print_rr_table(all_stops)
print_quick_reference()
if __name__ == "__main__":
main()
Related skills
FAQ
What are the exit categories?
Stop loss, take profit, trailing stop, time-based, signal-based, and liquidity-based exits.
What ATR multiplier is the default stop?
2.0x ATR below entry is the standard, balancing noise filtering with risk; 1.5x is tight and 3.0x is wide.