feat: Implement percentage-based position sizing

- Add usePercentageSize flag to SymbolSettings and TradingConfig
- Add calculateActualPositionSize() and getActualPositionSizeForSymbol() helpers
- Update execute and test endpoints to calculate position size from free collateral
- Add SOLANA_USE_PERCENTAGE_SIZE, ETHEREUM_USE_PERCENTAGE_SIZE, USE_PERCENTAGE_SIZE env vars
- Configure SOL to use 100% of portfolio (auto-adjusts to available balance)
- Fix TypeScript errors: replace fillNotionalUSD with actualSizeUSD
- Remove signalQualityVersion and fullyClosed references (not in interfaces)
- Add comprehensive documentation in PERCENTAGE_SIZING_FEATURE.md

Benefits:
- Prevents insufficient collateral errors by using available balance
- Auto-scales positions as account grows/shrinks
- Maintains risk proportional to capital
- Flexible per-symbol configuration (SOL percentage, ETH fixed)
This commit is contained in:
mindesbunister
2025-11-10 13:35:10 +01:00
parent d20190c5b0
commit 6f0a1bb49b
7 changed files with 741 additions and 284 deletions

View File

@@ -8,12 +8,14 @@ export interface SymbolSettings {
enabled: boolean
positionSize: number
leverage: number
usePercentageSize?: boolean // If true, positionSize is % of portfolio (0-100)
}
export interface TradingConfig {
// Position sizing (global fallback)
positionSize: number // USD amount to trade
positionSize: number // USD amount to trade (or percentage if usePercentageSize=true)
leverage: number // Leverage multiplier
usePercentageSize: boolean // If true, positionSize is % of free collateral
// Per-symbol settings
solana?: SymbolSettings
@@ -93,19 +95,22 @@ export interface MarketConfig {
// Default configuration for 5-minute scalping with $1000 capital and 10x leverage
export const DEFAULT_TRADING_CONFIG: TradingConfig = {
// Position sizing (global fallback)
positionSize: 50, // $50 base capital (SAFE FOR TESTING)
positionSize: 50, // $50 base capital (SAFE FOR TESTING) OR percentage if usePercentageSize=true
leverage: 10, // 10x leverage = $500 position size
usePercentageSize: false, // False = fixed USD, True = percentage of portfolio
// Per-symbol settings
solana: {
enabled: true,
positionSize: 210, // $210 base capital
positionSize: 210, // $210 base capital OR percentage if usePercentageSize=true
leverage: 10, // 10x leverage = $2100 notional
usePercentageSize: false,
},
ethereum: {
enabled: true,
positionSize: 4, // $4 base capital (DATA ONLY - minimum size)
leverage: 1, // 1x leverage = $4 notional
usePercentageSize: false,
},
// Risk parameters (wider for DEX slippage/wicks)
@@ -248,6 +253,85 @@ export function getPositionSizeForSymbol(symbol: string, baseConfig: TradingConf
}
}
/**
* Calculate actual USD position size from percentage or fixed amount
* @param configuredSize - The configured size (USD or percentage)
* @param usePercentage - Whether configuredSize is a percentage
* @param freeCollateral - Available collateral in USD (from Drift account)
* @returns Actual USD size to use for the trade
*/
export function calculateActualPositionSize(
configuredSize: number,
usePercentage: boolean,
freeCollateral: number
): number {
if (!usePercentage) {
// Fixed USD amount
return configuredSize
}
// Percentage of free collateral
const percentDecimal = configuredSize / 100
const calculatedSize = freeCollateral * percentDecimal
console.log(`📊 Percentage sizing: ${configuredSize}% of $${freeCollateral.toFixed(2)} = $${calculatedSize.toFixed(2)}`)
return calculatedSize
}
/**
* Get actual position size for symbol with percentage support
* This is the main function to use when opening positions
*/
export async function getActualPositionSizeForSymbol(
symbol: string,
baseConfig: TradingConfig,
freeCollateral: number
): Promise<{ size: number; leverage: number; enabled: boolean; usePercentage: boolean }> {
let symbolSettings: { size: number; leverage: number; enabled: boolean }
let usePercentage = false
// Get symbol-specific settings
if (symbol === 'SOL-PERP' && baseConfig.solana) {
symbolSettings = {
size: baseConfig.solana.positionSize,
leverage: baseConfig.solana.leverage,
enabled: baseConfig.solana.enabled,
}
usePercentage = baseConfig.solana.usePercentageSize ?? false
} else if (symbol === 'ETH-PERP' && baseConfig.ethereum) {
symbolSettings = {
size: baseConfig.ethereum.positionSize,
leverage: baseConfig.ethereum.leverage,
enabled: baseConfig.ethereum.enabled,
}
usePercentage = baseConfig.ethereum.usePercentageSize ?? false
} else {
// Fallback to market-specific or global config
const marketConfig = getMarketConfig(symbol)
symbolSettings = {
size: marketConfig.positionSize ?? baseConfig.positionSize,
leverage: marketConfig.leverage ?? baseConfig.leverage,
enabled: true,
}
usePercentage = baseConfig.usePercentageSize
}
// Calculate actual size
const actualSize = calculateActualPositionSize(
symbolSettings.size,
usePercentage,
freeCollateral
)
return {
size: actualSize,
leverage: symbolSettings.leverage,
enabled: symbolSettings.enabled,
usePercentage,
}
}
/**
* Calculate dynamic TP2 level based on ATR (Average True Range)
* Higher ATR = higher volatility = larger TP2 target to capture big moves
@@ -324,6 +408,10 @@ export function getConfigFromEnv(): Partial<TradingConfig> {
? parseFloat(process.env.MAX_POSITION_SIZE_USD)
: undefined,
usePercentageSize: process.env.USE_PERCENTAGE_SIZE
? process.env.USE_PERCENTAGE_SIZE === 'true'
: undefined,
// Per-symbol settings from ENV
solana: {
enabled: process.env.SOLANA_ENABLED !== 'false',
@@ -333,6 +421,9 @@ export function getConfigFromEnv(): Partial<TradingConfig> {
leverage: process.env.SOLANA_LEVERAGE
? parseInt(process.env.SOLANA_LEVERAGE)
: 10,
usePercentageSize: process.env.SOLANA_USE_PERCENTAGE_SIZE
? process.env.SOLANA_USE_PERCENTAGE_SIZE === 'true'
: false,
},
ethereum: {
enabled: process.env.ETHEREUM_ENABLED !== 'false',
@@ -342,6 +433,9 @@ export function getConfigFromEnv(): Partial<TradingConfig> {
leverage: process.env.ETHEREUM_LEVERAGE
? parseInt(process.env.ETHEREUM_LEVERAGE)
: 1,
usePercentageSize: process.env.ETHEREUM_USE_PERCENTAGE_SIZE
? process.env.ETHEREUM_USE_PERCENTAGE_SIZE === 'true'
: false,
},
leverage: process.env.LEVERAGE
? parseInt(process.env.LEVERAGE)