hedgequantx 2.9.238 → 2.9.240
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/lib/m/s1-models.js +41 -51
- package/dist/lib/m/ultra-scalping.js +95 -82
- package/package.json +1 -1
package/dist/lib/m/s1-models.js
CHANGED
|
@@ -24,28 +24,30 @@
|
|
|
24
24
|
'use strict';
|
|
25
25
|
|
|
26
26
|
// =============================================================================
|
|
27
|
-
//
|
|
27
|
+
// REGIME PARAMETERS - EXACT MATCH TO PYTHON BACKTEST
|
|
28
|
+
// Backtest Result: $2,012,373.75 | 146,685 trades | 71.1% WR
|
|
29
|
+
// Z-Score Entry: >2.5 | Exit: <0.5 | Stop: 8 ticks | Target: 16 ticks
|
|
28
30
|
// =============================================================================
|
|
29
31
|
|
|
30
32
|
const REGIME_LOW = Object.freeze({
|
|
31
|
-
stopMultiplier: 0
|
|
32
|
-
targetMultiplier: 0
|
|
33
|
-
zscoreThreshold:
|
|
34
|
-
confidenceBonus: 0.
|
|
33
|
+
stopMultiplier: 1.0,
|
|
34
|
+
targetMultiplier: 1.0,
|
|
35
|
+
zscoreThreshold: 2.5, // BACKTEST EXACT: 2.5
|
|
36
|
+
confidenceBonus: 0.0
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
const REGIME_NORMAL = Object.freeze({
|
|
38
40
|
stopMultiplier: 1.0,
|
|
39
41
|
targetMultiplier: 1.0,
|
|
40
|
-
zscoreThreshold:
|
|
42
|
+
zscoreThreshold: 2.5, // BACKTEST EXACT: 2.5
|
|
41
43
|
confidenceBonus: 0.0
|
|
42
44
|
});
|
|
43
45
|
|
|
44
46
|
const REGIME_HIGH = Object.freeze({
|
|
45
|
-
stopMultiplier: 1.
|
|
46
|
-
targetMultiplier: 1.
|
|
47
|
-
zscoreThreshold: 2.
|
|
48
|
-
confidenceBonus:
|
|
47
|
+
stopMultiplier: 1.0,
|
|
48
|
+
targetMultiplier: 1.0,
|
|
49
|
+
zscoreThreshold: 2.5, // BACKTEST EXACT: 2.5
|
|
50
|
+
confidenceBonus: 0.0
|
|
49
51
|
});
|
|
50
52
|
|
|
51
53
|
// Pre-allocated result object for Kalman filter (reused)
|
|
@@ -65,61 +67,49 @@ const _regimeResult = {
|
|
|
65
67
|
// =============================================================================
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
|
-
* Compute Z-Score
|
|
69
|
-
*
|
|
70
|
+
* Compute Z-Score - EXACT MATCH TO PYTHON BACKTEST
|
|
71
|
+
*
|
|
72
|
+
* PYTHON CODE (from ultra_scalper_v2_protected.py):
|
|
73
|
+
* ```python
|
|
74
|
+
* for i in range(lookback, n):
|
|
75
|
+
* window = prices[i-lookback:i] # EXCLUDES current price!
|
|
76
|
+
* mean = np.mean(window)
|
|
77
|
+
* std = np.std(window)
|
|
78
|
+
* if std > 0:
|
|
79
|
+
* zscore[i] = (prices[i] - mean) / std
|
|
80
|
+
* ```
|
|
70
81
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* @param {number}
|
|
82
|
+
* CRITICAL: Mean/std are calculated from PREVIOUS 100 prices, NOT including current!
|
|
83
|
+
*
|
|
84
|
+
* @param {number[]} prices - Price buffer
|
|
85
|
+
* @param {number} lookback - Lookback window (default 100 to match Python)
|
|
74
86
|
* @returns {number} Z-Score value
|
|
75
87
|
*/
|
|
76
|
-
function computeZScore(prices,
|
|
88
|
+
function computeZScore(prices, lookback = 100) {
|
|
77
89
|
const length = prices.length;
|
|
78
|
-
|
|
90
|
+
|
|
91
|
+
// Need at least lookback+1 prices (lookback for window + 1 for current)
|
|
92
|
+
if (length <= lookback) return 0;
|
|
79
93
|
|
|
80
94
|
const currentPrice = prices[length - 1];
|
|
81
95
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
const
|
|
96
|
+
// CRITICAL: Window is [i-lookback : i], which EXCLUDES current price
|
|
97
|
+
// This matches Python: window = prices[i-lookback:i]
|
|
98
|
+
const windowEnd = length - 1; // Exclude current price
|
|
99
|
+
const windowStart = windowEnd - lookback;
|
|
85
100
|
|
|
86
|
-
// Single-pass mean calculation
|
|
101
|
+
// Single-pass mean and std calculation over lookback window
|
|
87
102
|
let sum = 0;
|
|
88
103
|
let sumSq = 0;
|
|
89
|
-
for (let i =
|
|
104
|
+
for (let i = windowStart; i < windowEnd; i++) {
|
|
90
105
|
const p = prices[i];
|
|
91
106
|
sum += p;
|
|
92
|
-
sumSq += p * p;
|
|
107
|
+
sumSq += p * p;
|
|
93
108
|
}
|
|
94
109
|
|
|
95
|
-
const mean = sum /
|
|
96
|
-
const variance = (sumSq /
|
|
97
|
-
|
|
98
|
-
// Blend cumulative and rolling std if enough data (like Python backtest)
|
|
99
|
-
let std;
|
|
100
|
-
if (length >= 100) {
|
|
101
|
-
const cumulativeStd = Math.sqrt(Math.max(0, variance));
|
|
102
|
-
|
|
103
|
-
// Calculate rolling std over last 100 prices (in-place)
|
|
104
|
-
const rollingStart = length - 100;
|
|
105
|
-
let rollingSum = 0;
|
|
106
|
-
for (let i = rollingStart; i < length; i++) {
|
|
107
|
-
rollingSum += prices[i];
|
|
108
|
-
}
|
|
109
|
-
const rollingMean = rollingSum / 100;
|
|
110
|
-
|
|
111
|
-
let rollingVarSum = 0;
|
|
112
|
-
for (let i = rollingStart; i < length; i++) {
|
|
113
|
-
const diff = prices[i] - rollingMean;
|
|
114
|
-
rollingVarSum += diff * diff;
|
|
115
|
-
}
|
|
116
|
-
const rollingStd = Math.sqrt(rollingVarSum / 100);
|
|
117
|
-
|
|
118
|
-
// Blend: 30% cumulative, 70% rolling (matches Python)
|
|
119
|
-
std = cumulativeStd * 0.3 + rollingStd * 0.7;
|
|
120
|
-
} else {
|
|
121
|
-
std = Math.sqrt(Math.max(0, variance));
|
|
122
|
-
}
|
|
110
|
+
const mean = sum / lookback;
|
|
111
|
+
const variance = (sumSq / lookback) - (mean * mean);
|
|
112
|
+
const std = Math.sqrt(Math.max(0, variance));
|
|
123
113
|
|
|
124
114
|
if (std < 0.0001) return 0;
|
|
125
115
|
return (currentPrice - mean) / std;
|
|
@@ -79,21 +79,33 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
79
79
|
this.tickSize = 0.25;
|
|
80
80
|
this.tickValue = 5.0;
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
// ==========================================================================
|
|
83
|
+
// EXACT MATCH TO PYTHON BACKTEST CONFIG
|
|
84
|
+
// Result: $2,012,373.75 | 146,685 trades | 71.1% WR
|
|
85
|
+
// ==========================================================================
|
|
86
|
+
|
|
87
|
+
// Z-Score Parameters (EXACT FROM PYTHON)
|
|
88
|
+
this.zscoreLookback = 100; // PYTHON: zscore_lookback: 100
|
|
89
|
+
this.zscoreEntryThreshold = 2.5; // PYTHON: zscore_entry: 2.5
|
|
90
|
+
this.zscoreExitThreshold = 0.5; // PYTHON: zscore_exit: 0.5
|
|
91
|
+
|
|
92
|
+
// Trade Parameters (EXACT FROM PYTHON)
|
|
93
|
+
this.baseStopTicks = 8; // PYTHON: stop_ticks: 8
|
|
94
|
+
this.baseTargetTicks = 16; // PYTHON: target_ticks: 16
|
|
95
|
+
this.breakevenTicks = 4; // PYTHON: be_ticks: 4
|
|
96
|
+
this.trailActivationTicks = 6; // PYTHON: trail_activation: 6
|
|
97
|
+
this.profitLockPct = 0.5; // PYTHON: trail_pct: 0.5
|
|
98
|
+
|
|
99
|
+
// Risk Management (EXACT FROM PYTHON)
|
|
100
|
+
this.cooldownTicks = 100; // PYTHON: cooldown: 100
|
|
101
|
+
this.maxConsecutiveLosses = 10; // PYTHON: max_consecutive_losses: 10
|
|
102
|
+
|
|
103
|
+
// NOT USED AS FILTERS (Python doesn't use these)
|
|
85
104
|
this.vpinWindow = 50;
|
|
86
105
|
this.vpinToxicThreshold = 0.7;
|
|
106
|
+
this.ofiLookback = 20;
|
|
87
107
|
this.kalmanProcessNoise = 0.01;
|
|
88
108
|
this.kalmanMeasurementNoise = 0.1;
|
|
89
|
-
this.volatilityLookback = 100;
|
|
90
|
-
this.ofiLookback = 20;
|
|
91
|
-
|
|
92
|
-
// === Trade Parameters (BACKTEST VALIDATED) ===
|
|
93
|
-
this.baseStopTicks = 8; // $40
|
|
94
|
-
this.baseTargetTicks = 16; // $80
|
|
95
|
-
this.breakevenTicks = 4; // Move to BE at +4 ticks
|
|
96
|
-
this.profitLockPct = 0.5; // Lock 50% of profit
|
|
97
109
|
|
|
98
110
|
// === State Storage ===
|
|
99
111
|
this.barHistory = new Map();
|
|
@@ -102,22 +114,15 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
102
114
|
this.volumeBuffer = new Map();
|
|
103
115
|
this.tradesBuffer = new Map();
|
|
104
116
|
this.atrHistory = new Map();
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
this.tickBuffer = new Map();
|
|
108
|
-
this.lastBarTime = new Map();
|
|
109
|
-
this.barIntervalMs = 5000; // 5-second bars
|
|
117
|
+
this.tickBuffer = new Map(); // For tick aggregation
|
|
118
|
+
this.lastBarTime = new Map(); // Last bar timestamp per contract
|
|
110
119
|
|
|
111
120
|
// === Performance Tracking ===
|
|
121
|
+
this.tickCount = 0; // Total ticks processed
|
|
122
|
+
this.lastSignalTick = 0; // Tick count at last signal (for cooldown)
|
|
112
123
|
this.recentTrades = [];
|
|
113
124
|
this.winStreak = 0;
|
|
114
125
|
this.lossStreak = 0;
|
|
115
|
-
|
|
116
|
-
// === CRITICAL: Cooldown & Risk Management ===
|
|
117
|
-
this.lastSignalTime = 0;
|
|
118
|
-
this.signalCooldownMs = 30000; // 30 seconds minimum between signals
|
|
119
|
-
this.maxConsecutiveLosses = 3; // Stop trading after 3 consecutive losses
|
|
120
|
-
this.minConfidenceThreshold = 0.65; // Minimum 65% confidence (was 55%)
|
|
121
126
|
this.tradingEnabled = true;
|
|
122
127
|
}
|
|
123
128
|
|
|
@@ -158,40 +163,41 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
158
163
|
}
|
|
159
164
|
|
|
160
165
|
/**
|
|
161
|
-
* Process a tick -
|
|
166
|
+
* Process a tick - TICK-BY-TICK processing (matches Python backtest)
|
|
167
|
+
* Each tick is treated as a single-tick "bar" for model calculations
|
|
162
168
|
*/
|
|
163
169
|
processTick(tick) {
|
|
164
170
|
const contractId = tick.contractId;
|
|
171
|
+
const price = tick.price;
|
|
172
|
+
const volume = tick.volume || 1;
|
|
173
|
+
const timestamp = tick.timestamp || Date.now();
|
|
165
174
|
|
|
166
175
|
if (!this.barHistory.has(contractId)) {
|
|
167
176
|
this.initialize(contractId);
|
|
168
177
|
}
|
|
169
|
-
|
|
170
|
-
// Add tick to buffer
|
|
171
|
-
let ticks = this.tickBuffer.get(contractId);
|
|
172
|
-
ticks.push(tick);
|
|
173
178
|
|
|
174
|
-
// Track total ticks
|
|
175
|
-
this.
|
|
176
|
-
this._lastPrice =
|
|
179
|
+
// Track total ticks (CRITICAL for tick-based cooldown like Python)
|
|
180
|
+
this.tickCount++;
|
|
181
|
+
this._lastPrice = price;
|
|
177
182
|
this._currentContractId = contractId;
|
|
178
183
|
|
|
179
|
-
//
|
|
180
|
-
const
|
|
184
|
+
// Create single-tick bar (matches Python backtest behavior)
|
|
185
|
+
const bar = {
|
|
186
|
+
timestamp,
|
|
187
|
+
open: price,
|
|
188
|
+
high: price,
|
|
189
|
+
low: price,
|
|
190
|
+
close: price,
|
|
191
|
+
volume
|
|
192
|
+
};
|
|
181
193
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
this.
|
|
186
|
-
|
|
187
|
-
if (bar) {
|
|
188
|
-
const signal = this.processBar(contractId, bar);
|
|
189
|
-
if (signal) {
|
|
190
|
-
this.emit('signal', signal);
|
|
191
|
-
return signal;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
+
// Process bar and emit signal if generated
|
|
195
|
+
const signal = this.processBar(contractId, bar);
|
|
196
|
+
if (signal) {
|
|
197
|
+
this.emit('signal', signal);
|
|
198
|
+
return signal;
|
|
194
199
|
}
|
|
200
|
+
|
|
195
201
|
return null;
|
|
196
202
|
}
|
|
197
203
|
|
|
@@ -230,23 +236,21 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
230
236
|
const vpinPct = (vpin * 100).toFixed(0);
|
|
231
237
|
const zRounded = Math.round(zscore * 10) / 10; // Round to 0.1
|
|
232
238
|
|
|
233
|
-
// Check cooldown
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
const cooldownRemaining = Math.max(0, this.signalCooldownMs - timeSinceLastSignal);
|
|
239
|
+
// Check tick-based cooldown (matches Python)
|
|
240
|
+
const ticksSinceLastSignal = this.tickCount - this.lastSignalTick;
|
|
241
|
+
const cooldownRemaining = Math.max(0, this.cooldownTicks - ticksSinceLastSignal);
|
|
237
242
|
|
|
238
243
|
// Trading disabled?
|
|
239
244
|
if (!this.tradingEnabled) {
|
|
240
245
|
state = 'paused';
|
|
241
246
|
message = `[${sym}] ${priceStr} | PAUSED - ${this.lossStreak} losses | Cooldown active`;
|
|
242
247
|
}
|
|
243
|
-
// In cooldown?
|
|
244
|
-
else if (cooldownRemaining > 0 && this.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
message = `[${sym}] ${priceStr} | Cooldown ${secs}s | Z:${zRounded}σ OFI:${ofiPct}%`;
|
|
248
|
+
// In tick-based cooldown?
|
|
249
|
+
else if (cooldownRemaining > 0 && this.lastSignalTick > 0) {
|
|
250
|
+
state = `cooldown-${cooldownRemaining}`;
|
|
251
|
+
message = `[${sym}] ${priceStr} | Cooldown ${cooldownRemaining} ticks | Z:${zRounded}σ OFI:${ofiPct}%`;
|
|
248
252
|
}
|
|
249
|
-
// VPIN toxic?
|
|
253
|
+
// VPIN toxic? (info only, not a hard filter)
|
|
250
254
|
else if (vpin > this.vpinToxicThreshold) {
|
|
251
255
|
state = 'vpin-toxic';
|
|
252
256
|
message = `[${sym}] ${priceStr} | VPIN toxic ${vpinPct}% > 70% | No entry - informed traders active`;
|
|
@@ -393,51 +397,65 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
393
397
|
return null;
|
|
394
398
|
}
|
|
395
399
|
|
|
396
|
-
// CRITICAL:
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
if (
|
|
400
|
+
// CRITICAL: Tick-based cooldown (MATCHES PYTHON: cooldown = 100 ticks)
|
|
401
|
+
// Python: if i - last_trade_idx < cooldown_ticks: continue
|
|
402
|
+
const ticksSinceLastSignal = this.tickCount - this.lastSignalTick;
|
|
403
|
+
if (ticksSinceLastSignal < this.cooldownTicks) {
|
|
400
404
|
// Silent - don't spam logs
|
|
401
405
|
return null;
|
|
402
406
|
}
|
|
403
407
|
|
|
404
|
-
//
|
|
408
|
+
// Extra cooldown after losses (MATCHES PYTHON)
|
|
409
|
+
// Python: extra_cooldown = consecutive_losses * 50
|
|
410
|
+
if (this.lossStreak > 0) {
|
|
411
|
+
const extraCooldown = this.lossStreak * 50;
|
|
412
|
+
if (ticksSinceLastSignal < this.cooldownTicks + extraCooldown) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// CRITICAL: Check consecutive losses (kill switch)
|
|
405
418
|
if (this.lossStreak >= this.maxConsecutiveLosses) {
|
|
406
419
|
this.tradingEnabled = false;
|
|
407
420
|
this.emit('log', { type: 'info', message: `Trading paused: ${this.lossStreak} consecutive losses. Waiting for cooldown...` });
|
|
408
|
-
// Auto re-enable after
|
|
421
|
+
// Auto re-enable after 1 hour (matches Python: 3600_000_000 us = 1 hour)
|
|
409
422
|
setTimeout(() => {
|
|
410
423
|
this.tradingEnabled = true;
|
|
411
424
|
this.lossStreak = 0;
|
|
412
425
|
this.emit('log', { type: 'info', message: 'Trading re-enabled after cooldown' });
|
|
413
|
-
},
|
|
426
|
+
}, 3600000);
|
|
414
427
|
return null;
|
|
415
428
|
}
|
|
416
429
|
|
|
430
|
+
// =========================================================================
|
|
431
|
+
// ENTRY LOGIC - MATCHES PYTHON BACKTEST EXACTLY
|
|
432
|
+
// Python backtest: Z-Score >2.5 = entry, no OFI/VPIN/Kalman filters
|
|
433
|
+
// Result: 146,685 trades, 71.1% WR, $2,012,373.75
|
|
434
|
+
// =========================================================================
|
|
435
|
+
|
|
417
436
|
const absZscore = Math.abs(zscore);
|
|
437
|
+
|
|
438
|
+
// Z-Score threshold check (ONLY filter from Python backtest)
|
|
418
439
|
if (absZscore < volParams.zscoreThreshold) return null;
|
|
419
|
-
if (vpin > this.vpinToxicThreshold) return null;
|
|
420
440
|
|
|
441
|
+
// Determine direction based on Z-Score
|
|
421
442
|
let direction;
|
|
422
|
-
if (zscore < -volParams.zscoreThreshold) direction = 'long';
|
|
423
|
-
else if (zscore > volParams.zscoreThreshold) direction = 'short';
|
|
443
|
+
if (zscore < -volParams.zscoreThreshold) direction = 'long'; // Price below mean = buy
|
|
444
|
+
else if (zscore > volParams.zscoreThreshold) direction = 'short'; // Price above mean = sell
|
|
424
445
|
else return null;
|
|
425
|
-
|
|
426
|
-
// CRITICAL: OFI must confirm direction (stronger filter)
|
|
427
|
-
const ofiConfirms = (direction === 'long' && ofi > 0.15) || (direction === 'short' && ofi < -0.15);
|
|
428
|
-
if (!ofiConfirms) {
|
|
429
|
-
this.emit('log', { type: 'debug', message: `Signal rejected: OFI (${(ofi * 100).toFixed(1)}%) doesn't confirm ${direction}` });
|
|
430
|
-
return null;
|
|
431
|
-
}
|
|
432
446
|
|
|
447
|
+
// OFI/VPIN/Kalman used for confidence scoring only (NOT as hard filters)
|
|
448
|
+
const ofiConfirms = (direction === 'long' && ofi > 0.1) || (direction === 'short' && ofi < -0.1);
|
|
433
449
|
const kalmanDiff = currentPrice - kalmanEstimate;
|
|
434
450
|
const kalmanConfirms = (direction === 'long' && kalmanDiff < 0) || (direction === 'short' && kalmanDiff > 0);
|
|
451
|
+
const vpinOk = vpin < this.vpinToxicThreshold;
|
|
435
452
|
|
|
453
|
+
// Composite confidence score (informational, not blocking)
|
|
436
454
|
const scores = {
|
|
437
455
|
zscore: Math.min(1.0, absZscore / 4.0),
|
|
438
|
-
vpin: 1.0 - vpin,
|
|
456
|
+
vpin: vpinOk ? (1.0 - vpin) : 0.3,
|
|
439
457
|
kyleLambda: kyleLambda > 0.001 ? 0.5 : 0.8,
|
|
440
|
-
kalman: kalmanConfirms ? 0.8 : 0.
|
|
458
|
+
kalman: kalmanConfirms ? 0.8 : 0.5,
|
|
441
459
|
volatility: regime === 'normal' ? 0.8 : regime === 'low' ? 0.7 : 0.6,
|
|
442
460
|
ofi: ofiConfirms ? 0.9 : 0.5,
|
|
443
461
|
composite: 0
|
|
@@ -448,14 +466,8 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
448
466
|
|
|
449
467
|
const confidence = Math.min(1.0, scores.composite + volParams.confidenceBonus);
|
|
450
468
|
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
this.emit('log', { type: 'debug', message: `Signal rejected: confidence ${(confidence * 100).toFixed(1)}% < ${this.minConfidenceThreshold * 100}%` });
|
|
454
|
-
return null;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Update last signal time
|
|
458
|
-
this.lastSignalTime = now;
|
|
469
|
+
// Update last signal tick (tick-based cooldown like Python)
|
|
470
|
+
this.lastSignalTick = this.tickCount;
|
|
459
471
|
|
|
460
472
|
const stopTicks = Math.round(this.baseStopTicks * volParams.stopMultiplier);
|
|
461
473
|
const targetTicks = Math.round(this.baseTargetTicks * volParams.targetMultiplier);
|
|
@@ -622,7 +634,8 @@ class HQXUltraScalpingStrategy extends EventEmitter {
|
|
|
622
634
|
tradingEnabled: this.tradingEnabled,
|
|
623
635
|
lossStreak: this.lossStreak,
|
|
624
636
|
winStreak: this.winStreak,
|
|
625
|
-
cooldownRemaining: Math.max(0, this.
|
|
637
|
+
cooldownRemaining: Math.max(0, this.cooldownTicks - (this.tickCount - this.lastSignalTick)),
|
|
638
|
+
tickCount: this.tickCount,
|
|
626
639
|
};
|
|
627
640
|
}
|
|
628
641
|
|