@autonomaai/agent-core 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base-agent.d.ts +112 -0
- package/dist/base-agent.d.ts.map +1 -0
- package/dist/base-agent.js +173 -0
- package/dist/base-agent.js.map +1 -0
- package/dist/core.d.ts +81 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +633 -0
- package/dist/core.js.map +1 -0
- package/dist/error-handler.d.ts +78 -0
- package/dist/error-handler.d.ts.map +1 -0
- package/dist/error-handler.js +129 -0
- package/dist/error-handler.js.map +1 -0
- package/dist/factory.d.ts +60 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +621 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/streaming.d.ts +24 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +36 -0
- package/dist/streaming.js.map +1 -0
- package/dist/trading/formatters.d.ts +167 -0
- package/dist/trading/formatters.d.ts.map +1 -0
- package/dist/trading/formatters.js +271 -0
- package/dist/trading/formatters.js.map +1 -0
- package/dist/trading/index.d.ts +9 -0
- package/dist/trading/index.d.ts.map +1 -0
- package/dist/trading/index.js +10 -0
- package/dist/trading/index.js.map +1 -0
- package/dist/trading/types.d.ts +205 -0
- package/dist/trading/types.d.ts.map +1 -0
- package/dist/trading/types.js +7 -0
- package/dist/trading/types.js.map +1 -0
- package/dist/trading/utils.d.ts +120 -0
- package/dist/trading/utils.d.ts.map +1 -0
- package/dist/trading/utils.js +291 -0
- package/dist/trading/utils.js.map +1 -0
- package/dist/trading/validation.d.ts +40 -0
- package/dist/trading/validation.d.ts.map +1 -0
- package/dist/trading/validation.js +247 -0
- package/dist/trading/validation.js.map +1 -0
- package/dist/types.d.ts +282 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
- package/src/base-agent.ts +263 -0
- package/src/core.ts +792 -0
- package/src/error-handler.ts +166 -0
- package/src/factory.ts +687 -0
- package/src/global.d.ts +12 -0
- package/src/index.ts +24 -0
- package/src/streaming.ts +50 -0
- package/src/trading/formatters.ts +363 -0
- package/src/trading/index.ts +10 -0
- package/src/trading/types.ts +263 -0
- package/src/trading/utils.ts +355 -0
- package/src/trading/validation.ts +321 -0
- package/src/types.ts +402 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trading Utilities for autonoma Agent Core
|
|
3
|
+
*
|
|
4
|
+
* Common utility functions for trading calculations and operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Order,
|
|
9
|
+
Position,
|
|
10
|
+
Portfolio,
|
|
11
|
+
OrderSide,
|
|
12
|
+
YieldOpportunity,
|
|
13
|
+
TimeFrame,
|
|
14
|
+
PriceRange,
|
|
15
|
+
OHLCV
|
|
16
|
+
} from './types.js';
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Price & Market Calculations
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Calculate percentage change between two prices
|
|
24
|
+
*/
|
|
25
|
+
export function calculatePriceChange(oldPrice: number, newPrice: number): number {
|
|
26
|
+
if (oldPrice === 0) return 0;
|
|
27
|
+
return ((newPrice - oldPrice) / oldPrice) * 100;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Calculate simple moving average
|
|
32
|
+
*/
|
|
33
|
+
export function calculateSMA(prices: number[], period: number): number {
|
|
34
|
+
if (prices.length < period) return 0;
|
|
35
|
+
const sum = prices.slice(-period).reduce((acc, price) => acc + price, 0);
|
|
36
|
+
return sum / period;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Calculate exponential moving average
|
|
41
|
+
*/
|
|
42
|
+
export function calculateEMA(prices: number[], period: number): number {
|
|
43
|
+
if (prices.length === 0) return 0;
|
|
44
|
+
if (prices.length === 1) return prices[0]!;
|
|
45
|
+
|
|
46
|
+
const multiplier = 2 / (period + 1);
|
|
47
|
+
let ema = prices[0]!;
|
|
48
|
+
|
|
49
|
+
for (let i = 1; i < prices.length; i++) {
|
|
50
|
+
const price = prices[i];
|
|
51
|
+
if (price !== undefined) {
|
|
52
|
+
ema = (price * multiplier) + (ema * (1 - multiplier));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return ema;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Calculate volatility (standard deviation) of price returns
|
|
61
|
+
*/
|
|
62
|
+
export function calculateVolatility(prices: number[]): number {
|
|
63
|
+
if (prices.length < 2) return 0;
|
|
64
|
+
|
|
65
|
+
const returns = [];
|
|
66
|
+
for (let i = 1; i < prices.length; i++) {
|
|
67
|
+
const current = prices[i];
|
|
68
|
+
const previous = prices[i - 1];
|
|
69
|
+
if (current !== undefined && previous !== undefined && previous !== 0) {
|
|
70
|
+
returns.push((current - previous) / previous);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
|
75
|
+
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / returns.length;
|
|
76
|
+
|
|
77
|
+
return Math.sqrt(variance) * Math.sqrt(252); // Annualized volatility
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Calculate support and resistance levels
|
|
82
|
+
*/
|
|
83
|
+
export function findSupportResistance(ohlcv: OHLCV[], lookback: number = 20): { support: number; resistance: number } {
|
|
84
|
+
if (ohlcv.length < lookback) return { support: 0, resistance: 0 };
|
|
85
|
+
|
|
86
|
+
const recent = ohlcv.slice(-lookback);
|
|
87
|
+
const highs = recent.map(candle => candle.high);
|
|
88
|
+
const lows = recent.map(candle => candle.low);
|
|
89
|
+
|
|
90
|
+
const support = lows.reduce((min, low) => Math.min(min, low), lows[0] ?? 0);
|
|
91
|
+
const resistance = highs.reduce((max, high) => Math.max(max, high), highs[0] ?? 0);
|
|
92
|
+
|
|
93
|
+
return { support, resistance };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// =============================================================================
|
|
97
|
+
// Portfolio & Risk Calculations
|
|
98
|
+
// =============================================================================
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Calculate portfolio total value
|
|
102
|
+
*/
|
|
103
|
+
export function calculatePortfolioValue(portfolio: Portfolio): number {
|
|
104
|
+
const positionsValue = portfolio.positions.reduce((total, position) => {
|
|
105
|
+
return total + position.marketValue;
|
|
106
|
+
}, 0);
|
|
107
|
+
|
|
108
|
+
return portfolio.cash + positionsValue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Calculate position size as percentage of portfolio
|
|
113
|
+
*/
|
|
114
|
+
export function calculatePositionWeight(position: Position, portfolioValue: number): number {
|
|
115
|
+
if (portfolioValue === 0) return 0;
|
|
116
|
+
return (position.marketValue / portfolioValue) * 100;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Calculate unrealized P&L for a position
|
|
121
|
+
*/
|
|
122
|
+
export function calculateUnrealizedPnL(position: Position, currentPrice: number): number {
|
|
123
|
+
return (currentPrice - position.averagePrice) * position.quantity;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Calculate Sharpe ratio
|
|
128
|
+
*/
|
|
129
|
+
export function calculateSharpeRatio(returns: number[], riskFreeRate: number = 0.02): number {
|
|
130
|
+
if (returns.length === 0) return 0;
|
|
131
|
+
|
|
132
|
+
const avgReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length;
|
|
133
|
+
const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - avgReturn, 2), 0) / returns.length;
|
|
134
|
+
const stdDev = Math.sqrt(variance);
|
|
135
|
+
|
|
136
|
+
if (stdDev === 0) return 0;
|
|
137
|
+
return (avgReturn - riskFreeRate) / stdDev;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Calculate maximum drawdown
|
|
142
|
+
*/
|
|
143
|
+
export function calculateMaxDrawdown(portfolioValues: number[]): number {
|
|
144
|
+
if (portfolioValues.length < 2) return 0;
|
|
145
|
+
|
|
146
|
+
let maxDrawdown = 0;
|
|
147
|
+
let peak = portfolioValues[0] ?? 0;
|
|
148
|
+
|
|
149
|
+
for (let i = 1; i < portfolioValues.length; i++) {
|
|
150
|
+
const value = portfolioValues[i];
|
|
151
|
+
if (value !== undefined) {
|
|
152
|
+
if (value > peak) {
|
|
153
|
+
peak = value;
|
|
154
|
+
} else if (peak > 0) {
|
|
155
|
+
const drawdown = (peak - value) / peak;
|
|
156
|
+
maxDrawdown = Math.max(maxDrawdown, drawdown);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return maxDrawdown * 100; // Return as percentage
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Calculate Value at Risk (VaR) using historical simulation
|
|
166
|
+
*/
|
|
167
|
+
export function calculateVaR(returns: number[], confidence: number = 0.05): number {
|
|
168
|
+
if (returns.length === 0) return 0;
|
|
169
|
+
|
|
170
|
+
const sortedReturns = [...returns].sort((a, b) => a - b);
|
|
171
|
+
const index = Math.floor(returns.length * confidence);
|
|
172
|
+
|
|
173
|
+
return Math.abs(sortedReturns[index] || 0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// Order & Trading Calculations
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Calculate order fill ratio
|
|
182
|
+
*/
|
|
183
|
+
export function calculateFillRatio(order: Order): number {
|
|
184
|
+
if (order.quantity === 0) return 0;
|
|
185
|
+
return (order.filledQuantity / order.quantity) * 100;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Calculate slippage
|
|
190
|
+
*/
|
|
191
|
+
export function calculateSlippage(expectedPrice: number, executedPrice: number, side: OrderSide): number {
|
|
192
|
+
if (expectedPrice === 0) return 0;
|
|
193
|
+
|
|
194
|
+
const diff = side === 'buy'
|
|
195
|
+
? executedPrice - expectedPrice
|
|
196
|
+
: expectedPrice - executedPrice;
|
|
197
|
+
|
|
198
|
+
return (diff / expectedPrice) * 100;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Calculate optimal order size based on market impact
|
|
203
|
+
*/
|
|
204
|
+
export function calculateOptimalOrderSize(
|
|
205
|
+
availableLiquidity: number,
|
|
206
|
+
targetSize: number,
|
|
207
|
+
maxMarketImpact: number = 0.5 // 0.5% max impact
|
|
208
|
+
): number {
|
|
209
|
+
const maxSafeSize = availableLiquidity * (maxMarketImpact / 100);
|
|
210
|
+
return Math.min(targetSize, maxSafeSize);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// =============================================================================
|
|
214
|
+
// DeFi Calculations
|
|
215
|
+
// =============================================================================
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Calculate APY from APR
|
|
219
|
+
*/
|
|
220
|
+
export function aprToApy(apr: number, compoundingPeriods: number = 365): number {
|
|
221
|
+
return (Math.pow(1 + (apr / compoundingPeriods), compoundingPeriods) - 1) * 100;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Calculate yield farming returns
|
|
226
|
+
*/
|
|
227
|
+
export function calculateYieldFarmingReturns(
|
|
228
|
+
principal: number,
|
|
229
|
+
apy: number,
|
|
230
|
+
days: number,
|
|
231
|
+
compounding: boolean = true
|
|
232
|
+
): { totalReturn: number; profit: number } {
|
|
233
|
+
const periods = compounding ? days : 1;
|
|
234
|
+
const rate = apy / 100 / (compounding ? 365 : 1);
|
|
235
|
+
|
|
236
|
+
const totalReturn = compounding
|
|
237
|
+
? principal * Math.pow(1 + rate, periods)
|
|
238
|
+
: principal + (principal * rate * days);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
totalReturn,
|
|
242
|
+
profit: totalReturn - principal
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Calculate impermanent loss for liquidity provision
|
|
248
|
+
*/
|
|
249
|
+
export function calculateImpermanentLoss(
|
|
250
|
+
initialPrice1: number,
|
|
251
|
+
initialPrice2: number,
|
|
252
|
+
currentPrice1: number,
|
|
253
|
+
currentPrice2: number
|
|
254
|
+
): number {
|
|
255
|
+
const priceRatio = (currentPrice1 / currentPrice2) / (initialPrice1 / initialPrice2);
|
|
256
|
+
const impermanentLoss = (2 * Math.sqrt(priceRatio)) / (1 + priceRatio) - 1;
|
|
257
|
+
|
|
258
|
+
return Math.abs(impermanentLoss) * 100;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Compare yield opportunities
|
|
263
|
+
*/
|
|
264
|
+
export function rankYieldOpportunities(opportunities: YieldOpportunity[]): YieldOpportunity[] {
|
|
265
|
+
return opportunities.sort((a, b) => {
|
|
266
|
+
// Risk-adjusted APY calculation (simple risk penalty)
|
|
267
|
+
const riskPenalty = { low: 1, medium: 0.8, high: 0.6 };
|
|
268
|
+
const adjustedApyA = a.apy * riskPenalty[a.risk];
|
|
269
|
+
const adjustedApyB = b.apy * riskPenalty[b.risk];
|
|
270
|
+
|
|
271
|
+
return adjustedApyB - adjustedApyA;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// =============================================================================
|
|
276
|
+
// Utility Functions
|
|
277
|
+
// =============================================================================
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Convert timeframe to milliseconds
|
|
281
|
+
*/
|
|
282
|
+
export function timeFrameToMs(timeFrame: TimeFrame): number {
|
|
283
|
+
const units = {
|
|
284
|
+
second: 1000,
|
|
285
|
+
minute: 60 * 1000,
|
|
286
|
+
hour: 60 * 60 * 1000,
|
|
287
|
+
day: 24 * 60 * 60 * 1000,
|
|
288
|
+
week: 7 * 24 * 60 * 60 * 1000,
|
|
289
|
+
month: 30 * 24 * 60 * 60 * 1000
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
return timeFrame.value * units[timeFrame.unit];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Format currency with appropriate decimal places
|
|
297
|
+
*/
|
|
298
|
+
export function formatCurrency(amount: number, decimals: number = 2): string {
|
|
299
|
+
return new Intl.NumberFormat('en-US', {
|
|
300
|
+
style: 'currency',
|
|
301
|
+
currency: 'USD',
|
|
302
|
+
minimumFractionDigits: decimals,
|
|
303
|
+
maximumFractionDigits: decimals
|
|
304
|
+
}).format(amount);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Format percentage with sign
|
|
309
|
+
*/
|
|
310
|
+
export function formatPercentage(value: number, decimals: number = 2): string {
|
|
311
|
+
const sign = value >= 0 ? '+' : '';
|
|
312
|
+
return `${sign}${value.toFixed(decimals)}%`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Check if price is within range
|
|
317
|
+
*/
|
|
318
|
+
export function isPriceInRange(price: number, range: PriceRange): boolean {
|
|
319
|
+
return price >= range.min && price <= range.max;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Calculate distance from price to range bounds
|
|
324
|
+
*/
|
|
325
|
+
export function distanceToRange(price: number, range: PriceRange): { toMin: number; toMax: number } {
|
|
326
|
+
return {
|
|
327
|
+
toMin: Math.abs(price - range.min),
|
|
328
|
+
toMax: Math.abs(price - range.max)
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Generate trading session ID
|
|
334
|
+
*/
|
|
335
|
+
export function generateSessionId(): string {
|
|
336
|
+
const timestamp = Date.now().toString(36);
|
|
337
|
+
const random = Math.random().toString(36).substr(2, 5);
|
|
338
|
+
return `${timestamp}-${random}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Validate trading symbol format
|
|
343
|
+
*/
|
|
344
|
+
export function isValidSymbol(symbol: string): boolean {
|
|
345
|
+
// Basic validation for crypto trading pairs
|
|
346
|
+
const regex = /^[A-Z]{2,10}[\/\-][A-Z]{2,10}$/;
|
|
347
|
+
return regex.test(symbol);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Normalize symbol format
|
|
352
|
+
*/
|
|
353
|
+
export function normalizeSymbol(symbol: string): string {
|
|
354
|
+
return symbol.toUpperCase().replace('-', '/');
|
|
355
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trading Validation Utilities for autonoma Agent Core
|
|
3
|
+
*
|
|
4
|
+
* Validation functions for trading data, orders, and parameters.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { OrderRequest, Portfolio, RiskLimits } from './types.js';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Order Validation
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
export interface ValidationResult {
|
|
14
|
+
isValid: boolean;
|
|
15
|
+
errors: string[];
|
|
16
|
+
warnings?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate order request
|
|
21
|
+
*/
|
|
22
|
+
export function validateOrderRequest(orderRequest: OrderRequest): ValidationResult {
|
|
23
|
+
const errors: string[] = [];
|
|
24
|
+
const warnings: string[] = [];
|
|
25
|
+
|
|
26
|
+
// Basic required fields
|
|
27
|
+
if (!orderRequest.symbol) {
|
|
28
|
+
errors.push('Symbol is required');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!orderRequest.side || !['buy', 'sell'].includes(orderRequest.side)) {
|
|
32
|
+
errors.push('Valid side (buy/sell) is required');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!orderRequest.type || !['market', 'limit', 'stop', 'stop-limit'].includes(orderRequest.type)) {
|
|
36
|
+
errors.push('Valid order type is required');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!orderRequest.quantity || orderRequest.quantity <= 0) {
|
|
40
|
+
errors.push('Quantity must be positive');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Price validation for limit orders
|
|
44
|
+
if (orderRequest.type === 'limit' && (!orderRequest.price || orderRequest.price <= 0)) {
|
|
45
|
+
errors.push('Price is required for limit orders');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Stop price validation for stop orders
|
|
49
|
+
if (orderRequest.type === 'stop' && (!orderRequest.stopPrice || orderRequest.stopPrice <= 0)) {
|
|
50
|
+
errors.push('Stop price is required for stop orders');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Symbol format validation
|
|
54
|
+
if (orderRequest.symbol && !isValidSymbolFormat(orderRequest.symbol)) {
|
|
55
|
+
errors.push('Invalid symbol format. Expected format: BASE/QUOTE');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Warnings for potential issues
|
|
59
|
+
if (orderRequest.quantity && orderRequest.quantity > 1000000) {
|
|
60
|
+
warnings.push('Large order quantity may cause market impact');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
isValid: errors.length === 0,
|
|
65
|
+
errors,
|
|
66
|
+
warnings
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validate symbol format
|
|
72
|
+
*/
|
|
73
|
+
function isValidSymbolFormat(symbol: string): boolean {
|
|
74
|
+
const regex = /^[A-Z]{2,10}\/[A-Z]{2,10}$/;
|
|
75
|
+
return regex.test(symbol);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate order against risk limits
|
|
80
|
+
*/
|
|
81
|
+
export function validateOrderAgainstRiskLimits(
|
|
82
|
+
orderRequest: OrderRequest,
|
|
83
|
+
portfolio: Portfolio,
|
|
84
|
+
riskLimits: RiskLimits
|
|
85
|
+
): ValidationResult {
|
|
86
|
+
const errors: string[] = [];
|
|
87
|
+
const warnings: string[] = [];
|
|
88
|
+
|
|
89
|
+
// Check if symbol is blacklisted
|
|
90
|
+
if (riskLimits.blacklistedSymbols.includes(orderRequest.symbol)) {
|
|
91
|
+
errors.push(`Symbol ${orderRequest.symbol} is blacklisted`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Calculate order value
|
|
95
|
+
const orderValue = orderRequest.price ? orderRequest.price * orderRequest.quantity : 0;
|
|
96
|
+
|
|
97
|
+
// Check position size limit
|
|
98
|
+
if (orderValue > 0) {
|
|
99
|
+
const positionSizePercent = (orderValue / portfolio.totalValue) * 100;
|
|
100
|
+
if (positionSizePercent > riskLimits.maxPositionSize) {
|
|
101
|
+
errors.push(`Order would exceed maximum position size of ${riskLimits.maxPositionSize}%`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check available cash for buy orders
|
|
106
|
+
if (orderRequest.side === 'buy' && orderValue > portfolio.cash) {
|
|
107
|
+
errors.push('Insufficient cash for buy order');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check position availability for sell orders
|
|
111
|
+
if (orderRequest.side === 'sell') {
|
|
112
|
+
const position = portfolio.positions.find(p => p.symbol === orderRequest.symbol);
|
|
113
|
+
if (!position || position.quantity < orderRequest.quantity) {
|
|
114
|
+
errors.push('Insufficient position for sell order');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
isValid: errors.length === 0,
|
|
120
|
+
errors,
|
|
121
|
+
warnings
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// =============================================================================
|
|
126
|
+
// Portfolio Validation
|
|
127
|
+
// =============================================================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Validate portfolio consistency
|
|
131
|
+
*/
|
|
132
|
+
export function validatePortfolio(portfolio: Portfolio): ValidationResult {
|
|
133
|
+
const errors: string[] = [];
|
|
134
|
+
const warnings: string[] = [];
|
|
135
|
+
|
|
136
|
+
// Check for negative cash
|
|
137
|
+
if (portfolio.cash < 0) {
|
|
138
|
+
errors.push('Portfolio cash cannot be negative');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check for negative positions
|
|
142
|
+
portfolio.positions.forEach((position, index) => {
|
|
143
|
+
if (position.quantity < 0) {
|
|
144
|
+
errors.push(`Position ${index} has negative quantity`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (position.averagePrice <= 0) {
|
|
148
|
+
errors.push(`Position ${index} has invalid average price`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Check for duplicate positions
|
|
153
|
+
const symbols = portfolio.positions.map(p => p.symbol);
|
|
154
|
+
const duplicates = symbols.filter((symbol, index) => symbols.indexOf(symbol) !== index);
|
|
155
|
+
if (duplicates.length > 0) {
|
|
156
|
+
errors.push(`Duplicate positions found: ${duplicates.join(', ')}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Warnings for portfolio health
|
|
160
|
+
const totalValue = portfolio.totalValue;
|
|
161
|
+
if (totalValue > 0) {
|
|
162
|
+
const cashPercent = (portfolio.cash / totalValue) * 100;
|
|
163
|
+
if (cashPercent > 90) {
|
|
164
|
+
warnings.push('Portfolio is highly concentrated in cash');
|
|
165
|
+
} else if (cashPercent < 5) {
|
|
166
|
+
warnings.push('Portfolio has very low cash reserves');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
isValid: errors.length === 0,
|
|
172
|
+
errors,
|
|
173
|
+
warnings
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// =============================================================================
|
|
178
|
+
// Risk Validation
|
|
179
|
+
// =============================================================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Validate risk limits configuration
|
|
183
|
+
*/
|
|
184
|
+
export function validateRiskLimits(riskLimits: RiskLimits): ValidationResult {
|
|
185
|
+
const errors: string[] = [];
|
|
186
|
+
const warnings: string[] = [];
|
|
187
|
+
|
|
188
|
+
// Check percentage limits
|
|
189
|
+
if (riskLimits.maxPositionSize <= 0 || riskLimits.maxPositionSize > 100) {
|
|
190
|
+
errors.push('Max position size must be between 0 and 100 percent');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (riskLimits.maxDrawdown <= 0 || riskLimits.maxDrawdown > 100) {
|
|
194
|
+
errors.push('Max drawdown must be between 0 and 100 percent');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check dollar limits
|
|
198
|
+
if (riskLimits.maxDailyLoss <= 0) {
|
|
199
|
+
errors.push('Max daily loss must be positive');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check leverage
|
|
203
|
+
if (riskLimits.maxLeverage <= 0) {
|
|
204
|
+
errors.push('Max leverage must be positive');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Warnings for conservative limits
|
|
208
|
+
if (riskLimits.maxPositionSize > 20) {
|
|
209
|
+
warnings.push('Position size limit above 20% may be risky');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (riskLimits.maxLeverage > 10) {
|
|
213
|
+
warnings.push('High leverage increases risk significantly');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
isValid: errors.length === 0,
|
|
218
|
+
errors,
|
|
219
|
+
warnings
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if portfolio violates risk limits
|
|
225
|
+
*/
|
|
226
|
+
export function checkRiskLimitViolations(
|
|
227
|
+
portfolio: Portfolio,
|
|
228
|
+
riskLimits: RiskLimits,
|
|
229
|
+
dailyPnL: number
|
|
230
|
+
): ValidationResult {
|
|
231
|
+
const errors: string[] = [];
|
|
232
|
+
const warnings: string[] = [];
|
|
233
|
+
|
|
234
|
+
// Check daily loss limit
|
|
235
|
+
if (dailyPnL < 0 && Math.abs(dailyPnL) > riskLimits.maxDailyLoss) {
|
|
236
|
+
errors.push(`Daily loss of $${Math.abs(dailyPnL)} exceeds limit of $${riskLimits.maxDailyLoss}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check position concentration
|
|
240
|
+
portfolio.positions.forEach(position => {
|
|
241
|
+
const positionPercent = (position.marketValue / portfolio.totalValue) * 100;
|
|
242
|
+
if (positionPercent > riskLimits.maxPositionSize) {
|
|
243
|
+
errors.push(`Position in ${position.symbol} (${positionPercent.toFixed(1)}%) exceeds limit of ${riskLimits.maxPositionSize}%`);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Check for blacklisted symbols
|
|
248
|
+
const blacklistedPositions = portfolio.positions.filter(p =>
|
|
249
|
+
riskLimits.blacklistedSymbols.includes(p.symbol)
|
|
250
|
+
);
|
|
251
|
+
if (blacklistedPositions.length > 0) {
|
|
252
|
+
errors.push(`Positions in blacklisted symbols: ${blacklistedPositions.map(p => p.symbol).join(', ')}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
isValid: errors.length === 0,
|
|
257
|
+
errors,
|
|
258
|
+
warnings
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// =============================================================================
|
|
263
|
+
// Price Validation
|
|
264
|
+
// =============================================================================
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Validate price data
|
|
268
|
+
*/
|
|
269
|
+
export function validatePriceData(prices: number[]): ValidationResult {
|
|
270
|
+
const errors: string[] = [];
|
|
271
|
+
const warnings: string[] = [];
|
|
272
|
+
|
|
273
|
+
if (prices.length === 0) {
|
|
274
|
+
errors.push('Price data is empty');
|
|
275
|
+
return { isValid: false, errors, warnings };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Check for negative prices
|
|
279
|
+
const negativeCount = prices.filter(price => price <= 0).length;
|
|
280
|
+
if (negativeCount > 0) {
|
|
281
|
+
errors.push(`Found ${negativeCount} non-positive prices`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Check for extreme outliers
|
|
285
|
+
const mean = prices.reduce((sum, price) => sum + price, 0) / prices.length;
|
|
286
|
+
const outliers = prices.filter(price => Math.abs(price - mean) > mean * 2);
|
|
287
|
+
if (outliers.length > 0) {
|
|
288
|
+
warnings.push(`Found ${outliers.length} potential price outliers`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
isValid: errors.length === 0,
|
|
293
|
+
errors,
|
|
294
|
+
warnings
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Validate price is within reasonable bounds
|
|
300
|
+
*/
|
|
301
|
+
export function validatePriceRange(
|
|
302
|
+
price: number,
|
|
303
|
+
referencePrice: number,
|
|
304
|
+
maxDeviationPercent: number = 10
|
|
305
|
+
): ValidationResult {
|
|
306
|
+
const errors: string[] = [];
|
|
307
|
+
|
|
308
|
+
if (price <= 0) {
|
|
309
|
+
errors.push('Price must be positive');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const deviation = Math.abs((price - referencePrice) / referencePrice) * 100;
|
|
313
|
+
if (deviation > maxDeviationPercent) {
|
|
314
|
+
errors.push(`Price deviation of ${deviation.toFixed(1)}% exceeds maximum of ${maxDeviationPercent}%`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
isValid: errors.length === 0,
|
|
319
|
+
errors
|
|
320
|
+
};
|
|
321
|
+
}
|