@barchart/portfolio-api-common 1.0.263 → 1.0.267
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/lib/data/TransactionValidator.js +27 -1
- package/lib/processing/PositionContainer.js +24 -9
- package/lib/processing/PositionGroup.js +61 -37
- package/package.json +1 -1
- package/test/SpecRunner.js +501 -81
- package/test/specs/data/PositionSummaryFrameSpec.js +1 -1
- package/test/specs/data/TransactionValidatorSpec.js +51 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const assert = require('@barchart/common-js/lang/assert')
|
|
1
|
+
const assert = require('@barchart/common-js/lang/assert'),
|
|
2
|
+
array = require('@barchart/common-js/lang/array')
|
|
2
3
|
|
|
3
4
|
const InstrumentType = require('./InstrumentType'),
|
|
4
5
|
PositionDirection = require('./PositionDirection'),
|
|
@@ -17,6 +18,31 @@ module.exports = (() => {
|
|
|
17
18
|
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Given a set of transaction, ensures that sequence numbers and dates
|
|
23
|
+
* are properly ordered.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
* @static
|
|
27
|
+
* @param {Array.<Object>} transactions
|
|
28
|
+
* @param {Boolean=} partial - If true, sequence validation starts with the array's first transaction.
|
|
29
|
+
* @return {boolean}
|
|
30
|
+
*/
|
|
31
|
+
static validateOrder(transactions, partial) {
|
|
32
|
+
assert.argumentIsArray(transactions, 'transactions');
|
|
33
|
+
assert.argumentIsOptional(partial, 'partial', Boolean);
|
|
34
|
+
|
|
35
|
+
let startSequence;
|
|
36
|
+
|
|
37
|
+
if (partial && transactions.length !== 0) {
|
|
38
|
+
startSequence = array.first(transactions).sequence;
|
|
39
|
+
} else {
|
|
40
|
+
startSequence = 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return transactions.every((t, i) => t.sequence === (i + startSequence) && (i === 0 || !t.date.getIsBefore(transactions[i - 1].date)));
|
|
44
|
+
}
|
|
45
|
+
|
|
20
46
|
/**
|
|
21
47
|
* Given an instrument type, returns all valid transaction types.
|
|
22
48
|
*
|
|
@@ -146,6 +146,8 @@ module.exports = (() => {
|
|
|
146
146
|
}, { });
|
|
147
147
|
|
|
148
148
|
Object.keys(this._portfolios).forEach(key => updateEmptyPortfolioGroups.call(this, this._portfolios[key]));
|
|
149
|
+
|
|
150
|
+
recalculatePercentages.call(this);
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
/**
|
|
@@ -324,6 +326,8 @@ module.exports = (() => {
|
|
|
324
326
|
this._positionSymbolAddedEvent.fire(addedBarchartSymbol);
|
|
325
327
|
}
|
|
326
328
|
});
|
|
329
|
+
|
|
330
|
+
recalculatePercentages.call(this);
|
|
327
331
|
}
|
|
328
332
|
|
|
329
333
|
/**
|
|
@@ -337,6 +341,8 @@ module.exports = (() => {
|
|
|
337
341
|
assert.argumentIsRequired(position.position, 'position.position', String);
|
|
338
342
|
|
|
339
343
|
removePositionItem.call(this, this._items.find((item) => item.position.position === position.position));
|
|
344
|
+
|
|
345
|
+
recalculatePercentages.call(this);
|
|
340
346
|
}
|
|
341
347
|
|
|
342
348
|
/**
|
|
@@ -384,6 +390,8 @@ module.exports = (() => {
|
|
|
384
390
|
if (this._symbols.hasOwnProperty(symbol)) {
|
|
385
391
|
this._symbols[symbol].forEach(item => item.setQuote(quote));
|
|
386
392
|
}
|
|
393
|
+
|
|
394
|
+
recalculatePercentages.call(this);
|
|
387
395
|
}
|
|
388
396
|
|
|
389
397
|
/**
|
|
@@ -427,7 +435,9 @@ module.exports = (() => {
|
|
|
427
435
|
this._forexQuotes[index] = rate;
|
|
428
436
|
}
|
|
429
437
|
|
|
430
|
-
Object.keys(this._trees).forEach(key => this._trees[key].walk(group => group.
|
|
438
|
+
Object.keys(this._trees).forEach(key => this._trees[key].walk(group => group.setForexRates(this._forexQuotes), true, false));
|
|
439
|
+
|
|
440
|
+
recalculatePercentages.call(this);
|
|
431
441
|
}
|
|
432
442
|
|
|
433
443
|
/**
|
|
@@ -739,12 +749,6 @@ module.exports = (() => {
|
|
|
739
749
|
}
|
|
740
750
|
}
|
|
741
751
|
}));
|
|
742
|
-
|
|
743
|
-
addGroupBinding.call(this, group, group.registerMarketPercentChangeHandler(() => {
|
|
744
|
-
if (!groupTree.getIsRoot()) {
|
|
745
|
-
groupTree.getParent().walk((childGroup) => childGroup.refreshMarketPercent());
|
|
746
|
-
}
|
|
747
|
-
}));
|
|
748
752
|
}
|
|
749
753
|
|
|
750
754
|
function createGroups(parentTree, items, treeDefinition, levelDefinitions, overrideRequiredGroups) {
|
|
@@ -752,6 +756,8 @@ module.exports = (() => {
|
|
|
752
756
|
return;
|
|
753
757
|
}
|
|
754
758
|
|
|
759
|
+
const rates = this._forexQuotes;
|
|
760
|
+
|
|
755
761
|
const levelDefinition = levelDefinitions[0];
|
|
756
762
|
|
|
757
763
|
const populatedObjects = array.groupBy(items, levelDefinition.keySelector);
|
|
@@ -759,7 +765,7 @@ module.exports = (() => {
|
|
|
759
765
|
const items = populatedObjects[key];
|
|
760
766
|
const first = items[0];
|
|
761
767
|
|
|
762
|
-
list.push(new PositionGroup(
|
|
768
|
+
list.push(new PositionGroup(levelDefinition, items, rates, levelDefinition.currencySelector(first), key, levelDefinition.descriptionSelector(first), levelDefinition.aggregateCash));
|
|
763
769
|
|
|
764
770
|
return list;
|
|
765
771
|
}, [ ]);
|
|
@@ -772,7 +778,7 @@ module.exports = (() => {
|
|
|
772
778
|
});
|
|
773
779
|
|
|
774
780
|
const empty = missingGroups.map((group) => {
|
|
775
|
-
return new PositionGroup(
|
|
781
|
+
return new PositionGroup(levelDefinition, [ ], rates, group.currency, group.key, group.description);
|
|
776
782
|
});
|
|
777
783
|
|
|
778
784
|
const compositeGroups = populatedGroups.concat(empty);
|
|
@@ -812,6 +818,9 @@ module.exports = (() => {
|
|
|
812
818
|
|
|
813
819
|
this._nodes[group.id] = childTree;
|
|
814
820
|
|
|
821
|
+
group.setParentGroup(this.getParentGroup(group));
|
|
822
|
+
group.setPortfolioGroup(this.getParentGroupForPortfolio(group));
|
|
823
|
+
|
|
815
824
|
initializeGroupObservers.call(this, childTree, treeDefinition);
|
|
816
825
|
|
|
817
826
|
createGroups.call(this, childTree, group.items, treeDefinition, array.dropLeft(levelDefinitions));
|
|
@@ -952,5 +961,11 @@ module.exports = (() => {
|
|
|
952
961
|
groupNodeToSever.walk(group => delete this._nodes[group.id], false, true);
|
|
953
962
|
}
|
|
954
963
|
|
|
964
|
+
function recalculatePercentages() {
|
|
965
|
+
Object.keys(this._trees).forEach((key) => {
|
|
966
|
+
this._trees[key].walk(group => group.refreshMarketPercent(), false, false);
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
955
970
|
return PositionContainer;
|
|
956
971
|
})();
|
|
@@ -33,13 +33,17 @@ module.exports = (() => {
|
|
|
33
33
|
* @param {Boolean=} aggregateCash
|
|
34
34
|
*/
|
|
35
35
|
class PositionGroup {
|
|
36
|
-
constructor(
|
|
36
|
+
constructor(definition, items, rates, currency, key, description, aggregateCash) {
|
|
37
37
|
this._id = counter++;
|
|
38
38
|
|
|
39
39
|
this._definition = definition;
|
|
40
|
-
this._container = container;
|
|
41
40
|
|
|
42
41
|
this._items = items;
|
|
42
|
+
this._rates = rates;
|
|
43
|
+
|
|
44
|
+
this._parentGroup = null;
|
|
45
|
+
this._portfolioGroup = null;
|
|
46
|
+
|
|
43
47
|
this._currency = currency || Currency.CAD;
|
|
44
48
|
this._bypassCurrencyTranslation = false;
|
|
45
49
|
|
|
@@ -53,7 +57,6 @@ module.exports = (() => {
|
|
|
53
57
|
this._suspended = false;
|
|
54
58
|
this._showClosedPositions = false;
|
|
55
59
|
|
|
56
|
-
this._marketPercentChangeEvent = new Event(this);
|
|
57
60
|
this._groupExcludedChangeEvent = new Event(this);
|
|
58
61
|
this._showClosedPositionsChangeEvent = new Event(this);
|
|
59
62
|
|
|
@@ -266,6 +269,40 @@ module.exports = (() => {
|
|
|
266
269
|
return this._excluded;
|
|
267
270
|
}
|
|
268
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Sets the immediate parent group (allowing for calculation of relative
|
|
274
|
+
* percentages).
|
|
275
|
+
*
|
|
276
|
+
* @public
|
|
277
|
+
* @param {PortfolioGroup} group
|
|
278
|
+
*/
|
|
279
|
+
setParentGroup(group) {
|
|
280
|
+
assert.argumentIsOptional(group, 'group', PositionGroup, 'PositionGroup');
|
|
281
|
+
|
|
282
|
+
if (this._parentGroup !== null) {
|
|
283
|
+
throw new Error('The parent group has already been set.');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
this._parentGroup = group;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Sets the nearest parent group for a portfolio (allowing for calculation
|
|
291
|
+
* of relative percentages).
|
|
292
|
+
*
|
|
293
|
+
* @public
|
|
294
|
+
* @param {PortfolioGroup} group
|
|
295
|
+
*/
|
|
296
|
+
setPortfolioGroup(group) {
|
|
297
|
+
assert.argumentIsOptional(group, 'group', PositionGroup, 'PositionGroup');
|
|
298
|
+
|
|
299
|
+
if (this._portfolioGroup !== null) {
|
|
300
|
+
throw new Error('The portfolio group has already been set.');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this._portfolioGroup = group;
|
|
304
|
+
}
|
|
305
|
+
|
|
269
306
|
/**
|
|
270
307
|
* Adds a new {@link PositionItem} to the group.
|
|
271
308
|
*
|
|
@@ -311,9 +348,11 @@ module.exports = (() => {
|
|
|
311
348
|
* Causes aggregated data to be recalculated using a new exchange rate.
|
|
312
349
|
*
|
|
313
350
|
* @public
|
|
314
|
-
* @param {Rate} rate
|
|
351
|
+
* @param {Array.<Rate>} rate
|
|
315
352
|
*/
|
|
316
|
-
|
|
353
|
+
setForexRates(rates) {
|
|
354
|
+
this._rates = rates;
|
|
355
|
+
|
|
317
356
|
if (!this._bypassCurrencyTranslation) {
|
|
318
357
|
this.refresh();
|
|
319
358
|
}
|
|
@@ -401,10 +440,8 @@ module.exports = (() => {
|
|
|
401
440
|
return;
|
|
402
441
|
}
|
|
403
442
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
calculateStaticData(this, rates);
|
|
407
|
-
calculatePriceData(this, rates, null, true);
|
|
443
|
+
calculateStaticData(this, this._rates);
|
|
444
|
+
calculatePriceData(this, this._rates, null, true);
|
|
408
445
|
}
|
|
409
446
|
|
|
410
447
|
/**
|
|
@@ -414,7 +451,7 @@ module.exports = (() => {
|
|
|
414
451
|
* @public
|
|
415
452
|
*/
|
|
416
453
|
refreshMarketPercent() {
|
|
417
|
-
calculateMarketPercent(this, this.
|
|
454
|
+
calculateMarketPercent(this, this._rates, this._parentGroup, this._portfolioGroup);
|
|
418
455
|
}
|
|
419
456
|
|
|
420
457
|
/**
|
|
@@ -427,17 +464,6 @@ module.exports = (() => {
|
|
|
427
464
|
return this._items.length === 0;
|
|
428
465
|
}
|
|
429
466
|
|
|
430
|
-
/**
|
|
431
|
-
* Adds an observer for change in the market percentage of the group.
|
|
432
|
-
*
|
|
433
|
-
* @public
|
|
434
|
-
* @param {Function} handler
|
|
435
|
-
* @return {Disposable}
|
|
436
|
-
*/
|
|
437
|
-
registerMarketPercentChangeHandler(handler) {
|
|
438
|
-
return this._marketPercentChangeEvent.register(handler);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
467
|
/**
|
|
442
468
|
* Adds an observer for changes to the exclusion of the group
|
|
443
469
|
* from higher level aggregations.
|
|
@@ -492,10 +518,10 @@ module.exports = (() => {
|
|
|
492
518
|
this._dataFormat.currentPrice = null;
|
|
493
519
|
}
|
|
494
520
|
|
|
495
|
-
calculatePriceData(this, this.
|
|
521
|
+
calculatePriceData(this, this._rates, sender, false);
|
|
496
522
|
});
|
|
497
523
|
|
|
498
|
-
let fundamentalBinding = item.registerFundamentalDataChangeHandler((data
|
|
524
|
+
let fundamentalBinding = item.registerFundamentalDataChangeHandler((data) => {
|
|
499
525
|
if (this._single) {
|
|
500
526
|
this._dataFormat.fundamental = data;
|
|
501
527
|
} else {
|
|
@@ -546,13 +572,13 @@ module.exports = (() => {
|
|
|
546
572
|
let newsBinding = Disposable.getEmpty();
|
|
547
573
|
|
|
548
574
|
if (this._single) {
|
|
549
|
-
newsBinding = item.registerNewsExistsChangeHandler((exists
|
|
575
|
+
newsBinding = item.registerNewsExistsChangeHandler((exists) => {
|
|
550
576
|
this._dataActual.newsExists = exists;
|
|
551
577
|
this._dataFormat.newsExists = exists;
|
|
552
578
|
});
|
|
553
579
|
}
|
|
554
580
|
|
|
555
|
-
this._disposeStack.push(item.registerPortfolioChangeHandler((portfolio
|
|
581
|
+
this._disposeStack.push(item.registerPortfolioChangeHandler((portfolio) => {
|
|
556
582
|
const descriptionSelector = this._definition.descriptionSelector;
|
|
557
583
|
|
|
558
584
|
this._description = descriptionSelector(this._items[0]);
|
|
@@ -801,12 +827,11 @@ module.exports = (() => {
|
|
|
801
827
|
|
|
802
828
|
format.total = formatCurrency(actual.total, currency);
|
|
803
829
|
format.totalNegative = actual.total.getIsNegative();
|
|
804
|
-
|
|
805
|
-
calculateMarketPercent(group, rates, false);
|
|
830
|
+
|
|
806
831
|
calculateUnrealizedPercent(group);
|
|
807
832
|
}
|
|
808
833
|
|
|
809
|
-
function calculateMarketPercent(group, rates,
|
|
834
|
+
function calculateMarketPercent(group, rates, parentGroup, portfolioGroup) {
|
|
810
835
|
if (group.suspended) {
|
|
811
836
|
return;
|
|
812
837
|
}
|
|
@@ -815,12 +840,10 @@ module.exports = (() => {
|
|
|
815
840
|
const format = group._dataFormat;
|
|
816
841
|
const excluded = group._excluded;
|
|
817
842
|
|
|
818
|
-
const portfolioParent = group._container.getParentGroupForPortfolio(group);
|
|
819
|
-
|
|
820
843
|
const calculatePercent = (parent) => {
|
|
821
844
|
let marketPercent;
|
|
822
845
|
|
|
823
|
-
if (parent
|
|
846
|
+
if (parent && !excluded) {
|
|
824
847
|
const parentData = parent._dataActual;
|
|
825
848
|
|
|
826
849
|
if (parentData.marketAbsolute !== null && !parentData.marketAbsolute.getIsZero()) {
|
|
@@ -843,14 +866,15 @@ module.exports = (() => {
|
|
|
843
866
|
return marketPercent;
|
|
844
867
|
};
|
|
845
868
|
|
|
846
|
-
actual.marketPercent = calculatePercent(
|
|
869
|
+
actual.marketPercent = calculatePercent(parentGroup);
|
|
847
870
|
format.marketPercent = formatPercent(actual.marketPercent, 2);
|
|
848
871
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
872
|
+
if (parentGroup === portfolioGroup) {
|
|
873
|
+
actual.marketPercentPortfolio = actual.marketPercent;
|
|
874
|
+
format.marketPercentPortfolio = format.marketPercent;
|
|
875
|
+
} else {
|
|
876
|
+
actual.marketPercentPortfolio = calculatePercent(portfolioGroup);
|
|
877
|
+
format.marketPercentPortfolio = formatPercent(actual.marketPercentPortfolio, 2);
|
|
854
878
|
}
|
|
855
879
|
}
|
|
856
880
|
|