PositionSizing 2.0.0

dotnet add package PositionSizing --version 2.0.0
                    
NuGet\Install-Package PositionSizing -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PositionSizing" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PositionSizing" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="PositionSizing" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PositionSizing --version 2.0.0
                    
#r "nuget: PositionSizing, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PositionSizing@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PositionSizing&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=PositionSizing&version=2.0.0
                    
Install as a Cake Tool

PositionSizing

A small, dependency-free .NET library covering the questions every systematic trading strategy has to answer, from before a trade is even allowed through to grading it once it's closed.

It has no dependencies beyond the .NET base class library, and no notion of a broker, a market, or a strategy — just the arithmetic. Reference it as a normal package, or copy the files you need directly into your project.

Where this fits in the trading flow

Risk management in a real trading system is two distinct layers, and this library covers both:

Signal says "take this trade"
        │
        ▼
Portfolio risk gates ── "should I trade AT ALL right now?"   (the gate suite below)
        │  (allowed)
        ▼
Place stop/target, size the position ── "given that I'm trading, how?"  (the sizing suite)
        │
        ▼
Manage the stop while open, grade the outcome once closed

The gates answer a binary question about the portfolio — halt or don't halt — independent of any single trade. The sizing tools answer a continuous question about this trade, once the gates have already said trading is allowed. Neither one substitutes for the other: a system with great position sizing but no daily-loss kill switch can still blow through an unacceptable loss one trade at a time, and a system with strict gates but naive sizing can still take on too much risk on any individual trade that does get through.

What's in it

Should I trade at all right now? — portfolio risk gates

PortfolioHeatGate — total open risk across every position at once. This is the question the other gates structurally cannot answer: they cap capital, position count, P&L, streaks and time, but ten positions each risking 1% to their stops is 10% of equity at risk simultaneously, and nothing else here sees it. Supports Elder's 6% rule (open risk plus realized losses for the period) and reports the remaining risk budget so you can size the next trade into what's left. Because it reads your current stops, a position whose stop has trailed to breakeven contributes almost no heat — which is the whole point of trailing.

DrawdownRiskScaler — tapers risk as a drawdown deepens, rather than trading full size right up to a cliff. Complements DrawdownGate: this one shrinks the bet, that one stops the trading.

DailyLossGate — a daily loss kill switch: halts new entries once cumulative P&L for the day (realized AND unrealized — see the class doc for why the unrealized half matters) breaches a limit.

DrawdownGate — halts new entries once the running drawdown from the account's peak equity breaches a limit, independent of how any single day is going.

MaxPositionCountGate — caps the number of simultaneously open positions, as a pure count.

ExposureConcentrationGate — caps exposure to any single category (sector, asset class, anything you choose to group by), not just total portfolio exposure — two portfolios can have identical total exposure and very different real risk depending on how concentrated it is.

ConsecutiveLossGate — halts new entries after a losing streak long enough to suggest something has changed (a regime shift, a broken signal) rather than ordinary variance.

SessionTimeGate — blocks new entries outside a permitted entry window within the session (e.g. avoiding opening-range chop or late-session risk), with session times as parameters rather than assuming any particular market's hours.

CooldownGate — tracks whether a halt triggered by any of the gates above is still within its cooldown period before new entries can resume.

VolatilityExposureScaler — scales a maximum-exposure cap down as volatility rises above a calm-market baseline, so the portfolio automatically de-risks in rough conditions instead of carrying a fixed cap regardless of how volatile things get.

Placing the initial stop and target

TargetPriceCalculator — places a stop-loss and take-profit level from an entry price, a volatility measure (typically ATR), a stop-distance multiplier, and a risk:reward ratio. The target distance is always a multiple of the stop distance, so it scales proportionally with however wide or tight the stop is set.

When the ATR isn't usable (a warm-up period, a non-finite value from a broken feed) the default substitutes a 1%-of-price proxy so a level can still be placed. That convenience has teeth, because the invented stop distance becomes the divisor in the risk budget — a proxy four times too tight produces a position four times too large. So the result now carries UsedAtrFallback to make the substitution visible, and requireRealAtr: true refuses to invent a figure at all. Prefer the strict form in automated paths; it's the same call ChandelierStop already makes when handed a bad ATR.

Sizing the position — independent philosophies, plus caps that bind against them

