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.
- package/examples/basic_usage.js +126 -0
- package/package.json +25 -0
- package/src/index.js +472 -0
- package/test/index.test.js +106 -0
|
@@ -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);
|