@barchart/portfolio-api-common 1.0.162 → 1.0.166

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.
@@ -48,17 +48,9 @@ module.exports = (() => {
48
48
  const currentSummaryFrame = PositionSummaryFrame.YTD;
49
49
  const currentSummaryRange = array.last(currentSummaryFrame.getRecentRanges(0));
50
50
 
51
- this._groupBindings = { };
52
-
53
- const addGroupBinding = (group, dispoable) => {
54
- const id = group.id;
55
-
56
- if (!this._groupBindings.hasOwnProperty(id)) {
57
- this._groupBindings[id] = new DisposableStack();
58
- }
51
+ this._definitions = definitions;
59
52
 
60
- this._groupBindings[id].push(dispoable);
61
- };
53
+ this._groupBindings = { };
62
54
 
63
55
  this._portfolios = portfolios.reduce((map, portfolio) => {
64
56
  map[portfolio.portfolio] = portfolio;
@@ -164,136 +156,70 @@ module.exports = (() => {
164
156
  return Rate.fromPair(Decimal.ONE, symbol);
165
157
  });
166
158
 
167
- this._trees = definitions.reduce((map, treeDefinition) => {
159
+ this._trees = this._definitions.reduce((map, treeDefinition) => {
168
160
  const tree = new Tree();
169
161
 
170
- const createGroups = (currentTree, items, levelDefinitions) => {
171
- if (levelDefinitions.length === 0) {
172
- return;
173
- }
174
-
175
- const parent = currentTree.getValue() || null;
176
-
177
- const levelDefinition = levelDefinitions[0];
178
-
179
- const populatedObjects = array.groupBy(items, levelDefinition.keySelector);
180
- const populatedGroups = Object.keys(populatedObjects).reduce((list, key) => {
181
- const items = populatedObjects[key];
182
- const first = items[0];
162
+ createGroups.call(this, tree, tree, this._items, treeDefinition, treeDefinition.definitions);
163
+
164
+ map[treeDefinition.name] = tree;
183
165
 
184
- list.push(new PositionGroup(this, parent, items, levelDefinition.currencySelector(first), key, levelDefinition.descriptionSelector(first), levelDefinition.single && items.length === 1, levelDefinition.aggregateCash));
166
+ return map;
167
+ }, { });
168
+ }
185
169
 
186
- return list;
187
- }, [ ]);
170
+ addPortfolio(portfolio) {
171
+ assert.argumentIsRequired(portfolio, 'portfolio', Object);
172
+ assert.argumentIsRequired(portfolio.portfolio, 'portfolio.portfolio', String);
173
+ assert.argumentIsRequired(portfolio.name, 'portfolio.name', String);
188
174
 
189
- const missingGroups = array.difference(levelDefinition.requiredGroups.map(group => group.key), populatedGroups.map(group => group.key))
190
- .map((key) => {
191
- return levelDefinition.requiredGroups.find(g => g.key === key);
192
- });
175
+ const key = portfolio.portfolio;
193
176
 
194
- const empty = missingGroups.map((group) => {
195
- return new PositionGroup(this, parent, [ ], group.currency, group.key, group.description);
196
- });
177
+ if (!this._portfolios.hasOwnProperty(key)) {
178
+ this._portfolios[key] = portfolio;
197
179
 
198
- const compositeGroups = populatedGroups.concat(empty);
180
+ this._definitions.forEach((treeDefinition) => {
181
+ const tree = this._trees[treeDefinition.name];
182
+ const levelDefinitions = treeDefinition.definitions;
199
183
 
200
- let builder;
184
+ let portfolioRequiredGroup = null;
201
185
 
202
- if (levelDefinition.requiredGroups.length !== 0) {
203
- const ordering = levelDefinition.requiredGroups.reduce((map, group, index) => {
204
- map[group.description] = index;
186
+ let portfolioLevelDefinition = null;
187
+ let portfolioLevelDefinitionIndex = null;
205
188
 
206
- return map;
207
- }, { });
189
+ levelDefinitions.forEach((levelDefinition, i) => {
190
+ if (portfolioRequiredGroup === null) {
191
+ portfolioRequiredGroup = levelDefinition.generateRequiredGroup(portfolio);
208
192
 
209
- const getIndex = (description) => {
210
- if (ordering.hasOwnProperty(description)) {
211
- return ordering[description];
212
- } else {
213
- return Number.MAX_VALUE;
193
+ if (portfolioRequiredGroup !== null) {
194
+ portfolioLevelDefinition = levelDefinition;
195
+ portfolioLevelDefinitionIndex = i;
214
196
  }
215
- };
216
-
217
- builder = ComparatorBuilder.startWith((a, b) => {
218
- return comparators.compareNumbers(getIndex(a.description), getIndex(b.description));
219
- }).thenBy((a, b) => {
220
- return comparators.compareStrings(a.description, b.description);
221
- });
222
- } else {
223
- builder = ComparatorBuilder.startWith((a, b) => {
224
- return comparators.compareStrings(a.description, b.description);
225
- });
226
- }
227
-
228
- compositeGroups.sort(builder.toComparator());
197
+ }
198
+ });
229
199
 
230
- const initializeGroupObservers = (group, groupTree) => {
231
- addGroupBinding(group, group.registerGroupExcludedChangeHandler((excluded, sender) => {
232
- groupTree.climb((parentGroup) => {
233
- if (parentGroup) {
234
- let excludedItems = [];
200
+ if (portfolioRequiredGroup !== null) {
201
+ let parentTrees = [ ];
235
202
 
236
- currentTree.walk((childGroup) => {
237
- if (childGroup.excluded) {
238
- excludedItems = excludedItems.concat(childGroup.items);
239
- }
240
- }, false, false);
203
+ if (portfolioLevelDefinitionIndex === 0) {
204
+ parentTrees.push(tree);
205
+ } else {
206
+ const parentLevelDefinition = levelDefinitions[ portfolioLevelDefinitionIndex - 1 ];
241
207
 
242
- parentGroup.setExcludedItems(array.unique(excludedItems));
243
- }
244
- }, false);
245
-
246
- if (treeDefinition.exclusionDependencies.length > 0) {
247
- const dependantTrees = treeDefinition.exclusionDependencies.reduce((trees, name) => {
248
- if (this._trees.hasOwnProperty(name)) {
249
- trees.push(this._trees[name]);
250
- }
251
-
252
- return trees;
253
- }, [ ]);
254
-
255
- if (dependantTrees.length > 0) {
256
- let excludedItems = [ ];
257
-
258
- tree.walk((childGroup) => {
259
- if (childGroup.excluded) {
260
- excludedItems = excludedItems.concat(childGroup.items);
261
- }
262
- }, false, false);
263
-
264
- dependantTrees.forEach((dependantTrees) => {
265
- dependantTrees.walk((childGroup) => {
266
- childGroup.setExcludedItems(excludedItems);
267
- }, false, false);
268
- });
208
+ tree.walk((group, groupTree) => {
209
+ if (group.definition === parentLevelDefinition) {
210
+ parentTrees.push(groupTree);
269
211
  }
270
- }
271
- }));
272
- };
273
-
274
- compositeGroups.forEach((group) => {
275
- const childTree = currentTree.addChild(group);
212
+ }, false, false);
213
+ }
276
214
 
277
- initializeGroupObservers(group, childTree);
278
-
279
- addGroupBinding(group, group.registerMarketPercentChangeHandler(() => {
280
- currentTree.walk((childGroup) => childGroup.refreshMarketPercent());
281
- }));
282
-
283
- createGroups(childTree, group.items, array.dropLeft(levelDefinitions));
284
- });
285
- };
286
-
287
- createGroups(tree, this._items, treeDefinition.definitions);
288
-
289
- map[treeDefinition.name] = tree;
290
-
291
- return map;
292
- }, { });
293
- }
294
-
295
- addPortfolio(portfolio) {
215
+ const overrideRequiredGroups = [ portfolioRequiredGroup ];
296
216
 
217
+ parentTrees.forEach((t) => {
218
+ createGroups.call(this, tree, t, [ ], treeDefinition, levelDefinitions.slice(portfolioLevelDefinitionIndex), overrideRequiredGroups);
219
+ });
220
+ }
221
+ });
222
+ }
297
223
  }
298
224
 
299
225
  removePortfolio(portfolio) {
@@ -482,6 +408,16 @@ module.exports = (() => {
482
408
  return findNode(this._trees[name], keys).getChildren().map(node => node.getValue());
483
409
  }
484
410
 
411
+ /**
412
+ * Returns all portfolios in the container
413
+ *
414
+ * @public
415
+ * @return {Array.<Object>}
416
+ */
417
+ getPortfolios() {
418
+ return this._portfolios;
419
+ }
420
+
485
421
  /**
486
422
  * Returns all positions for the given portfolio.
487
423
  *
@@ -539,5 +475,134 @@ module.exports = (() => {
539
475
  }
540
476
  }
541
477
 
478
+ function addGroupBinding(group, dispoable) {
479
+ const id = group.id;
480
+
481
+ if (!this._groupBindings.hasOwnProperty(id)) {
482
+ this._groupBindings[id] = new DisposableStack();
483
+ }
484
+
485
+ this._groupBindings[id].push(dispoable);
486
+ }
487
+
488
+ function createGroups(parentTree, currentTree, items, treeDefinition, levelDefinitions, overrideRequiredGroups) {
489
+ if (levelDefinitions.length === 0) {
490
+ return;
491
+ }
492
+
493
+ const parent = currentTree.getValue() || null;
494
+
495
+ const levelDefinition = levelDefinitions[0];
496
+
497
+ const populatedObjects = array.groupBy(items, levelDefinition.keySelector);
498
+ const populatedGroups = Object.keys(populatedObjects).reduce((list, key) => {
499
+ const items = populatedObjects[key];
500
+ const first = items[0];
501
+
502
+ list.push(new PositionGroup(this, parent, levelDefinition, items, levelDefinition.currencySelector(first), key, levelDefinition.descriptionSelector(first), levelDefinition.single && items.length === 1, levelDefinition.aggregateCash));
503
+
504
+ return list;
505
+ }, [ ]);
506
+
507
+ const requiredGroupsToUse = overrideRequiredGroups || levelDefinition.requiredGroups;
508
+
509
+ const missingGroups = array.difference(requiredGroupsToUse.map(group => group.key), populatedGroups.map(group => group.key))
510
+ .map((key) => {
511
+ return requiredGroupsToUse.find(g => g.key === key);
512
+ });
513
+
514
+ const empty = missingGroups.map((group) => {
515
+ return new PositionGroup(this, parent, levelDefinition, [ ], group.currency, group.key, group.description);
516
+ });
517
+
518
+ const compositeGroups = populatedGroups.concat(empty);
519
+
520
+ let builder;
521
+
522
+ if (requiredGroupsToUse.length !== 0) {
523
+ const ordering = requiredGroupsToUse.reduce((map, group, index) => {
524
+ map[group.description] = index;
525
+
526
+ return map;
527
+ }, { });
528
+
529
+ const getIndex = (description) => {
530
+ if (ordering.hasOwnProperty(description)) {
531
+ return ordering[description];
532
+ } else {
533
+ return Number.MAX_VALUE;
534
+ }
535
+ };
536
+
537
+ builder = ComparatorBuilder.startWith((a, b) => {
538
+ return comparators.compareNumbers(getIndex(a.description), getIndex(b.description));
539
+ }).thenBy((a, b) => {
540
+ return comparators.compareStrings(a.description, b.description);
541
+ });
542
+ } else {
543
+ builder = ComparatorBuilder.startWith((a, b) => {
544
+ return comparators.compareStrings(a.description, b.description);
545
+ });
546
+ }
547
+
548
+ compositeGroups.sort(builder.toComparator());
549
+
550
+ const initializeGroupObservers = (group, groupTree) => {
551
+ addGroupBinding.call(this, group, group.registerGroupExcludedChangeHandler((excluded, sender) => {
552
+ groupTree.climb((parentGroup) => {
553
+ if (parentGroup) {
554
+ let excludedItems = [];
555
+
556
+ currentTree.walk((childGroup) => {
557
+ if (childGroup.excluded) {
558
+ excludedItems = excludedItems.concat(childGroup.items);
559
+ }
560
+ }, false, false);
561
+
562
+ parentGroup.setExcludedItems(array.unique(excludedItems));
563
+ }
564
+ }, false);
565
+
566
+ if (treeDefinition.exclusionDependencies.length > 0) {
567
+ const dependantTrees = treeDefinition.exclusionDependencies.reduce((trees, name) => {
568
+ if (this._trees.hasOwnProperty(name)) {
569
+ trees.push(this._trees[name]);
570
+ }
571
+
572
+ return trees;
573
+ }, [ ]);
574
+
575
+ if (dependantTrees.length > 0) {
576
+ let excludedItems = [ ];
577
+
578
+ parentTree.walk((childGroup) => {
579
+ if (childGroup.excluded) {
580
+ excludedItems = excludedItems.concat(childGroup.items);
581
+ }
582
+ }, false, false);
583
+
584
+ dependantTrees.forEach((dependantTrees) => {
585
+ dependantTrees.walk((childGroup) => {
586
+ childGroup.setExcludedItems(excludedItems);
587
+ }, false, false);
588
+ });
589
+ }
590
+ }
591
+ }));
592
+ };
593
+
594
+ compositeGroups.forEach((group) => {
595
+ const childTree = currentTree.addChild(group);
596
+
597
+ initializeGroupObservers(group, childTree);
598
+
599
+ addGroupBinding.call(this, group, group.registerMarketPercentChangeHandler(() => {
600
+ currentTree.walk((childGroup) => childGroup.refreshMarketPercent());
601
+ }));
602
+
603
+ createGroups.call(this, parentTree, childTree, group.items, treeDefinition, array.dropLeft(levelDefinitions));
604
+ });
605
+ }
606
+
542
607
  return PositionContainer;
543
608
  })();
@@ -22,6 +22,7 @@ module.exports = (() => {
22
22
  * @public
23
23
  * @param {PositionContainer} container
24
24
  * @param {PositionGroup|null} parent
25
+ * @param {LevelDefinition} definition
25
26
  * @param {Array.<PositionItem>} items
26
27
  * @param {Currency} currency
27
28
  * @param {String} key
@@ -30,8 +31,10 @@ module.exports = (() => {
30
31
  * @param {Boolean=} aggregateCash
31
32
  */
32
33
  class PositionGroup {
33
- constructor(container, parent, items, currency, key, description, single, aggregateCash) {
34
+ constructor(container, parent, definition, items, currency, key, description, single, aggregateCash) {
34
35
  this._id = counter++;
36
+
37
+ this._definition = definition;
35
38
  this._container = container;
36
39
  this._parent = parent || null;
37
40
 
@@ -209,6 +212,16 @@ module.exports = (() => {
209
212
  return this._id;
210
213
  }
211
214
 
215
+ /**
216
+ * The {@link LevelDefinition} which was used to generate this group.
217
+ *
218
+ * @public
219
+ * @returns {LevelDefinition}
220
+ */
221
+ get definition() {
222
+ return this._definition;
223
+ }
224
+
212
225
  /**
213
226
  * The key of the group.
214
227
  *
@@ -20,9 +20,10 @@ module.exports = (() => {
20
20
  * @param {Array.<PositionLevelDefinition~RequiredGroup>=} requiredGroups
21
21
  * @param {Boolean=} single
22
22
  * @param {Boolean=} aggregateCash
23
+ * @param {Function=} requiredGroupGenerator
23
24
  */
24
25
  class PositionLevelDefinition {
25
- constructor(name, keySelector, descriptionSelector, currencySelector, requiredGroups, single, aggregateCash) {
26
+ constructor(name, keySelector, descriptionSelector, currencySelector, requiredGroups, single, aggregateCash, requiredGroupGenerator) {
26
27
  assert.argumentIsRequired(name, 'name', String);
27
28
  assert.argumentIsRequired(keySelector, 'keySelector', Function);
28
29
  assert.argumentIsRequired(descriptionSelector, 'descriptionSelector', Function);
@@ -34,6 +35,7 @@ module.exports = (() => {
34
35
 
35
36
  assert.argumentIsOptional(single, 'single', Boolean);
36
37
  assert.argumentIsOptional(aggregateCash, 'aggregateCash', Boolean);
38
+ assert.argumentIsOptional(requiredGroupGenerator, 'requiredGroupGenerator', Function);
37
39
 
38
40
  this._name = name;
39
41
 
@@ -42,8 +44,11 @@ module.exports = (() => {
42
44
  this._currencySelector = currencySelector;
43
45
 
44
46
  this._requiredGroups = requiredGroups || [ ];
47
+
45
48
  this._single = is.boolean(single) && single;
46
49
  this._aggregateCash = is.boolean(aggregateCash) && aggregateCash;
50
+
51
+ this._requiredGroupGenerator = requiredGroupGenerator || (input => null);
47
52
  }
48
53
 
49
54
  /**
@@ -120,6 +125,23 @@ module.exports = (() => {
120
125
  return this._aggregateCash;
121
126
  }
122
127
 
128
+ /**
129
+ * Given an input, potentially creates a new {@link PositionLevelDefinition~RequiredGroup}.
130
+ *
131
+ * @public
132
+ * @param {*} input
133
+ * @returns {PositionLevelDefinition~RequiredGroup|null}
134
+ */
135
+ generateRequiredGroup(input) {
136
+ const requiredGroup = this._requiredGroupGenerator(input);
137
+
138
+ if (requiredGroup !== null) {
139
+ this._requiredGroups.push(requiredGroup);
140
+ }
141
+
142
+ return requiredGroup;
143
+ }
144
+
123
145
  /**
124
146
  * Builds a {@link PositionLevelDefinition~RequiredGroup} for a portfolio.
125
147
  *
@@ -136,7 +158,6 @@ module.exports = (() => {
136
158
  };
137
159
  }
138
160
 
139
-
140
161
  static getKeyForPortfolioGroup(portfolio) {
141
162
  assert.argumentIsRequired(portfolio, 'portfolio', Object);
142
163
 
@@ -149,6 +170,20 @@ module.exports = (() => {
149
170
  return portfolio.name;
150
171
  }
151
172
 
173
+ static getRequiredGroupGeneratorForPortfolio() {
174
+ return (portfolio) => {
175
+ let requiredGroup;
176
+
177
+ if (is.object(portfolio) && is.string(portfolio.portfolio) && is.string(portfolio.name)) {
178
+ requiredGroup = PositionLevelDefinition.buildRequiredGroupForPortfolio(portfolio);
179
+ } else {
180
+ requiredGroup = null;
181
+ }
182
+
183
+ return requiredGroup;
184
+ };
185
+ }
186
+
152
187
  /**
153
188
  * Builds a {@link PositionLevelDefinition~RequiredGroup} for an asset class.
154
189
  *
@@ -163,10 +198,9 @@ module.exports = (() => {
163
198
  key: PositionLevelDefinition.getKeyForAssetClassGroup(type, currency),
164
199
  description: PositionLevelDefinition.getDescriptionForAssetClassGroup(type, currency),
165
200
  currency: currency
166
- }
201
+ };
167
202
  }
168
203
 
169
-
170
204
  static getKeyForAssetClassGroup(type, currency) {
171
205
  assert.argumentIsRequired(type, 'type', InstrumentType, 'InstrumentType');
172
206
  assert.argumentIsRequired(currency, 'currency', Currency, 'Currency');
@@ -43,7 +43,7 @@ module.exports = (() => {
43
43
  * bottom-most level of the tree (i.e. leaf nodes).
44
44
  *
45
45
  * @public
46
- * @returns {Array.<PositionTreeDefinition>}
46
+ * @returns {Array.<PositionLevelDefinitions>}
47
47
  */
48
48
  get definitions() {
49
49
  return this._definitions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barchart/portfolio-api-common",
3
- "version": "1.0.162",
3
+ "version": "1.0.166",
4
4
  "description": "Common classes used by the Portfolio system",
5
5
  "author": {
6
6
  "name": "Bryan Ingle",
@@ -764,17 +764,9 @@ module.exports = (() => {
764
764
  const currentSummaryFrame = PositionSummaryFrame.YTD;
765
765
  const currentSummaryRange = array.last(currentSummaryFrame.getRecentRanges(0));
766
766
 
767
- this._groupBindings = { };
768
-
769
- const addGroupBinding = (group, dispoable) => {
770
- const id = group.id;
771
-
772
- if (!this._groupBindings.hasOwnProperty(id)) {
773
- this._groupBindings[id] = new DisposableStack();
774
- }
767
+ this._definitions = definitions;
775
768
 
776
- this._groupBindings[id].push(dispoable);
777
- };
769
+ this._groupBindings = { };
778
770
 
779
771
  this._portfolios = portfolios.reduce((map, portfolio) => {
780
772
  map[portfolio.portfolio] = portfolio;
@@ -880,136 +872,70 @@ module.exports = (() => {
880
872
  return Rate.fromPair(Decimal.ONE, symbol);
881
873
  });
882
874
 
883
- this._trees = definitions.reduce((map, treeDefinition) => {
875
+ this._trees = this._definitions.reduce((map, treeDefinition) => {
884
876
  const tree = new Tree();
885
877
 
886
- const createGroups = (currentTree, items, levelDefinitions) => {
887
- if (levelDefinitions.length === 0) {
888
- return;
889
- }
890
-
891
- const parent = currentTree.getValue() || null;
892
-
893
- const levelDefinition = levelDefinitions[0];
894
-
895
- const populatedObjects = array.groupBy(items, levelDefinition.keySelector);
896
- const populatedGroups = Object.keys(populatedObjects).reduce((list, key) => {
897
- const items = populatedObjects[key];
898
- const first = items[0];
878
+ createGroups.call(this, tree, tree, this._items, treeDefinition, treeDefinition.definitions);
879
+
880
+ map[treeDefinition.name] = tree;
899
881
 
900
- list.push(new PositionGroup(this, parent, items, levelDefinition.currencySelector(first), key, levelDefinition.descriptionSelector(first), levelDefinition.single && items.length === 1, levelDefinition.aggregateCash));
882
+ return map;
883
+ }, { });
884
+ }
901
885
 
902
- return list;
903
- }, [ ]);
886
+ addPortfolio(portfolio) {
887
+ assert.argumentIsRequired(portfolio, 'portfolio', Object);
888
+ assert.argumentIsRequired(portfolio.portfolio, 'portfolio.portfolio', String);
889
+ assert.argumentIsRequired(portfolio.name, 'portfolio.name', String);
904
890
 
905
- const missingGroups = array.difference(levelDefinition.requiredGroups.map(group => group.key), populatedGroups.map(group => group.key))
906
- .map((key) => {
907
- return levelDefinition.requiredGroups.find(g => g.key === key);
908
- });
891
+ const key = portfolio.portfolio;
909
892
 
910
- const empty = missingGroups.map((group) => {
911
- return new PositionGroup(this, parent, [ ], group.currency, group.key, group.description);
912
- });
893
+ if (!this._portfolios.hasOwnProperty(key)) {
894
+ this._portfolios[key] = portfolio;
913
895
 
914
- const compositeGroups = populatedGroups.concat(empty);
896
+ this._definitions.forEach((treeDefinition) => {
897
+ const tree = this._trees[treeDefinition.name];
898
+ const levelDefinitions = treeDefinition.definitions;
915
899
 
916
- let builder;
900
+ let portfolioRequiredGroup = null;
917
901
 
918
- if (levelDefinition.requiredGroups.length !== 0) {
919
- const ordering = levelDefinition.requiredGroups.reduce((map, group, index) => {
920
- map[group.description] = index;
902
+ let portfolioLevelDefinition = null;
903
+ let portfolioLevelDefinitionIndex = null;
921
904
 
922
- return map;
923
- }, { });
905
+ levelDefinitions.forEach((levelDefinition, i) => {
906
+ if (portfolioRequiredGroup === null) {
907
+ portfolioRequiredGroup = levelDefinition.generateRequiredGroup(portfolio);
924
908
 
925
- const getIndex = (description) => {
926
- if (ordering.hasOwnProperty(description)) {
927
- return ordering[description];
928
- } else {
929
- return Number.MAX_VALUE;
909
+ if (portfolioRequiredGroup !== null) {
910
+ portfolioLevelDefinition = levelDefinition;
911
+ portfolioLevelDefinitionIndex = i;
930
912
  }
931
- };
932
-
933
- builder = ComparatorBuilder.startWith((a, b) => {
934
- return comparators.compareNumbers(getIndex(a.description), getIndex(b.description));
935
- }).thenBy((a, b) => {
936
- return comparators.compareStrings(a.description, b.description);
937
- });
938
- } else {
939
- builder = ComparatorBuilder.startWith((a, b) => {
940
- return comparators.compareStrings(a.description, b.description);
941
- });
942
- }
943
-
944
- compositeGroups.sort(builder.toComparator());
913
+ }
914
+ });
945
915
 
946
- const initializeGroupObservers = (group, groupTree) => {
947
- addGroupBinding(group, group.registerGroupExcludedChangeHandler((excluded, sender) => {
948
- groupTree.climb((parentGroup) => {
949
- if (parentGroup) {
950
- let excludedItems = [];
916
+ if (portfolioRequiredGroup !== null) {
917
+ let parentTrees = [ ];
951
918
 
952
- currentTree.walk((childGroup) => {
953
- if (childGroup.excluded) {
954
- excludedItems = excludedItems.concat(childGroup.items);
955
- }
956
- }, false, false);
919
+ if (portfolioLevelDefinitionIndex === 0) {
920
+ parentTrees.push(tree);
921
+ } else {
922
+ const parentLevelDefinition = levelDefinitions[ portfolioLevelDefinitionIndex - 1 ];
957
923
 
958
- parentGroup.setExcludedItems(array.unique(excludedItems));
959
- }
960
- }, false);
961
-
962
- if (treeDefinition.exclusionDependencies.length > 0) {
963
- const dependantTrees = treeDefinition.exclusionDependencies.reduce((trees, name) => {
964
- if (this._trees.hasOwnProperty(name)) {
965
- trees.push(this._trees[name]);
966
- }
967
-
968
- return trees;
969
- }, [ ]);
970
-
971
- if (dependantTrees.length > 0) {
972
- let excludedItems = [ ];
973
-
974
- tree.walk((childGroup) => {
975
- if (childGroup.excluded) {
976
- excludedItems = excludedItems.concat(childGroup.items);
977
- }
978
- }, false, false);
979
-
980
- dependantTrees.forEach((dependantTrees) => {
981
- dependantTrees.walk((childGroup) => {
982
- childGroup.setExcludedItems(excludedItems);
983
- }, false, false);
984
- });
924
+ tree.walk((group, groupTree) => {
925
+ if (group.definition === parentLevelDefinition) {
926
+ parentTrees.push(groupTree);
985
927
  }
986
- }
987
- }));
988
- };
989
-
990
- compositeGroups.forEach((group) => {
991
- const childTree = currentTree.addChild(group);
992
-
993
- initializeGroupObservers(group, childTree);
994
-
995
- addGroupBinding(group, group.registerMarketPercentChangeHandler(() => {
996
- currentTree.walk((childGroup) => childGroup.refreshMarketPercent());
997
- }));
998
-
999
- createGroups(childTree, group.items, array.dropLeft(levelDefinitions));
1000
- });
1001
- };
1002
-
1003
- createGroups(tree, this._items, treeDefinition.definitions);
1004
-
1005
- map[treeDefinition.name] = tree;
1006
-
1007
- return map;
1008
- }, { });
1009
- }
928
+ }, false, false);
929
+ }
1010
930
 
1011
- addPortfolio(portfolio) {
931
+ const overrideRequiredGroups = [ portfolioRequiredGroup ];
1012
932
 
933
+ parentTrees.forEach((t) => {
934
+ createGroups.call(this, tree, t, [ ], treeDefinition, levelDefinitions.slice(portfolioLevelDefinitionIndex), overrideRequiredGroups);
935
+ });
936
+ }
937
+ });
938
+ }
1013
939
  }
1014
940
 
1015
941
  removePortfolio(portfolio) {
@@ -1198,6 +1124,16 @@ module.exports = (() => {
1198
1124
  return findNode(this._trees[name], keys).getChildren().map(node => node.getValue());
1199
1125
  }
1200
1126
 
1127
+ /**
1128
+ * Returns all portfolios in the container
1129
+ *
1130
+ * @public
1131
+ * @return {Array.<Object>}
1132
+ */
1133
+ getPortfolios() {
1134
+ return this._portfolios;
1135
+ }
1136
+
1201
1137
  /**
1202
1138
  * Returns all positions for the given portfolio.
1203
1139
  *
@@ -1255,6 +1191,135 @@ module.exports = (() => {
1255
1191
  }
1256
1192
  }
1257
1193
 
1194
+ function addGroupBinding(group, dispoable) {
1195
+ const id = group.id;
1196
+
1197
+ if (!this._groupBindings.hasOwnProperty(id)) {
1198
+ this._groupBindings[id] = new DisposableStack();
1199
+ }
1200
+
1201
+ this._groupBindings[id].push(dispoable);
1202
+ }
1203
+
1204
+ function createGroups(parentTree, currentTree, items, treeDefinition, levelDefinitions, overrideRequiredGroups) {
1205
+ if (levelDefinitions.length === 0) {
1206
+ return;
1207
+ }
1208
+
1209
+ const parent = currentTree.getValue() || null;
1210
+
1211
+ const levelDefinition = levelDefinitions[0];
1212
+
1213
+ const populatedObjects = array.groupBy(items, levelDefinition.keySelector);
1214
+ const populatedGroups = Object.keys(populatedObjects).reduce((list, key) => {
1215
+ const items = populatedObjects[key];
1216
+ const first = items[0];
1217
+
1218
+ list.push(new PositionGroup(this, parent, levelDefinition, items, levelDefinition.currencySelector(first), key, levelDefinition.descriptionSelector(first), levelDefinition.single && items.length === 1, levelDefinition.aggregateCash));
1219
+
1220
+ return list;
1221
+ }, [ ]);
1222
+
1223
+ const requiredGroupsToUse = overrideRequiredGroups || levelDefinition.requiredGroups;
1224
+
1225
+ const missingGroups = array.difference(requiredGroupsToUse.map(group => group.key), populatedGroups.map(group => group.key))
1226
+ .map((key) => {
1227
+ return requiredGroupsToUse.find(g => g.key === key);
1228
+ });
1229
+
1230
+ const empty = missingGroups.map((group) => {
1231
+ return new PositionGroup(this, parent, levelDefinition, [ ], group.currency, group.key, group.description);
1232
+ });
1233
+
1234
+ const compositeGroups = populatedGroups.concat(empty);
1235
+
1236
+ let builder;
1237
+
1238
+ if (requiredGroupsToUse.length !== 0) {
1239
+ const ordering = requiredGroupsToUse.reduce((map, group, index) => {
1240
+ map[group.description] = index;
1241
+
1242
+ return map;
1243
+ }, { });
1244
+
1245
+ const getIndex = (description) => {
1246
+ if (ordering.hasOwnProperty(description)) {
1247
+ return ordering[description];
1248
+ } else {
1249
+ return Number.MAX_VALUE;
1250
+ }
1251
+ };
1252
+
1253
+ builder = ComparatorBuilder.startWith((a, b) => {
1254
+ return comparators.compareNumbers(getIndex(a.description), getIndex(b.description));
1255
+ }).thenBy((a, b) => {
1256
+ return comparators.compareStrings(a.description, b.description);
1257
+ });
1258
+ } else {
1259
+ builder = ComparatorBuilder.startWith((a, b) => {
1260
+ return comparators.compareStrings(a.description, b.description);
1261
+ });
1262
+ }
1263
+
1264
+ compositeGroups.sort(builder.toComparator());
1265
+
1266
+ const initializeGroupObservers = (group, groupTree) => {
1267
+ addGroupBinding.call(this, group, group.registerGroupExcludedChangeHandler((excluded, sender) => {
1268
+ groupTree.climb((parentGroup) => {
1269
+ if (parentGroup) {
1270
+ let excludedItems = [];
1271
+
1272
+ currentTree.walk((childGroup) => {
1273
+ if (childGroup.excluded) {
1274
+ excludedItems = excludedItems.concat(childGroup.items);
1275
+ }
1276
+ }, false, false);
1277
+
1278
+ parentGroup.setExcludedItems(array.unique(excludedItems));
1279
+ }
1280
+ }, false);
1281
+
1282
+ if (treeDefinition.exclusionDependencies.length > 0) {
1283
+ const dependantTrees = treeDefinition.exclusionDependencies.reduce((trees, name) => {
1284
+ if (this._trees.hasOwnProperty(name)) {
1285
+ trees.push(this._trees[name]);
1286
+ }
1287
+
1288
+ return trees;
1289
+ }, [ ]);
1290
+
1291
+ if (dependantTrees.length > 0) {
1292
+ let excludedItems = [ ];
1293
+
1294
+ parentTree.walk((childGroup) => {
1295
+ if (childGroup.excluded) {
1296
+ excludedItems = excludedItems.concat(childGroup.items);
1297
+ }
1298
+ }, false, false);
1299
+
1300
+ dependantTrees.forEach((dependantTrees) => {
1301
+ dependantTrees.walk((childGroup) => {
1302
+ childGroup.setExcludedItems(excludedItems);
1303
+ }, false, false);
1304
+ });
1305
+ }
1306
+ }
1307
+ }));
1308
+ };
1309
+
1310
+ compositeGroups.forEach((group) => {
1311
+ const childTree = currentTree.addChild(group);
1312
+
1313
+ initializeGroupObservers(group, childTree);
1314
+
1315
+ addGroupBinding.call(this, group, group.registerMarketPercentChangeHandler(() => {
1316
+ currentTree.walk((childGroup) => childGroup.refreshMarketPercent());
1317
+ }));
1318
+
1319
+ createGroups.call(this, parentTree, childTree, group.items, treeDefinition, array.dropLeft(levelDefinitions));
1320
+ });
1321
+ }
1322
+
1258
1323
  return PositionContainer;
1259
1324
  })();
1260
1325
 
@@ -1283,6 +1348,7 @@ module.exports = (() => {
1283
1348
  * @public
1284
1349
  * @param {PositionContainer} container
1285
1350
  * @param {PositionGroup|null} parent
1351
+ * @param {LevelDefinition} definition
1286
1352
  * @param {Array.<PositionItem>} items
1287
1353
  * @param {Currency} currency
1288
1354
  * @param {String} key
@@ -1291,8 +1357,10 @@ module.exports = (() => {
1291
1357
  * @param {Boolean=} aggregateCash
1292
1358
  */
1293
1359
  class PositionGroup {
1294
- constructor(container, parent, items, currency, key, description, single, aggregateCash) {
1360
+ constructor(container, parent, definition, items, currency, key, description, single, aggregateCash) {
1295
1361
  this._id = counter++;
1362
+
1363
+ this._definition = definition;
1296
1364
  this._container = container;
1297
1365
  this._parent = parent || null;
1298
1366
 
@@ -1470,6 +1538,16 @@ module.exports = (() => {
1470
1538
  return this._id;
1471
1539
  }
1472
1540
 
1541
+ /**
1542
+ * The {@link LevelDefinition} which was used to generate this group.
1543
+ *
1544
+ * @public
1545
+ * @returns {LevelDefinition}
1546
+ */
1547
+ get definition() {
1548
+ return this._definition;
1549
+ }
1550
+
1473
1551
  /**
1474
1552
  * The key of the group.
1475
1553
  *
@@ -2342,9 +2420,10 @@ module.exports = (() => {
2342
2420
  * @param {Array.<PositionLevelDefinition~RequiredGroup>=} requiredGroups
2343
2421
  * @param {Boolean=} single
2344
2422
  * @param {Boolean=} aggregateCash
2423
+ * @param {Function=} requiredGroupGenerator
2345
2424
  */
2346
2425
  class PositionLevelDefinition {
2347
- constructor(name, keySelector, descriptionSelector, currencySelector, requiredGroups, single, aggregateCash) {
2426
+ constructor(name, keySelector, descriptionSelector, currencySelector, requiredGroups, single, aggregateCash, requiredGroupGenerator) {
2348
2427
  assert.argumentIsRequired(name, 'name', String);
2349
2428
  assert.argumentIsRequired(keySelector, 'keySelector', Function);
2350
2429
  assert.argumentIsRequired(descriptionSelector, 'descriptionSelector', Function);
@@ -2356,6 +2435,7 @@ module.exports = (() => {
2356
2435
 
2357
2436
  assert.argumentIsOptional(single, 'single', Boolean);
2358
2437
  assert.argumentIsOptional(aggregateCash, 'aggregateCash', Boolean);
2438
+ assert.argumentIsOptional(requiredGroupGenerator, 'requiredGroupGenerator', Function);
2359
2439
 
2360
2440
  this._name = name;
2361
2441
 
@@ -2364,8 +2444,11 @@ module.exports = (() => {
2364
2444
  this._currencySelector = currencySelector;
2365
2445
 
2366
2446
  this._requiredGroups = requiredGroups || [ ];
2447
+
2367
2448
  this._single = is.boolean(single) && single;
2368
2449
  this._aggregateCash = is.boolean(aggregateCash) && aggregateCash;
2450
+
2451
+ this._requiredGroupGenerator = requiredGroupGenerator || (input => null);
2369
2452
  }
2370
2453
 
2371
2454
  /**
@@ -2442,6 +2525,23 @@ module.exports = (() => {
2442
2525
  return this._aggregateCash;
2443
2526
  }
2444
2527
 
2528
+ /**
2529
+ * Given an input, potentially creates a new {@link PositionLevelDefinition~RequiredGroup}.
2530
+ *
2531
+ * @public
2532
+ * @param {*} input
2533
+ * @returns {PositionLevelDefinition~RequiredGroup|null}
2534
+ */
2535
+ generateRequiredGroup(input) {
2536
+ const requiredGroup = this._requiredGroupGenerator(input);
2537
+
2538
+ if (requiredGroup !== null) {
2539
+ this._requiredGroups.push(requiredGroup);
2540
+ }
2541
+
2542
+ return requiredGroup;
2543
+ }
2544
+
2445
2545
  /**
2446
2546
  * Builds a {@link PositionLevelDefinition~RequiredGroup} for a portfolio.
2447
2547
  *
@@ -2458,7 +2558,6 @@ module.exports = (() => {
2458
2558
  };
2459
2559
  }
2460
2560
 
2461
-
2462
2561
  static getKeyForPortfolioGroup(portfolio) {
2463
2562
  assert.argumentIsRequired(portfolio, 'portfolio', Object);
2464
2563
 
@@ -2471,6 +2570,20 @@ module.exports = (() => {
2471
2570
  return portfolio.name;
2472
2571
  }
2473
2572
 
2573
+ static getRequiredGroupGeneratorForPortfolio() {
2574
+ return (portfolio) => {
2575
+ let requiredGroup;
2576
+
2577
+ if (is.object(portfolio) && is.string(portfolio.portfolio) && is.string(portfolio.name)) {
2578
+ requiredGroup = PositionLevelDefinition.buildRequiredGroupForPortfolio(portfolio);
2579
+ } else {
2580
+ requiredGroup = null;
2581
+ }
2582
+
2583
+ return requiredGroup;
2584
+ };
2585
+ }
2586
+
2474
2587
  /**
2475
2588
  * Builds a {@link PositionLevelDefinition~RequiredGroup} for an asset class.
2476
2589
  *
@@ -2485,10 +2598,9 @@ module.exports = (() => {
2485
2598
  key: PositionLevelDefinition.getKeyForAssetClassGroup(type, currency),
2486
2599
  description: PositionLevelDefinition.getDescriptionForAssetClassGroup(type, currency),
2487
2600
  currency: currency
2488
- }
2601
+ };
2489
2602
  }
2490
2603
 
2491
-
2492
2604
  static getKeyForAssetClassGroup(type, currency) {
2493
2605
  assert.argumentIsRequired(type, 'type', InstrumentType, 'InstrumentType');
2494
2606
  assert.argumentIsRequired(currency, 'currency', Currency, 'Currency');
@@ -2598,7 +2710,7 @@ module.exports = (() => {
2598
2710
  * bottom-most level of the tree (i.e. leaf nodes).
2599
2711
  *
2600
2712
  * @public
2601
- * @returns {Array.<PositionTreeDefinition>}
2713
+ * @returns {Array.<PositionLevelDefinitions>}
2602
2714
  */
2603
2715
  get definitions() {
2604
2716
  return this._definitions;