PositionSizing 2.0.0
dotnet add package PositionSizing --version 2.0.0
NuGet\Install-Package PositionSizing -Version 2.0.0
<PackageReference Include="PositionSizing" Version="2.0.0" />
<PackageVersion Include="PositionSizing" Version="2.0.0" />
<PackageReference Include="PositionSizing" />
paket add PositionSizing --version 2.0.0
#r "nuget: PositionSizing, 2.0.0"
#:package PositionSizing@2.0.0
#addin nuget:?package=PositionSizing&version=2.0.0
#tool nuget:?package=PositionSizing&version=2.0.0
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.
CapitalDeployedis 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,openExposurePercentandmaxExposurePercentare 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 withMarginBasedSizingagainst the margin your broker actually quotes, and letPortfolioHeatGate— which measures risk to the stop, not capital — carry the portfolio limit.MarginBasedSizingsizes 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 | Versions 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. |
-
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.