gfil-trading-calculators 1.0.0

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.
@@ -0,0 +1,126 @@
1
+ /**
2
+ * GFIL Trading Calculators — JavaScript Quick Start Examples
3
+ *
4
+ * Run: node examples/basic_usage.js
5
+ */
6
+
7
+ import {
8
+ positionSize, positionSizeATR,
9
+ pipValue, pipsFromPrice, priceFromPips,
10
+ atrStopLoss, atrTakeProfit,
11
+ fibonacciAll,
12
+ kellyCriterion,
13
+ drawdown, recoveryNeeded,
14
+ marginRequired,
15
+ riskReward, profitFactor,
16
+ compoundInterest,
17
+ getInstrument,
18
+ } from '../src/index.js';
19
+
20
+ const sep = '='.repeat(60);
21
+ const sep2 = '-'.repeat(40);
22
+
23
+ console.log(sep);
24
+ console.log('GFIL Trading Calculators — JavaScript Examples');
25
+ console.log(sep);
26
+
27
+ // ── 1. Position Sizing ──────────────────────────────────────
28
+ console.log(`\n📊 1. POSITION SIZING\n${sep2}`);
29
+
30
+ let ps = positionSize(10000, 1.0, 20, 'EURUSD');
31
+ console.log(` EURUSD: ${ps.lots} lots, risk $${ps.riskAmount}`);
32
+
33
+ ps = positionSize(5000, 2.0, 50, 'XAUUSD');
34
+ console.log(` XAUUSD: ${ps.lots} lots, risk $${ps.riskAmount}`);
35
+
36
+ ps = positionSize(10000, 1.0, 500, 'BTCUSD');
37
+ console.log(` BTCUSD: ${ps.lots} lots, risk $${ps.riskAmount}`);
38
+
39
+ ps = positionSize(5000, 1.5, 30, 'GOLD'); // alias
40
+ console.log(` GOLD (alias): ${ps.lots} lots, risk $${ps.riskAmount}`);
41
+
42
+ // ── 2. ATR-Based Position Sizing ───────────────────────────
43
+ console.log(`\n📈 2. ATR-BASED POSITION SIZING\n${sep2}`);
44
+
45
+ const psAtr = positionSizeATR(10000, 1.0, 0.0050, 1.5, 'EURUSD');
46
+ console.log(` EURUSD: ${psAtr.lots} lots, SL ${psAtr.stopLossPips} pips away`);
47
+
48
+ // ── 3. Pip Value ───────────────────────────────────────────
49
+ console.log(`\n💰 3. PIP VALUE\n${sep2}`);
50
+
51
+ for (const sym of ['EURUSD', 'XAUUSD', 'BTCUSD', 'SPX500', 'USOIL']) {
52
+ const pv = pipValue(sym, 1.0);
53
+ console.log(` ${sym}: 1 pip = $${pv.onePip}, 10 pips = $${pv.tenPips}`);
54
+ }
55
+
56
+ // ── 4. Fibonacci Levels ───────────────────────────────────
57
+ console.log(`\n📐 4. FIBONACCI LEVELS\n${sep2}`);
58
+
59
+ const fib = fibonacciAll(1.1100, 1.1000, 'up');
60
+ console.log(' Retracement (uptrend pullback):');
61
+ for (const level of fib.retracement) {
62
+ console.log(` ${level.label.padEnd(25)} → ${level.price.toFixed(4)}`);
63
+ }
64
+ console.log(' Extension (first 3 targets):');
65
+ for (const level of fib.extension.slice(0, 3)) {
66
+ console.log(` ${level.label.padEnd(25)} → ${level.price.toFixed(4)}`);
67
+ }
68
+
69
+ // ── 5. Kelly Criterion ────────────────────────────────────
70
+ console.log(`\n🎲 5. KELLY CRITERION\n${sep2}`);
71
+
72
+ const kc = kellyCriterion(0.60, 200, 100, 0.5);
73
+ console.log(` Full Kelly: ${kc.kellyPercent}%`);
74
+ console.log(` Half-Kelly (recommended): ${kc.adjustedPercent}%`);
75
+
76
+ // ── 6. Drawdown ────────────────────────────────────────────
77
+ console.log(`\n📉 6. DRAWDOWN ANALYSIS\n${sep2}`);
78
+
79
+ const dd = drawdown([10000, 11000, 10500, 12000, 10800, 11500, 13000]);
80
+ console.log(` Max drawdown: ${dd.maxDrawdownPercent}%`);
81
+ console.log(` Recovery needed: ${dd.recoveryPercent}%`);
82
+
83
+ console.log('\n Drawdown Recovery Table:');
84
+ for (const ddPct of [10, 20, 30, 50, 75, 90]) {
85
+ console.log(` ${ddPct}% drawdown → needs ${recoveryNeeded(ddPct)}% gain`);
86
+ }
87
+
88
+ // ── 7. Risk/Reward ─────────────────────────────────────────
89
+ console.log(`\n⚖️ 7. RISK/REWARD\n${sep2}`);
90
+
91
+ const rr = riskReward(1.1050, 1.1000, 1.1150, 'long');
92
+ console.log(` RRR: ${rr.rrr}:1`);
93
+ console.log(` Breakeven win rate: ${rr.breakevenWinrate}%`);
94
+ console.log(` Favorable? ${rr.isFavorable ? '✅' : '❌'}`);
95
+
96
+ const pf = profitFactor(5000, 2500);
97
+ console.log(` Profit factor: ${pf.profitFactor} (${pf.assessment})`);
98
+
99
+ // ── 8. Margin ──────────────────────────────────────────────
100
+ console.log(`\n🏦 8. MARGIN CALCULATION\n${sep2}`);
101
+
102
+ let mg = marginRequired('EURUSD', 1.0, 1.0850, 100);
103
+ console.log(` EURUSD: 1 lot @ 1.0850, 100:1 → margin $${mg.margin}`);
104
+
105
+ mg = marginRequired('XAUUSD', 0.5, 2350, 200);
106
+ console.log(` XAUUSD: 0.5 lot @ $2350, 200:1 → margin $${mg.margin}`);
107
+
108
+ // ── 9. Compound Interest ──────────────────────────────────
109
+ console.log(`\n📈 9. COMPOUND INTEREST\n${sep2}`);
110
+
111
+ const ci = compoundInterest(10000, 12, 5, 12);
112
+ console.log(` $10,000 @ 12% for 5 years → $${ci.finalValue.toLocaleString()}`);
113
+ console.log(` Total interest: $${ci.totalInterest.toLocaleString()}`);
114
+
115
+ // ── 10. ATR Stop/Take Profit ───────────────────────────────
116
+ console.log(`\n🎯 10. ATR STOP & TAKE PROFIT\n${sep2}`);
117
+
118
+ const sl = atrStopLoss(1.1050, 0.0050, 'long', 1.5);
119
+ console.log(` Long EURUSD @ 1.1050: SL = ${sl.stopLossPrice}`);
120
+
121
+ const tp = atrTakeProfit(1.1050, 0.0050, 'long', 2.0, 1.5);
122
+ console.log(` Long EURUSD @ 1.1050: SL = ${tp.stopLossPrice}, TP = ${tp.takeProfitPrice}`);
123
+
124
+ console.log(`\n${sep}`);
125
+ console.log('All examples completed successfully!');
126
+ console.log(sep);
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "gfil-trading-calculators",
3
+ "version": "1.0.0",
4
+ "description": "Trading risk calculators covering Forex, Gold, Crypto, and Indices. The only library with proper pip values for 30+ instruments.",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "node --experimental-vm-modules test/index.test.js"
9
+ },
10
+ "keywords": [
11
+ "forex", "trading", "calculator", "risk-management", "position-size",
12
+ "pip", "kelly", "atr", "fibonacci", "gold", "crypto", "xauusd",
13
+ "drawdown", "margin", "lot-size"
14
+ ],
15
+ "author": "LiuDecai <contact@gfil-lab.com>",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/liudapao880807-arch/gfil-trading-calculators"
20
+ },
21
+ "homepage": "https://github.com/liudapao880807-arch/gfil-trading-calculators#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/liudapao880807-arch/gfil-trading-calculators/issues"
24
+ }
25
+ }
package/src/index.js ADDED
@@ -0,0 +1,472 @@
1
+ /**
2
+ * GFIL Trading Calculators — JavaScript Edition
3
+ *
4
+ * The only trading calculator library covering Forex + Gold + Crypto + Indices
5
+ * with proper pip values and contract sizes.
6
+ *
7
+ * Zero dependencies. Works in Node.js and browser.
8
+ *
9
+ * @module gfil-trading-calculators
10
+ * @version 1.0.0
11
+ * @author LiuDecai <contact@gfil-lab.com>
12
+ * @license MIT
13
+ */
14
+
15
+ // ============================================================================
16
+ // INSTRUMENT DATABASE
17
+ // ============================================================================
18
+
19
+ const STANDARD_LOT = 100000;
20
+ const GOLD_LOT = 100;
21
+
22
+ const TYPE_FOREX = 'forex';
23
+ const TYPE_GOLD = 'gold';
24
+ const TYPE_SILVER = 'silver';
25
+ const TYPE_CRYPTO = 'crypto';
26
+ const TYPE_INDEX = 'index';
27
+ const TYPE_ENERGY = 'energy';
28
+
29
+ /**
30
+ * Instrument metadata for 30+ trading instruments.
31
+ * pipSize: smallest standard price movement
32
+ * contractSize: units per standard lot
33
+ * pipValuePerLotUSD: USD value of 1 pip per standard lot
34
+ * quoteCurrency: quote currency of the pair
35
+ */
36
+ const INSTRUMENTS = {
37
+ // Forex Majors
38
+ EURUSD: { symbol: 'EURUSD', name: 'Euro / US Dollar', type: TYPE_FOREX, pipSize: 0.0001, contractSize: STANDARD_LOT, pipValuePerLotUSD: 10, quoteCurrency: 'USD' },
39
+ GBPUSD: { symbol: 'GBPUSD', name: 'British Pound / US Dollar', type: TYPE_FOREX, pipSize: 0.0001, contractSize: STANDARD_LOT, pipValuePerLotUSD: 10, quoteCurrency: 'USD' },
40
+ USDJPY: { symbol: 'USDJPY', name: 'US Dollar / Japanese Yen', type: TYPE_FOREX, pipSize: 0.01, contractSize: STANDARD_LOT, pipValuePerLotUSD: 6.50, quoteCurrency: 'JPY' },
41
+ USDCHF: { symbol: 'USDCHF', name: 'US Dollar / Swiss Franc', type: TYPE_FOREX, pipSize: 0.0001, contractSize: STANDARD_LOT, pipValuePerLotUSD: 11.20, quoteCurrency: 'CHF' },
42
+ AUDUSD: { symbol: 'AUDUSD', name: 'Australian Dollar / US Dollar', type: TYPE_FOREX, pipSize: 0.0001, contractSize: STANDARD_LOT, pipValuePerLotUSD: 10, quoteCurrency: 'USD' },
43
+ NZDUSD: { symbol: 'NZDUSD', name: 'New Zealand Dollar / US Dollar', type: TYPE_FOREX, pipSize: 0.0001, contractSize: STANDARD_LOT, pipValuePerLotUSD: 10, quoteCurrency: 'USD' },
44
+ USDCAD: { symbol: 'USDCAD', name: 'US Dollar / Canadian Dollar', type: TYPE_FOREX, pipSize: 0.0001, contractSize: STANDARD_LOT, pipValuePerLotUSD: 7.30, quoteCurrency: 'CAD' },
45
+ // Cross Pairs
46
+ EURJPY: { symbol: 'EURJPY', name: 'Euro / Japanese Yen', type: TYPE_FOREX, pipSize: 0.01, contractSize: STANDARD_LOT, pipValuePerLotUSD: 6.50, quoteCurrency: 'JPY' },
47
+ GBPJPY: { symbol: 'GBPJPY', name: 'British Pound / Japanese Yen', type: TYPE_FOREX, pipSize: 0.01, contractSize: STANDARD_LOT, pipValuePerLotUSD: 6.50, quoteCurrency: 'JPY' },
48
+ EURGBP: { symbol: 'EURGBP', name: 'Euro / British Pound', type: TYPE_FOREX, pipSize: 0.0001, contractSize: STANDARD_LOT, pipValuePerLotUSD: 12.70, quoteCurrency: 'GBP' },
49
+ EURAUD: { symbol: 'EURAUD', name: 'Euro / Australian Dollar', type: TYPE_FOREX, pipSize: 0.0001, contractSize: STANDARD_LOT, pipValuePerLotUSD: 6.60, quoteCurrency: 'AUD' },
50
+ GBPAUD: { symbol: 'GBPAUD', name: 'British Pound / Australian Dollar', type: TYPE_FOREX, pipSize: 0.0001, contractSize: STANDARD_LOT, pipValuePerLotUSD: 6.60, quoteCurrency: 'AUD' },
51
+ GBPCAD: { symbol: 'GBPCAD', name: 'British Pound / Canadian Dollar', type: TYPE_FOREX, pipSize: 0.0001, contractSize: STANDARD_LOT, pipValuePerLotUSD: 7.30, quoteCurrency: 'CAD' },
52
+ // Metals
53
+ XAUUSD: { symbol: 'XAUUSD', name: 'Gold / US Dollar', type: TYPE_GOLD, pipSize: 0.10, contractSize: GOLD_LOT, pipValuePerLotUSD: 10, quoteCurrency: 'USD' },
54
+ XAGUSD: { symbol: 'XAGUSD', name: 'Silver / US Dollar', type: TYPE_SILVER, pipSize: 0.001, contractSize: 5000, pipValuePerLotUSD: 5, quoteCurrency: 'USD' },
55
+ // Crypto
56
+ BTCUSD: { symbol: 'BTCUSD', name: 'Bitcoin / US Dollar', type: TYPE_CRYPTO, pipSize: 0.01, contractSize: 1, pipValuePerLotUSD: 1, quoteCurrency: 'USD' },
57
+ ETHUSD: { symbol: 'ETHUSD', name: 'Ethereum / US Dollar', type: TYPE_CRYPTO, pipSize: 0.01, contractSize: 1, pipValuePerLotUSD: 1, quoteCurrency: 'USD' },
58
+ SOLUSD: { symbol: 'SOLUSD', name: 'Solana / US Dollar', type: TYPE_CRYPTO, pipSize: 0.001, contractSize: 1, pipValuePerLotUSD: 1, quoteCurrency: 'USD' },
59
+ ADAUSD: { symbol: 'ADAUSD', name: 'Cardano / US Dollar', type: TYPE_CRYPTO, pipSize: 0.0001, contractSize: 1, pipValuePerLotUSD: 1, quoteCurrency: 'USD' },
60
+ DOGEUSD: { symbol: 'DOGEUSD', name: 'Dogecoin / US Dollar', type: TYPE_CRYPTO, pipSize: 0.00001, contractSize: 1, pipValuePerLotUSD: 1, quoteCurrency: 'USD' },
61
+ BNBUSD: { symbol: 'BNBUSD', name: 'BNB / US Dollar', type: TYPE_CRYPTO, pipSize: 0.01, contractSize: 1, pipValuePerLotUSD: 1, quoteCurrency: 'USD' },
62
+ XRPUSD: { symbol: 'XRPUSD', name: 'XRP / US Dollar', type: TYPE_CRYPTO, pipSize: 0.0001, contractSize: 1, pipValuePerLotUSD: 1, quoteCurrency: 'USD' },
63
+ // Indices
64
+ SPX500: { symbol: 'SPX500', name: 'S&P 500', type: TYPE_INDEX, pipSize: 0.25, contractSize: 1, pipValuePerLotUSD: 0.25, quoteCurrency: 'USD' },
65
+ NAS100: { symbol: 'NAS100', name: 'Nasdaq 100', type: TYPE_INDEX, pipSize: 0.50, contractSize: 1, pipValuePerLotUSD: 0.50, quoteCurrency: 'USD' },
66
+ US30: { symbol: 'US30', name: 'Dow Jones 30', type: TYPE_INDEX, pipSize: 1.0, contractSize: 1, pipValuePerLotUSD: 1, quoteCurrency: 'USD' },
67
+ GER40: { symbol: 'GER40', name: 'DAX 40', type: TYPE_INDEX, pipSize: 0.50, contractSize: 1, pipValuePerLotUSD: 0.50, quoteCurrency: 'EUR' },
68
+ UK100: { symbol: 'UK100', name: 'FTSE 100', type: TYPE_INDEX, pipSize: 0.50, contractSize: 1, pipValuePerLotUSD: 0.50, quoteCurrency: 'GBP' },
69
+ HK50: { symbol: 'HK50', name: 'Hang Seng 50', type: TYPE_INDEX, pipSize: 1.0, contractSize: 1, pipValuePerLotUSD: 1, quoteCurrency: 'HKD' },
70
+ JPN225: { symbol: 'JPN225', name: 'Nikkei 225', type: TYPE_INDEX, pipSize: 1.0, contractSize: 1, pipValuePerLotUSD: 1, quoteCurrency: 'JPY' },
71
+ // Energy
72
+ USOIL: { symbol: 'USOIL', name: 'WTI Crude Oil', type: TYPE_ENERGY, pipSize: 0.01, contractSize: 1000, pipValuePerLotUSD: 10, quoteCurrency: 'USD' },
73
+ UKOIL: { symbol: 'UKOIL', name: 'Brent Crude Oil', type: TYPE_ENERGY, pipSize: 0.01, contractSize: 1000, pipValuePerLotUSD: 10, quoteCurrency: 'USD' },
74
+ NGAS: { symbol: 'NGAS', name: 'Natural Gas', type: TYPE_ENERGY, pipSize: 0.001, contractSize: 10000, pipValuePerLotUSD: 10, quoteCurrency: 'USD' },
75
+ };
76
+
77
+ const ALIASES = {
78
+ GOLD: 'XAUUSD', SILVER: 'XAGUSD', BTC: 'BTCUSD', ETH: 'ETHUSD',
79
+ SOL: 'SOLUSD', DOGE: 'DOGEUSD', BNB: 'BNBUSD', XRP: 'XRPUSD', ADA: 'ADAUSD',
80
+ SP500: 'SPX500', NASDAQ: 'NAS100', DAX: 'GER40', FTSE: 'UK100', DOW: 'US30',
81
+ NIKKEI: 'JPN225', WTI: 'USOIL', BRENT: 'UKOIL', NATGAS: 'NGAS',
82
+ };
83
+
84
+ /**
85
+ * Look up instrument by symbol or alias.
86
+ */
87
+ export function getInstrument(symbol) {
88
+ const upper = symbol.toUpperCase().replace(/[\/\-]/g, '');
89
+ if (INSTRUMENTS[upper]) return INSTRUMENTS[upper];
90
+ if (ALIASES[upper]) return INSTRUMENTS[ALIASES[upper]];
91
+ throw new Error(`Unknown instrument: '${symbol}'. Available: ${Object.keys(INSTRUMENTS).join(', ')}`);
92
+ }
93
+
94
+ // ============================================================================
95
+ // POSITION SIZE
96
+ // ============================================================================
97
+
98
+ /**
99
+ * Calculate position size in lots for any trading instrument.
100
+ *
101
+ * @param {number} accountBalance - Account balance in account currency
102
+ * @param {number} riskPercent - Risk per trade as percentage (1.0 = 1%)
103
+ * @param {number} stopLossPips - Stop loss distance in pips
104
+ * @param {string} symbol - Trading instrument (default: 'EURUSD')
105
+ * @param {number} [pipValuePerLot] - Override pip value per lot
106
+ * @returns {Object} Position size result
107
+ *
108
+ * @example
109
+ * positionSize(10000, 1.0, 20, 'EURUSD')
110
+ * // → { lots: 0.5, riskAmount: 100, pipValue: 10, ... }
111
+ */
112
+ export function positionSize(accountBalance, riskPercent, stopLossPips, symbol = 'EURUSD', pipValuePerLot = null) {
113
+ if (accountBalance <= 0) throw new Error(`Account balance must be positive, got ${accountBalance}`);
114
+ if (riskPercent <= 0 || riskPercent > 100) throw new Error(`Risk percent must be 0-100, got ${riskPercent}`);
115
+ if (stopLossPips <= 0) throw new Error(`Stop loss pips must be positive, got ${stopLossPips}`);
116
+
117
+ const inst = getInstrument(symbol);
118
+ const pv = pipValuePerLot ?? inst.pipValuePerLotUSD;
119
+ if (pv <= 0) throw new Error(`Pip value must be positive for ${symbol}`);
120
+
121
+ const riskAmount = accountBalance * (riskPercent / 100);
122
+ const lots = riskAmount / (stopLossPips * pv);
123
+
124
+ return {
125
+ lots: Math.round(lots * 100) / 100,
126
+ riskAmount: Math.round(riskAmount * 100) / 100,
127
+ pipValue: pv,
128
+ microLots: Math.round(lots * 100),
129
+ miniLots: Math.round(lots * 10 * 10) / 10,
130
+ symbol,
131
+ instrument: inst,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Calculate position size using ATR-based stop loss.
137
+ */
138
+ export function positionSizeATR(accountBalance, riskPercent, atrValue, atrMultiplier = 1.5, symbol = 'EURUSD', pipValuePerLot = null) {
139
+ if (atrValue <= 0) throw new Error(`ATR must be positive, got ${atrValue}`);
140
+ const inst = getInstrument(symbol);
141
+ const stopLossPrice = atrValue * atrMultiplier;
142
+ const stopLossPips = stopLossPrice / inst.pipSize;
143
+ const result = positionSize(accountBalance, riskPercent, stopLossPips, symbol, pipValuePerLot);
144
+ result.stopLossPrice = Math.round(stopLossPrice * 100000) / 100000;
145
+ result.stopLossPips = Math.round(stopLossPips * 10) / 10;
146
+ result.atrValue = atrValue;
147
+ result.atrMultiplier = atrMultiplier;
148
+ return result;
149
+ }
150
+
151
+ // ============================================================================
152
+ // PIP VALUE
153
+ // ============================================================================
154
+
155
+ /**
156
+ * Calculate pip value for any trading instrument.
157
+ *
158
+ * @example
159
+ * pipValue('EURUSD', 1.0) // → { onePip: 10, tenPips: 100, ... }
160
+ * pipValue('XAUUSD', 0.1) // → { onePip: 1, ... }
161
+ */
162
+ export function pipValue(symbol = 'EURUSD', lots = 1.0) {
163
+ const inst = getInstrument(symbol);
164
+ const pvPerLot = inst.pipValuePerLotUSD;
165
+ const onePip = pvPerLot * lots;
166
+ return {
167
+ onePip: Math.round(onePip * 10000) / 10000,
168
+ tenPips: Math.round(onePip * 10 * 100) / 100,
169
+ hundredPips: Math.round(onePip * 100 * 100) / 100,
170
+ pipValuePerLot: pvPerLot,
171
+ lots,
172
+ symbol,
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Convert price difference to pips.
178
+ * @example
179
+ * pipsFromPrice('EURUSD', 0.0020) // → 20
180
+ * pipsFromPrice('XAUUSD', 5.0) // → 50
181
+ */
182
+ export function pipsFromPrice(symbol, priceDiff) {
183
+ const inst = getInstrument(symbol);
184
+ return Math.abs(priceDiff) / inst.pipSize;
185
+ }
186
+
187
+ /**
188
+ * Convert pips to price difference.
189
+ * @example
190
+ * priceFromPips('EURUSD', 20) // → 0.002
191
+ */
192
+ export function priceFromPips(symbol, pips) {
193
+ const inst = getInstrument(symbol);
194
+ return pips * inst.pipSize;
195
+ }
196
+
197
+ // ============================================================================
198
+ // ATR
199
+ // ============================================================================
200
+
201
+ /**
202
+ * Calculate True Range for a single period.
203
+ */
204
+ export function trueRange(high, low, prevClose) {
205
+ return Math.max(high - low, Math.abs(high - prevClose), Math.abs(low - prevClose));
206
+ }
207
+
208
+ /**
209
+ * Calculate ATR stop loss level.
210
+ * @example
211
+ * atrStopLoss(1.1050, 0.0050, 'long', 1.5)
212
+ * // → { stopLossPrice: 1.0975, ... }
213
+ */
214
+ export function atrStopLoss(entryPrice, atrValue, direction = 'long', multiplier = 1.5) {
215
+ const stopDistance = atrValue * multiplier;
216
+ const stopPrice = direction === 'long' ? entryPrice - stopDistance : entryPrice + stopDistance;
217
+ return {
218
+ stopLossPrice: Math.round(stopPrice * 100000) / 100000,
219
+ entryPrice,
220
+ atrValue,
221
+ atrMultiplier: multiplier,
222
+ stopDistancePrice: Math.round(stopDistance * 100000) / 100000,
223
+ direction,
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Calculate ATR-based take profit with risk/reward ratio.
229
+ */
230
+ export function atrTakeProfit(entryPrice, atrValue, direction = 'long', riskRewardRatio = 2.0, atrMultiplier = 1.5) {
231
+ const stop = atrStopLoss(entryPrice, atrValue, direction, atrMultiplier);
232
+ const stopDistance = Math.abs(entryPrice - stop.stopLossPrice);
233
+ const tpDistance = stopDistance * riskRewardRatio;
234
+ const tpPrice = direction === 'long' ? entryPrice + tpDistance : entryPrice - tpDistance;
235
+ return {
236
+ entryPrice,
237
+ stopLossPrice: stop.stopLossPrice,
238
+ takeProfitPrice: Math.round(tpPrice * 100000) / 100000,
239
+ riskRewardRatio,
240
+ stopDistancePrice: Math.round(stopDistance * 100000) / 100000,
241
+ tpDistancePrice: Math.round(tpDistance * 100000) / 100000,
242
+ direction,
243
+ };
244
+ }
245
+
246
+ // ============================================================================
247
+ // FIBONACCI
248
+ // ============================================================================
249
+
250
+ const RETRACEMENT_RATIOS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0];
251
+ const EXTENSION_RATIOS = [1.272, 1.414, 1.618, 2.0, 2.618, 3.618, 4.236];
252
+
253
+ const RETRACEMENT_LABELS = ['0% (Start)', '23.6%', '38.2%', '50% (Psychological)', '61.8% (Golden Ratio)', '78.6%', '100% (End)'];
254
+ const EXTENSION_LABELS = ['127.2%', '141.4%', '161.8% (Golden Ext)', '200%', '261.8%', '361.8%', '423.6%'];
255
+
256
+ /**
257
+ * Calculate Fibonacci retracement levels.
258
+ * @example
259
+ * fibonacciRetracement(1.1100, 1.1000, 'up')
260
+ */
261
+ export function fibonacciRetracement(high, low, direction = 'up') {
262
+ const range = high - low;
263
+ return RETRACEMENT_RATIOS.map((ratio, i) => ({
264
+ ratio,
265
+ label: RETRACEMENT_LABELS[i],
266
+ price: Math.round((direction === 'up' ? high - range * ratio : low + range * ratio) * 1000000) / 1000000,
267
+ }));
268
+ }
269
+
270
+ /**
271
+ * Calculate Fibonacci extension levels.
272
+ */
273
+ export function fibonacciExtension(high, low, direction = 'up') {
274
+ const range = high - low;
275
+ return EXTENSION_RATIOS.map((ratio, i) => ({
276
+ ratio,
277
+ label: EXTENSION_LABELS[i],
278
+ price: Math.round((direction === 'up' ? high + range * ratio : low - range * ratio) * 1000000) / 1000000,
279
+ }));
280
+ }
281
+
282
+ /**
283
+ * All Fibonacci levels at once.
284
+ */
285
+ export function fibonacciAll(high, low, direction = 'up') {
286
+ return {
287
+ retracement: fibonacciRetracement(high, low, direction),
288
+ extension: fibonacciExtension(high, low, direction),
289
+ range: Math.round((high - low) * 1000000) / 1000000,
290
+ direction,
291
+ high,
292
+ low,
293
+ };
294
+ }
295
+
296
+ // ============================================================================
297
+ // KELLY CRITERION
298
+ // ============================================================================
299
+
300
+ /**
301
+ * Calculate Kelly Criterion optimal position size.
302
+ * @example
303
+ * kellyCriterion(0.60, 200, 100)
304
+ * // → { kellyPercent: 40, adjustedPercent: 20, ... }
305
+ */
306
+ export function kellyCriterion(winRate, avgWin, avgLoss, fraction = 0.5, accountBalance = 0) {
307
+ if (winRate <= 0 || winRate >= 1) throw new Error(`Win rate must be between 0 and 1, got ${winRate}`);
308
+ if (avgLoss <= 0) throw new Error(`Avg loss must be positive`);
309
+ if (avgWin <= 0) throw new Error(`Avg win must be positive`);
310
+
311
+ const lossRate = 1 - winRate;
312
+ const winLossRatio = avgWin / avgLoss;
313
+ const fullKelly = Math.max(0, winRate - lossRate / winLossRatio);
314
+ const adjusted = fullKelly * fraction;
315
+ const riskAmount = accountBalance > 0 ? accountBalance * adjusted : 0;
316
+
317
+ return {
318
+ kellyPercent: Math.round(fullKelly * 10000) / 100,
319
+ adjustedPercent: Math.round(adjusted * 10000) / 100,
320
+ riskAmount: Math.round(riskAmount * 100) / 100,
321
+ winLossRatio: Math.round(winLossRatio * 100) / 100,
322
+ fractionUsed: fraction,
323
+ hasEdge: fullKelly > 0,
324
+ };
325
+ }
326
+
327
+ // ============================================================================
328
+ // DRAWDOWN
329
+ // ============================================================================
330
+
331
+ /**
332
+ * Calculate maximum drawdown from equity curve.
333
+ * @example
334
+ * drawdown([10000, 11000, 10500, 12000, 10800])
335
+ * // → { maxDrawdownPercent: -10, recoveryPercent: 11.11, ... }
336
+ */
337
+ export function drawdown(equityCurve) {
338
+ if (equityCurve.length < 2) throw new Error('Need at least 2 equity values');
339
+
340
+ let peak = equityCurve[0];
341
+ let maxDD = 0, ddPeak = peak, ddTrough = peak;
342
+
343
+ for (const val of equityCurve) {
344
+ if (val > peak) peak = val;
345
+ const dd = ((val - peak) / peak) * 100;
346
+ if (dd < maxDD) { maxDD = dd; ddPeak = peak; ddTrough = val; }
347
+ }
348
+
349
+ const recovery = ddTrough > 0 ? ((ddPeak / ddTrough) - 1) * 100 : 0;
350
+ const currentDD = ((equityCurve[equityCurve.length - 1] - peak) / peak) * 100;
351
+
352
+ return {
353
+ maxDrawdownPercent: Math.round(maxDD * 100) / 100,
354
+ maxDrawdownAmount: Math.round((ddPeak - ddTrough) * 100) / 100,
355
+ peak: ddPeak,
356
+ trough: ddTrough,
357
+ recoveryPercent: Math.round(recovery * 100) / 100,
358
+ currentDrawdownPercent: Math.round(currentDD * 100) / 100,
359
+ currentEquity: equityCurve[equityCurve.length - 1],
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Gain percentage needed to recover from a drawdown.
365
+ * @example
366
+ * recoveryNeeded(50) // → 100
367
+ * recoveryNeeded(20) // → 25
368
+ */
369
+ export function recoveryNeeded(drawdownPercent) {
370
+ if (drawdownPercent >= 100) return Infinity;
371
+ if (drawdownPercent <= 0) return 0;
372
+ return Math.round(((1 / (1 - drawdownPercent / 100) - 1) * 100) * 100) / 100;
373
+ }
374
+
375
+ // ============================================================================
376
+ // MARGIN
377
+ // ============================================================================
378
+
379
+ /**
380
+ * Calculate required margin for a position.
381
+ * @example
382
+ * marginRequired('EURUSD', 1.0, 1.0850, 100)
383
+ * // → { margin: 1085, notionalValue: 108500, ... }
384
+ */
385
+ export function marginRequired(symbol = 'EURUSD', lots = 1.0, entryPrice = null, leverage = 100) {
386
+ const inst = getInstrument(symbol);
387
+ const price = entryPrice ?? 1.0;
388
+ const notionalValue = lots * inst.contractSize * price;
389
+ const margin = notionalValue / leverage;
390
+ return {
391
+ margin: Math.round(margin * 100) / 100,
392
+ notionalValue: Math.round(notionalValue * 100) / 100,
393
+ leverage,
394
+ lots,
395
+ entryPrice: price,
396
+ symbol,
397
+ };
398
+ }
399
+
400
+ // ============================================================================
401
+ // RISK/REWARD
402
+ // ============================================================================
403
+
404
+ /**
405
+ * Calculate risk/reward ratio.
406
+ * @example
407
+ * riskReward(1.1050, 1.1000, 1.1150, 'long')
408
+ * // → { rrr: 2.0, breakevenWinrate: 33.33, ... }
409
+ */
410
+ export function riskReward(entry, stopLoss, takeProfit, direction = 'long') {
411
+ const risk = direction === 'long' ? Math.abs(entry - stopLoss) : Math.abs(stopLoss - entry);
412
+ const reward = direction === 'long' ? Math.abs(takeProfit - entry) : Math.abs(entry - takeProfit);
413
+ if (risk <= 0) throw new Error('Risk must be positive');
414
+ const rrr = reward / risk;
415
+ const breakeven = (1 / (1 + rrr)) * 100;
416
+ return {
417
+ rrr: Math.round(rrr * 100) / 100,
418
+ riskPrice: Math.round(risk * 1000000) / 1000000,
419
+ rewardPrice: Math.round(reward * 1000000) / 1000000,
420
+ riskPips: Math.round(risk * 10000 * 10) / 10,
421
+ rewardPips: Math.round(reward * 10000 * 10) / 10,
422
+ breakevenWinrate: Math.round(breakeven * 100) / 100,
423
+ isFavorable: rrr >= 1.5,
424
+ direction,
425
+ entry,
426
+ stopLoss,
427
+ takeProfit,
428
+ };
429
+ }
430
+
431
+ /**
432
+ * Calculate profit factor.
433
+ * @example
434
+ * profitFactor(5000, 2500) // → { profitFactor: 2.0, assessment: 'excellent' }
435
+ */
436
+ export function profitFactor(totalWins, totalLosses) {
437
+ if (totalLosses <= 0) return { profitFactor: totalWins > 0 ? Infinity : 0, netProfit: totalWins, assessment: 'no losses' };
438
+ const pf = totalWins / totalLosses;
439
+ const assessment = pf < 1 ? 'unprofitable' : pf < 1.5 ? 'marginal' : pf < 2 ? 'good' : 'excellent';
440
+ return {
441
+ profitFactor: Math.round(pf * 100) / 100,
442
+ netProfit: Math.round((totalWins - totalLosses) * 100) / 100,
443
+ totalWins,
444
+ totalLosses,
445
+ assessment,
446
+ };
447
+ }
448
+
449
+ // ============================================================================
450
+ // COMPOUND INTEREST
451
+ // ============================================================================
452
+
453
+ /**
454
+ * Calculate compound interest growth.
455
+ * @example
456
+ * compoundInterest(10000, 12, 5, 12)
457
+ */
458
+ export function compoundInterest(principal, annualRate, years, compoundingPerYear = 12) {
459
+ const rate = annualRate / 100;
460
+ const rPerPeriod = rate / compoundingPerYear;
461
+ const nPeriods = compoundingPerYear * years;
462
+ const finalValue = principal * Math.pow(1 + rPerPeriod, nPeriods);
463
+ const totalContributions = principal;
464
+ return {
465
+ finalValue: Math.round(finalValue * 100) / 100,
466
+ totalContributions,
467
+ totalInterest: Math.round((finalValue - totalContributions) * 100) / 100,
468
+ principal,
469
+ annualRate,
470
+ years,
471
+ };
472
+ }
@@ -0,0 +1,106 @@
1
+ import {
2
+ positionSize, positionSizeATR, pipValue, pipsFromPrice, priceFromPips,
3
+ trueRange, atrStopLoss, atrTakeProfit,
4
+ fibonacciRetracement, fibonacciExtension, fibonacciAll,
5
+ kellyCriterion, drawdown, recoveryNeeded,
6
+ marginRequired, riskReward, profitFactor, compoundInterest,
7
+ getInstrument
8
+ } from '../src/index.js';
9
+
10
+ let passed = 0;
11
+ let failed = 0;
12
+
13
+ function assert(condition, msg) {
14
+ if (condition) { passed++; }
15
+ else { failed++; console.error(`FAIL: ${msg}`); }
16
+ }
17
+
18
+ function assertEqual(actual, expected, msg) {
19
+ const ok = Math.abs(actual - expected) < 0.001;
20
+ if (ok) { passed++; }
21
+ else { failed++; console.error(`FAIL: ${msg} — expected ${expected}, got ${actual}`); }
22
+ }
23
+
24
+ // ── Position Size ──────────────────────────────────────────────
25
+ const ps1 = positionSize(10000, 1.0, 20, 'EURUSD');
26
+ assertEqual(ps1.lots, 0.5, 'EURUSD position size');
27
+ assertEqual(ps1.riskAmount, 100, 'EURUSD risk amount');
28
+
29
+ const ps2 = positionSize(5000, 2.0, 50, 'XAUUSD');
30
+ assertEqual(ps2.lots, 0.2, 'XAUUSD position size');
31
+
32
+ // ── Pip Value ──────────────────────────────────────────────────
33
+ const pv1 = pipValue('EURUSD', 1.0);
34
+ assertEqual(pv1.onePip, 10, 'EURUSD pip value');
35
+
36
+ const pv2 = pipValue('XAUUSD', 0.1);
37
+ assertEqual(pv2.onePip, 1, 'XAUUSD pip value 0.1 lot');
38
+
39
+ // ── Pips Conversion ───────────────────────────────────────────
40
+ assertEqual(pipsFromPrice('EURUSD', 0.0020), 20, 'EURUSD pips from price');
41
+ assertEqual(pipsFromPrice('XAUUSD', 5.0), 50, 'XAUUSD pips from price');
42
+ assertEqual(priceFromPips('EURUSD', 20), 0.002, 'EURUSD price from pips');
43
+
44
+ // ── ATR ────────────────────────────────────────────────────────
45
+ const tr = trueRange(1.1050, 1.1000, 1.1020);
46
+ assert(Math.abs(tr - 0.005) < 0.0001, 'True Range');
47
+
48
+ const sl = atrStopLoss(1.1050, 0.0050, 'long', 1.5);
49
+ assertEqual(sl.stopLossPrice, 1.0975, 'ATR stop loss long');
50
+
51
+ const sl2 = atrStopLoss(1.1050, 0.0050, 'short', 2.0);
52
+ assertEqual(sl2.stopLossPrice, 1.115, 'ATR stop loss short');
53
+
54
+ // ── Fibonacci ──────────────────────────────────────────────────
55
+ const fib = fibonacciRetracement(1.1100, 1.1000, 'up');
56
+ assert(fib.length === 7, 'Fibonacci retracement count');
57
+
58
+ const fibExt = fibonacciExtension(1.1100, 1.1000, 'up');
59
+ assert(fibExt.length === 7, 'Fibonacci extension count');
60
+
61
+ const fibAll = fibonacciAll(1.1100, 1.1000, 'up');
62
+ assert(fibAll.retracement.length === 7, 'Fibonacci all retracement');
63
+ assert(fibAll.extension.length === 7, 'Fibonacci all extension');
64
+
65
+ // ── Kelly ──────────────────────────────────────────────────────
66
+ const k = kellyCriterion(0.60, 200, 100);
67
+ assertEqual(k.kellyPercent, 40, 'Kelly full');
68
+ assertEqual(k.adjustedPercent, 20, 'Kelly half');
69
+
70
+ // ── Drawdown ───────────────────────────────────────────────────
71
+ const dd = drawdown([10000, 11000, 10500, 12000, 10800]);
72
+ assertEqual(dd.maxDrawdownPercent, -10, 'Max drawdown');
73
+
74
+ assertEqual(recoveryNeeded(50), 100, 'Recovery 50%');
75
+ assertEqual(recoveryNeeded(20), 25, 'Recovery 20%');
76
+
77
+ // ── Margin ─────────────────────────────────────────────────────
78
+ const m = marginRequired('EURUSD', 1.0, 1.0850, 100);
79
+ assertEqual(m.margin, 1085, 'EURUSD margin');
80
+
81
+ // ── Risk/Reward ────────────────────────────────────────────────
82
+ const rr = riskReward(1.1050, 1.1000, 1.1150, 'long');
83
+ assertEqual(rr.rrr, 2.0, 'Risk reward ratio');
84
+
85
+ // ── Profit Factor ──────────────────────────────────────────────
86
+ const pf = profitFactor(5000, 2500);
87
+ assertEqual(pf.profitFactor, 2.0, 'Profit factor');
88
+
89
+ // ── Compound ───────────────────────────────────────────────────
90
+ const ci = compoundInterest(10000, 12, 5, 12);
91
+ assert(ci.finalValue > 10000, 'Compound interest grows');
92
+
93
+ // ── Instrument Lookup ──────────────────────────────────────────
94
+ const gold = getInstrument('GOLD');
95
+ assert(gold.symbol === 'XAUUSD', 'GOLD alias resolves');
96
+
97
+ const btc = getInstrument('BTC');
98
+ assert(btc.symbol === 'BTCUSD', 'BTC alias resolves');
99
+
100
+ // ── Error Handling ─────────────────────────────────────────────
101
+ try { positionSize(-100, 1.0, 20); assert(false, 'Should throw'); } catch (e) { assert(true, 'Negative balance error'); }
102
+ try { getInstrument('FAKE'); assert(false, 'Should throw'); } catch (e) { assert(true, 'Unknown instrument error'); }
103
+
104
+ // ── Summary ────────────────────────────────────────────────────
105
+ console.log(`\n${passed} passed, ${failed} failed`);
106
+ if (failed > 0) process.exit(1);