PositionSizeCalculator — the standard risk-budget approach: sizes a position under three constraints (how much you're willing to lose if the stop is hit, a hard cap on capital per trade, and how much portfolio exposure headroom remains) and returns the tightest one. A position with no headroom left for even one unit returns null rather than a zero-or-negative-sized position — "there's no size that fits" is a distinct, real outcome from "here's a valid size." ComputeForRiskBudget takes the risk budget as a currency amount instead of a percentage of equity, which is what you need to size into PortfolioHeatGate.RemainingRiskBudget or MarketsMoneySizing.RiskBudget.

KellyCriterion — a different starting point: given a measured edge (win probability and payoff ratio), computes the fraction of equity that maximizes long-run growth, rather than a fraction chosen independent of that edge. Includes fractional-Kelly (half-Kelly by default), the standard practitioner adjustment for the large drawdowns full Kelly produces under real-world estimation error.

VolatilityTargetSizing — a third approach, from risk-parity and vol-targeting practice: sizes a position so its expected day-to-day P&L swing matches a fixed target, independent of any stop-loss distance. A calm instrument gets a bigger position and a volatile one a smaller position, for the same risk contribution.

OptimalF — Ralph Vince's optimal f: the fraction that maximizes terminal wealth relative over your actual trade history. Documented plainly as what it is — growth-maximizing, notoriously aggressive, and acutely sensitive to the single largest loss in the sample.

FixedRatioSizing — Ryan Jones's fixed ratio progression, where contracts increase only after profit accumulates by a widening increment governed by a delta.

MarketsMoneySizing — Van Tharp's market's-money model: risk original capital and accumulated profits at different rates.

MarginBasedSizing — percent-of-margin sizing for margined instruments. You supply the margin per lot; this library still does not compute margin, and says so.

LiquidityCappedSize — a participation cap against average traded volume. A position you can't exit without moving the price is a risk that stop-distance arithmetic cannot see.

CppiExposure — constant proportion portfolio insurance: hold a floor, expose a multiple of the cushion above it, with a floor that ratchets up on new equity highs and never down.

These are alternatives, not a pipeline — pick the philosophy that matches your strategy. Where several apply, combine them by taking the minimum; the caps in particular (LiquidityCappedSize, MarginBasedSizing) are meant to bind against whatever your primary sizer produced.

Managing the stop while the trade is open

ChandelierStop — an ATR-based trailing stop that ratchets toward price as a trend extends, tightening only, never loosening — a favorable move can't be undone by a later, less favorable one pulling the stop back out.

BreakevenStop — the "once this trade can no longer be a loser" rule: moves the stop to entry once price has moved a given multiple of the initial risk in your favor. Composes with ChandelierStop: both only ever tighten, so calling either (or both, taking whichever is tighter) as new bars arrive is always safe.

After the trade closes

RMultipleCalculator — grades a closed trade as a multiple of its initial risk (a "+2.4R" winner or a "-0.6R" loser), the standard way to compare trade outcomes independent of how big any particular position happened to be. Pass a per-unit round-trip cost and the grade is net of costs — which is the grade anything feeding a sizing decision should be built from (see Costs are an input, not a footnote below).

CostDrag — expresses transaction costs as a fraction of R, the only form in which they can be compared against an edge. Also solves the inverse: the narrowest stop that keeps drag inside a budget you choose.

ScaleOutPlan — splits a position across profit-taking levels you supply (this library has no opinion on which R-multiples or percentages make a good plan), using largest-remainder apportionment so legs never lose or double-count a unit, and separately reporting any quantity you deliberately left unassigned to run.

TradeExpectancy — turns a list of R-multiples into win probability, average win/loss, payoff ratio, expectancy and Van Tharp's SQN. This is the bridge that was missing: KellyCriterion needs a win probability and a payoff ratio, and until now nothing produced them, so the one edge-based sizer's inputs had to be guessed — which defeats the point of an edge-based sizer.

RiskOfRuin — a seeded (reproducible) Monte Carlo estimate of the probability of hitting a ruin threshold, for sanity-checking the riskPerTradePercent you were about to type in.

StressLossEstimator — what the position actually loses when the stop does not work. Every stop-distance-based sizer embeds an assumption that is simply false: that the exit happens at the stop price. Gaps, circuit limits and trading halts break it, and this is the only thing here that quantifies the damage.

Instruments traded in lots

PositionSizeCalculator.ComputeForLotSize — a thin wrapper around the same risk-budget sizing for instruments traded in fixed contract sizes (index futures and options): pass a lot size, and the returned quantity is already expressed in whole lots, with capital already correctly scaled to real notional value.

Install

Reference the project directly, or build and reference the package:

dotnet add reference path/to/PositionSizing.csproj

or pack it and add it as a local NuGet feed:

dotnet pack src/PositionSizing -o ./nupkg
dotnet add package PositionSizing --source ./nupkg

Usage

Should this trade even be allowed?

using PositionSizing;

bool halted =
    DailyLossGate.IsBreached(dailyPnL: todaysPnLIncludingUnrealized, dailyLossLimit: 20_000m)
    || DrawdownGate.IsBreached(currentEquity, peakEquity, maxDrawdownPercent: 0.15m)
    || MaxPositionCountGate.IsReached(openPositionCount, maxConcurrentPositions: 5)
    || ConsecutiveLossGate.IsBreached(consecutiveLosses, maxConsecutiveLosses: 4)
    || SessionTimeGate.IsBlocked(currentTime, entryOpensAfter: new(9, 30, 0), entryClosesBefore: new(15, 0, 0));

if (halted) { /* don't take the trade, whatever the signal says */ }

var breachedSectors = ExposureConcentrationGate.BreachedCategories(sectorExposures, maxCategoryPercent: 0.20m);
decimal maxExposureNow = VolatilityExposureScaler.Compute(
    baseMaxExposurePercent: 0.60m, currentVolatility: todaysAtrPercent, baselineVolatility: 0.02m);

The core lifecycle: place, size, trail, grade

TargetPriceCalculator.Result is a struct, so Compute returns Result? and you reach the values through .Value after a null check — the null is a real answer ("this geometry has no tradeable levels"), not a formality.

using PositionSizing;

// 1. Place the initial stop and target from ATR.
var levels = TargetPriceCalculator.Compute(
    entryPrice: 100m, atrValue: 2.0, isShort: false,
    atrStopMultiplier: 1.5m, riskRewardRatio: 2.0m, requireRealAtr: true);

if (levels is null)
    return; // no usable ATR yet, or a stop that would sit at or below zero

var placement = levels.Value;
// placement.StopLossPrice = 97, placement.TargetPrice = 106, placement.StopDistance = 3

// 2. Size the position.
var size = PositionSizeCalculator.Compute(
    accountEquity: 1_000_000m, currentPrice: 100m, stopDistance: placement.StopDistance,
    riskPerTradePercent: 0.005m, maxCapitalPerTradePercent: 0.25m,
    openExposurePercent: 0.30m, maxExposurePercent: 0.60m);

if (size is null)
    return; // no exposure headroom left, or neither cap can afford one unit

// 3. As new bars arrive, trail the stop (never loosens) and check the breakeven rule.
decimal stop = placement.StopLossPrice;
stop = ChandelierStop.Compute(stop, extremePriceSinceEntry: runningHighSinceEntry,
    atrValue: latestAtr, isShort: false);
stop = BreakevenStop.Compute(stop, entryPrice: 100m, currentPrice: latestPrice,
    initialStopDistance: placement.StopDistance, isShort: false);

// 4. Once closed, grade the outcome NET of what it cost to trade.
decimal costPerUnit = CostDrag.PerUnit(roundTripCost: 386m, quantity: size.Quantity) ?? 0m;
double? rMultiple = RMultipleCalculator.Compute(
    entryPrice: 100m, exitPrice: 108m, initialStopDistance: placement.StopDistance,
    isShort: false, roundTripCostPerUnit: costPerUnit);

Kelly sizing, from a measured edge

// From your own trade history: 55% win rate, average win 1.8x average loss.
double halfKellyFraction = KellyCriterion.ComputeFractional(winProbability: 0.55, payoffRatio: 1.8);
decimal riskBudget = accountEquity * (decimal)halfKellyFraction;

Volatility-targeted sizing, independent of a stop

decimal? quantity = VolatilityTargetSizing.Compute(
    accountEquity: 1_000_000m, currentPrice: 100m,
    dailyVolatilityPercent: 0.02m,   // e.g. ATR / price
    targetDailyRiskPercent: 0.01m);  // this position should swing ~1% of equity on a normal day

Costs are an input, not a footnote

A cost quoted as a percentage of turnover is not comparable to an edge measured as a fraction of risk. Divide one by the other and the same fee schedule means something different for every strategy — on a ₹1,000 instrument, 250 shares, a real Indian intraday round trip of about ₹386:

stop width cost, in R
1.5% (a swing stop) 0.10R
0.5% 0.31R
0.3% (a scalping stop) 0.52R

Same fees, same position. The scalper pays five times as much of their edge away purely because the denominator is smaller, and halving a stop to "risk less" doubles the drag. That number is also the gross edge you need just to break even.

decimal? drag = CostDrag.InR(roundTripCost: 386m, quantity: 250m, stopDistance: 5m);
// 0.309 -> a gross expectancy of +0.31R earns exactly nothing

// The inverse, which is usually the question that matters: is my stop too tight to bother?
decimal? minStop = CostDrag.MinimumStopDistanceForMaxDrag(386m, 250m, maxDragInR: 0.10m);

This is why RMultipleCalculator takes a cost: grade net at the point of measurement and everything downstream — TradeExpectancy, and through it KellyCriterion and RiskOfRuin — is net automatically, with no separate cost step anywhere. Grade gross and every statistic is overstated in the same direction at once. A history with a genuine gross expectancy of +0.10R, against 0.23R of drag:

GROSS: win 40%  payoff 1.75  expectancy  0.100R  SQN  0.22  -> Kelly: risk 2.86% of equity
NET  : win 40%  payoff 1.24  expectancy -0.130R  SQN -0.28  -> Kelly: risk 0.00% of equity

Identical trades. The gross reading reports a positive edge, a positive SQN, and instructs you to bet 2.86% of equity on a system that loses money. There are tests pinning both rows.

This library has no opinion on what your costs are and no knowledge of any market's fee schedule — feed it a total from wherever you model costs.

Sizing into what the portfolio has left

PortfolioHeatGate reports RemainingRiskBudget as a currency amount, so use ComputeForRiskBudget rather than converting back to a percentage. Heat is an additional ceiling on your per-trade limit, not a replacement for it — take the smaller of the two:

var heat = PortfolioHeatGate.Compute(accountEquity, openPositionRisks, maxHeatPercent: 0.06m);
if (heat is null || heat.IsBreached)
    return; // no new risk allowed at all

decimal budget = Math.Min(accountEquity * 0.01m, heat.RemainingRiskBudget);

var size = PositionSizeCalculator.ComputeForRiskBudget(
    accountEquity, currentPrice: 100m, stopDistance: 3m, riskBudget: budget,
    maxCapitalPerTradePercent: 0.25m,
    openExposurePercent: openExposure, maxExposurePercent: 0.60m);

Scaling out at caller-chosen levels

var (legs, remainder) = ScaleOutPlan.Compute(
    totalQuantity: 100m,
    levels: new[] { (RMultiple: 1.0m, ExitPercent: 0.5m) }); // half off at 1R, let the rest run
// legs[0].Quantity = 50, remainder = 50 (deliberately left to run)

Sizing a lot-based instrument (e.g. index options)

var lotSize = PositionSizeCalculator.ComputeForLotSize(
    accountEquity: 1_000_000m,
    underlyingPrice: 200m,       // premium per unit, not multiplied by lot size
    stopDistancePerUnit: 4m,
    lotSize: 25,
    riskPerTradePercent: 0.01m, maxCapitalPerTradePercent: 0.5m,
    openExposurePercent: 0m, maxExposurePercent: 0.6m);
// lotSize.Quantity is already in LOTS; lotSize.CapitalDeployed is already correct notional value.

What this library does not do

  • It does not decide whether the SIGNAL is good. Confidence thresholds, signal scoring, and regime filters are strategy decisions that belong upstream of this library. The gates above decide whether trading is allowed at the portfolio level right now; they have no opinion on whether any particular signal is worth acting on.
  • It does not track state for you. The gates are pure functions over numbers you already have — a running peak equity, a consecutive-loss count, when a halt began. Tracking those values over time (updating the peak as new equity highs occur, incrementing/resetting the loss streak as trades close, remembering when a halt started) is the caller's job; these functions just answer "given where things stand, is the threshold breached."
  • It does not model margin or SPAN requirements, and the direction of that error is not the same for every instrument. CapitalDeployed is always price × quantity, whatever you passed as the price. For a long option that is exactly right: premium paid is the capital committed and the maximum loss. For futures it is conservative — real notional, against a margin that is a fraction of it, so the cap binds earlier than it needs to. For a short option it is close to meaningless and wrong in the unsafe direction: the "price" is premium received, so a short NIFTY put at ₹150 on a 75-unit lot computes ₹11,250 of "capital deployed" against roughly ₹1.3 lakh of real margin and an unbounded loss. maxCapitalPerTradePercent, openExposurePercent and maxExposurePercent are all measured against premium there and none of them will bind before the account is far more exposed than the numbers suggest. Cap short premium with MarginBasedSizing against the margin your broker actually quotes, and let PortfolioHeatGate — which measures risk to the stop, not capital — carry the portfolio limit. MarginBasedSizing sizes given a margin requirement you supply; it does not compute one.
  • It does not know about correlation across positions. Exposure headroom and portfolio heat both sum across positions as if they were independent; two highly correlated positions carry more combined risk than two uncorrelated ones of the same size. A correlation-adjusted risk aggregator was considered and deliberately left out: it needs a covariance estimate that a caller of a library like this almost certainly does not have, and shipping one would imply a precision the inputs cannot support. Treat portfolio heat as a floor on true portfolio risk, not a measure of it.
  • It does not model transaction costs. It takes them as an input so grades can be net of them (RMultipleCalculator, CostDrag), but it does not know any market's brokerage, taxes or exchange charges. Compute those wherever you already model them and pass a total.
  • It does not fetch data. ATR, win rates, account equity, and open exposure are all inputs you provide.

Design note: folders, and one flat namespace

The source is organized into Risk/, Sizing/, Stops/ and Analysis/ folders, but every file declares the same flat namespace PositionSizing — so using PositionSizing; gets you the entire library and the folders never appear in consuming code.

That is deliberate on two grounds. Microsoft's Framework Design Guidelines say types used in the same scenarios belong in the same namespace, and that users "should not have to import many namespaces to develop common scenarios" — the lifecycle example above touches all four folders in a dozen lines, so sub-namespaces would turn one using into four for the most ordinary use of the library. And separately, this package is published with the flat namespace, so introducing sub-namespaces now would break every existing consumer at compile time.

The repo carries an .editorconfig that disables IDE0130 for exactly this reason: without it, Visual Studio and Rider flag every file as "namespace does not match folder structure" and offer a one-click fix that would silently rewrite them into sub-namespaces. Don't accept that fix.

Design note: why there's no shared "domain model" package

Every function in this library takes plain values — decimal, int, TimeSpan, Dictionary<string, decimal> — never a shared Trade, Position, or Instrument type from some separate foundational package. That's deliberate, not an oversight: a shared types package would mean anyone who wants just KellyCriterion also has to pull in a transitive dependency for types they may not otherwise need, and it would couple every package's version to that shared package's version indefinitely. Every one of these gates and calculators turned out to need nothing richer than the numbers above — there hasn't yet been a case where two of these packages independently needed the same non-trivial shape (the closest is CandleIndicators.Candle, an 8-field record simple enough that duplicating its shape costs less than depending on a shared package would). If that changes — a future options-specific package and this one both needing a real OptionPosition shape, say — that's the point to extract a shared kernel, based on actual duplication rather than in anticipation of it.

Provenance

TargetPriceCalculator and PositionSizeCalculator's core sizing formulas, along with the underlying logic in DailyLossGate and MaxPositionCountGate, were extracted from a private trading-research platform's risk-management gate, where they were the part of a much larger, strategy-specific gate (AI-confidence thresholds, composite-score gates, regime filters) that had no dependency on any of that — pure arithmetic over equity, price, and exposure. (Two hard-won lessons from that platform's own history are carried forward in the doc comments: include unrealized P&L in a daily-loss check, and never let a position count be confused with a position's rupee value.) Everything else — KellyCriterion, ChandelierStop, BreakevenStop, VolatilityTargetSizing, RMultipleCalculator, ScaleOutPlan, ComputeForLotSize, DrawdownGate, ExposureConcentrationGate, ConsecutiveLossGate, SessionTimeGate, CooldownGate, and VolatilityExposureScaler — was not part of that platform and was written directly for this package, to round out the full risk-management, sizing, stop-management, and trade-grading lifecycle around the extracted core.

License

MIT — see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 135 8/31/2026
1.0.0 136 8/25/2026 1.0.0 is deprecated because it has critical bugs.