ClientSizing implements position size calculation logic for trading signals. It provides three distinct sizing methods: fixed percentage allocation, Kelly criterion optimization, and ATR-based volatility scaling. This class belongs to the Client Classes layer (Layer 4) and operates without dependency injection, accepting all dependencies through constructor parameters.
For information about risk validation and portfolio limits, see ClientRisk. For strategy-level signal generation, see ClientStrategy.
ClientSizing calculates the quantity of an asset to trade based on portfolio state and signal parameters. It consumes a sizing schema (registered via addSizing()) and execution context, then returns a formatted position size via the calculatePositionSize() method. The calculation considers:
ClientSizing does not validate signals, track positions, or interact with exchanges. These responsibilities belong to ClientStrategy, ClientRisk, and ClientExchange respectively.
ClientSizing sits in the Client Classes layer and is instantiated by SizingConnectionService. The connection service memoizes one ClientSizing instance per sizing schema name.
ClientSizing supports three sizing methods, each defined by a discriminated union type. The method field serves as the discriminator.
| Method | Discriminator | Key Parameters | Use Case |
|---|---|---|---|
| Fixed Percentage | "fixed-percentage" |
percentage |
Simple constant allocation |
| Kelly Criterion | "kelly-criterion" |
winRate, avgWinPnl, avgLossPnl |
Optimal allocation based on edge |
| ATR-Based | "atr-based" |
atrMultiplier, atrPeriod |
Volatility-adjusted sizing |
Fixed percentage sizing allocates a constant percentage of the portfolio to each trade. This is the simplest method and requires no historical data or performance statistics.
The schema for fixed percentage sizing:
interface ISizingSchemaFixedPercentage {
sizingName: string;
method: "fixed-percentage";
params: {
percentage: number; // e.g., 0.02 for 2% of portfolio
};
}
When ClientStrategy calls calculatePositionSize() for a fixed percentage signal:
interface ISizingCalculateParamsFixedPercentage {
method: "fixed-percentage";
params: {
portfolioBalance: number; // Current account balance
entryPrice: number; // Signal's priceOpen
};
}
The position size formula:
positionSize = (portfolioBalance × percentage) / entryPrice
Example:
Kelly criterion sizing maximizes long-term growth by calculating optimal position size based on historical win rate and average profit/loss ratios.
interface ISizingSchemaKelly {
sizingName: string;
method: "kelly-criterion";
params: {
winRate: number; // Win rate as decimal (e.g., 0.55 for 55%)
avgWinPnl: number; // Average winning PNL percentage
avgLossPnl: number; // Average losing PNL percentage (absolute value)
};
}
interface ISizingCalculateParamsKelly {
method: "kelly-criterion";
params: {
portfolioBalance: number;
entryPrice: number;
};
}
The Kelly formula:
kellyPercentage = (winRate × avgWinPnl - (1 - winRate) × avgLossPnl) / avgWinPnl
positionSize = (portfolioBalance × kellyPercentage) / entryPrice
Example:
Note: ClientSizing may apply a fractional Kelly (e.g., half-Kelly) to reduce risk.
ATR (Average True Range) sizing adjusts position size based on market volatility. Higher volatility results in smaller positions, maintaining consistent risk exposure.
interface ISizingSchemaATR {
sizingName: string;
method: "atr-based";
params: {
atrMultiplier: number; // Multiplier for ATR (e.g., 2.0)
atrPeriod: number; // Number of periods for ATR calculation (e.g., 14)
};
}
interface ISizingCalculateParamsATR {
method: "atr-based";
params: {
portfolioBalance: number;
entryPrice: number;
stopLoss: number; // Signal's priceStopLoss
historicalCandles: ICandleData[]; // Candles for ATR calculation
};
}
The ATR-based formula:
1. Calculate ATR from historicalCandles using atrPeriod
2. stopDistance = |entryPrice - stopLoss|
3. riskPerUnit = atrMultiplier × ATR
4. positionSize = (portfolioBalance × riskPercentage) / riskPerUnit
Example:
The diagram below shows how ClientSizing integrates into the signal generation flow:
ClientSizing implements the ISizing interface, which defines the contract for position size calculation:
interface ISizing {
calculatePositionSize(
params: ISizingCalculateParams
): Promise<string>;
}
The ISizingCalculateParams type is a discriminated union matching the sizing method:
type ISizingCalculateParams =
| ISizingCalculateParamsFixedPercentage
| ISizingCalculateParamsKelly
| ISizingCalculateParamsATR;
Each variant contains the method discriminator and method-specific params object.
The calculatePositionSize() method returns a Promise<string> containing the formatted quantity. This string is formatted according to the exchange's precision rules via ClientExchange.formatQuantity().
Example:
{ method: "fixed-percentage", params: { portfolioBalance: 10000, entryPrice: 50000 } }"0.004" (8 decimal places for BTC)Sizing schemas are registered via the addSizing() function and stored in SizingSchemaService:
SizingConnectionService memoizes ClientSizing instances:
sizingNamesizingNameStrategies reference sizing via the optional sizingName field in IStrategySchema:
interface IStrategySchema {
strategyName: string;
// ... other fields
sizingName?: string; // Optional reference to sizing schema
}
If sizingName is omitted, ClientStrategy uses a default sizing method (typically fixed percentage at 100% of portfolio).
When ClientStrategy needs to calculate position size:
methodContext.sizingNameISizingCalculateParams based on schema methodcalculatePositionSize() with constructed parametersSizing schemas support optional callbacks for tracking calculation events:
interface ISizingCallbacks {
onCalculate?: (
params: ISizingCalculateParams,
result: string
) => void;
}
The onCalculate callback fires after each position size calculation, receiving the input parameters and formatted result. This enables logging, debugging, and custom analytics.
Example usage:
addSizing({
sizingName: "my-sizing",
method: "kelly-criterion",
params: { winRate: 0.6, avgWinPnl: 0.05, avgLossPnl: 0.03 },
callbacks: {
onCalculate: (params, result) => {
console.log(`Calculated position size: ${result}`);
}
}
});
ClientSizing constructor accepts parameters conforming to a schema-based interface:
interface ISizingParams extends ISizingSchema {
logger: ILogger;
execution: TExecutionContextService;
exchange: IExchange;
}
The parameters combine:
addSizing())formatQuantity() callsNote: Unlike other client classes, ClientSizing requires IExchange because it must format the calculated quantity according to exchange-specific precision rules.
ClientSizing operates identically in all execution modes (Backtest, Live, Walker):