@barchart/portfolio-api-common 1.0.128 → 1.0.132

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.
@@ -22,11 +22,11 @@ module.exports = (() => {
22
22
 
23
23
  /**
24
24
  * A container for positions which groups the positions into one or more
25
- * trees for aggregation and display purposes. For example, perhaps a positions
26
- * grouped first by asset class then by position is desired.
25
+ * trees for aggregation and display purposes. For example, positions could be
26
+ * grouped first by asset class then by position.
27
27
  *
28
28
  * Furthermore, the container performs aggregation (driven primarily by price
29
- * changes) for each level of grouping in the internal tree(s).
29
+ * changes) for each level of grouping.
30
30
  *
31
31
  * @public
32
32
  * @param {Array.<PositionTreeDefinition>} definitions
@@ -108,6 +108,20 @@ module.exports = (() => {
108
108
  return map;
109
109
  }, { });
110
110
 
111
+ this._symbolsDisplay = this._items.reduce((map, item) => {
112
+ const symbol = extractSymbolForDisplay(item.position);
113
+
114
+ if (symbol) {
115
+ if (!map.hasOwnProperty(symbol)) {
116
+ map[symbol] = [ ];
117
+ }
118
+
119
+ map[symbol].push(item);
120
+ }
121
+
122
+ return map;
123
+ }, { });
124
+
111
125
  this._currencies = this._items.reduce((map, item) => {
112
126
  const position = item.position;
113
127
 
@@ -219,10 +233,14 @@ module.exports = (() => {
219
233
  }, { });
220
234
  }
221
235
 
222
- get defaultCurrency() {
223
- return this._defaultCurrency;
224
- }
225
-
236
+ /**
237
+ * Returns a distinct list of all symbols used by the positions
238
+ * within the container.
239
+ *
240
+ * @public
241
+ * @param {Boolean} display - If true, all "display" symbols are returned; otherwise Barchart symbols are returned.
242
+ * @returns {Array.<String>}
243
+ */
226
244
  getPositionSymbols(display) {
227
245
  const symbols = this._items.reduce((symbols, item) => {
228
246
  const position = item.position;
@@ -245,6 +263,14 @@ module.exports = (() => {
245
263
  return array.unique(symbols);
246
264
  }
247
265
 
266
+ /**
267
+ * Updates the quote for a single symbol; causing updates to any grouping
268
+ * level that contains the position(s) for the symbol.
269
+ *
270
+ * @public
271
+ * @param {String} symbol
272
+ * @param {Object} quote
273
+ */
248
274
  setPositionQuote(symbol, quote) {
249
275
  assert.argumentIsRequired(symbol, 'symbol', String);
250
276
  assert.argumentIsRequired(quote, 'quote', Object);
@@ -254,14 +280,34 @@ module.exports = (() => {
254
280
  }
255
281
  }
256
282
 
283
+ /**
284
+ * Returns all forex symbols that are required to do currency translations.
285
+ *
286
+ * @public
287
+ * @returns {Array.<String>}
288
+ */
257
289
  getForexSymbols() {
258
290
  return this._forexSymbols;
259
291
  }
260
292
 
293
+ /**
294
+ * Returns all current forex quotes.
295
+ *
296
+ * @public
297
+ * @returns {Array.<Object>}
298
+ */
261
299
  getForexQuotes() {
262
300
  return this._forexQuotes;
263
301
  }
264
302
 
303
+ /**
304
+ * Updates the forex quote for a single currency pair; causing updates to
305
+ * any grouping level that contains that requires translation.
306
+ *
307
+ * @public
308
+ * @param {String} symbol
309
+ * @param {Object} quote
310
+ */
265
311
  setForexQuote(symbol, quote) {
266
312
  assert.argumentIsRequired(symbol, 'symbol', String);
267
313
  assert.argumentIsRequired(quote, 'quote', Object);
@@ -279,10 +325,50 @@ module.exports = (() => {
279
325
  Object.keys(this._trees).forEach(key => this._trees[key].walk(group => group.setForexRate(rate), true, false));
280
326
  }
281
327
 
282
- setPositionFundamentals(data) {
328
+ /**
329
+ * Updates fundamental data for a single symbol.
330
+ *
331
+ * @public
332
+ * @param {String} symbol
333
+ * @param {Object} data
334
+ */
335
+ setPositionFundamentalData(symbol, data) {
283
336
  return;
284
337
  }
285
338
 
339
+ /**
340
+ * Indicates if a news article exists for a symbol.
341
+ *
342
+ * @public
343
+ * @param {String} symbol
344
+ * @param {Boolean} display
345
+ * @param {Boolean} exists
346
+ */
347
+ setNewsArticleExists(symbol, display, exists) {
348
+ assert.argumentIsRequired(symbol, 'symbol', String);
349
+ assert.argumentIsRequired(display, 'display', Boolean);
350
+ assert.argumentIsRequired(exists, 'exists', Boolean);
351
+
352
+ let map;
353
+
354
+ if (display) {
355
+ map = this._symbols;
356
+ } else {
357
+ map = this._symbolsDisplay;
358
+ }
359
+
360
+ if (map.hasOwnProperty(symbol)) {
361
+ map[symbol].forEach(item => item.setNewsArticleExists(exists));
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Returns a single level of grouping from one of the internal trees.
367
+ *
368
+ * @param {String} name
369
+ * @param {Array.<String> keys
370
+ * @returns {PositionGroup}
371
+ */
286
372
  getGroup(name, keys) {
287
373
  assert.argumentIsRequired(name, 'name', String);
288
374
  assert.argumentIsArray(keys, 'keys', Number);
@@ -290,6 +376,14 @@ module.exports = (() => {
290
376
  return findNode(this._trees[name], keys).getValue();
291
377
  }
292
378
 
379
+ /**
380
+ * Returns all child groups from a level of grouping within one of
381
+ * the internal trees.
382
+ *
383
+ * @param {String} name
384
+ * @param {Array.<String> keys
385
+ * @returns {Array.<PositionGroup>}
386
+ */
293
387
  getGroups(name, keys) {
294
388
  assert.argumentIsRequired(name, 'name', String);
295
389
  assert.argumentIsArray(keys, 'keys', Number);
@@ -301,8 +395,6 @@ module.exports = (() => {
301
395
  assert.argumentIsRequired(name, 'name', String);
302
396
  assert.argumentIsRequired(executor, 'executor', Function);
303
397
 
304
- assert.argumentIsRequired(executor, 'executor', Function);
305
-
306
398
  this._trees[name].walk(group => group.setSuspended(true), false, false);
307
399
 
308
400
  executor(this);
@@ -10,7 +10,17 @@ module.exports = (() => {
10
10
  'use strict';
11
11
 
12
12
  /**
13
+ * A grouping of {@link PositionItem} instances. The group aggregates from across
14
+ * all the positions and performs currency translation, as necessary.
15
+ *
13
16
  * @public
17
+ * @param {PositionContainer} container
18
+ * @param {PositionGroup|null} parent
19
+ * @param {Array.<PositionItem>} items
20
+ * @param {Currency} currency
21
+ * @param {String} key
22
+ * @param {String} description
23
+ * @param {Boolean=} single
14
24
  */
15
25
  class PositionGroup {
16
26
  constructor(container, parent, items, currency, key, description, single) {
@@ -30,15 +40,22 @@ module.exports = (() => {
30
40
  this._suspended = false;
31
41
 
32
42
  this._marketPercentChangeEvent = new Event(this);
43
+ this._excludedChangeEvent = new Event(this);
33
44
 
34
45
  this._dataFormat = { };
35
46
  this._dataActual = { };
36
47
 
37
48
  this._dataFormat.key = this._key;
38
49
  this._dataFormat.description = this._description;
39
- this._dataFormat.c = this._currency.code;
40
-
50
+ this._dataFormat.newsExists = false;
41
51
  this._dataFormat.quantity = null;
52
+ this._dataFormat.basisPrice = null;
53
+
54
+ this._dataActual.key = this._key;
55
+ this._dataActual.description = this._description;
56
+ this._dataActual.newsExists = false;
57
+ this._dataActual.quantity = null;
58
+ this._dataActual.basisPrice = null;
42
59
 
43
60
  if (this._single) {
44
61
  const item = items[0];
@@ -117,35 +134,85 @@ module.exports = (() => {
117
134
 
118
135
  calculatePriceData(this, this._container.getForexQuotes(), sender, false);
119
136
  });
137
+
138
+ if (this._single) {
139
+ item.registerNewsExistsChangeHandler((exists, sender) => {
140
+ this._dataFormat.newsExists = exists;
141
+ this._dataFormat.newsExists = exists;
142
+ });
143
+ }
120
144
  });
121
145
 
122
146
  this.refresh();
123
147
  }
124
148
 
149
+ /**
150
+ * The key of the group.
151
+ *
152
+ * @public
153
+ * @returns {String}
154
+ */
125
155
  get key() {
126
156
  return this._key;
127
157
  }
128
158
 
159
+ /**
160
+ * The description of the group.
161
+ *
162
+ * @public
163
+ * @returns {String}
164
+ */
129
165
  get description() {
130
166
  return this._description;
131
167
  }
132
168
 
169
+ /**
170
+ * The {@link Currency} which all aggregated data is presented in.
171
+ *
172
+ * @public
173
+ * @returns {Currency}
174
+ */
133
175
  get currency() {
134
176
  return this._currency;
135
177
  }
136
178
 
179
+ /**
180
+ * The {@link PositionItem} instances which for which aggregated data is compiled.
181
+ *
182
+ * @public
183
+ * @returns {Currency}
184
+ */
137
185
  get items() {
138
186
  return this._items;
139
187
  }
140
188
 
189
+ /**
190
+ * The string-based, human-readable aggregated data for the group.
191
+ *
192
+ * @public
193
+ * @returns {Object}
194
+ */
141
195
  get data() {
142
196
  return this._dataFormat;
143
197
  }
144
198
 
199
+ /**
200
+ * The raw aggregated data for the group (typically {@link Decimal} instances).
201
+ *
202
+ * @public
203
+ * @returns {Object}
204
+ */
145
205
  get actual() {
146
206
  return this._dataActual;
147
207
  }
148
208
 
209
+ /**
210
+ * Indicates if the group will only contain one {@link PositionItem} -- that is,
211
+ * indicates if the group represents a single position.
212
+ *
213
+ * @public
214
+ * @returns {Boolean}
215
+ */
149
216
  get single() {
150
217
  return this._single;
151
218
  }
@@ -154,10 +221,22 @@ module.exports = (() => {
154
221
  return this._suspended;
155
222
  }
156
223
 
224
+ /**
225
+ * Indicates if the group should be excluded from higher-level aggregations.
226
+ *
227
+ * @public
228
+ * @returns {Boolean}
229
+ */
157
230
  get excluded() {
158
231
  return this._excluded;
159
232
  }
160
233
 
234
+ /**
235
+ * Causes aggregated data to be recalculated using a new exchange rate.
236
+ *
237
+ * @public
238
+ * @param {Rate} rate
239
+ */
161
240
  setForexRate(rate) {
162
241
  if (!this._bypassCurrencyTranslation) {
163
242
  this.refresh();
@@ -168,11 +247,7 @@ module.exports = (() => {
168
247
  assert.argumentIsRequired(value, 'value', Boolean);
169
248
 
170
249
  if (this._excluded !== value) {
171
- this._container.startTransaction(() => {
172
- this._items.forEach((item) => {
173
- item.setExcluded(value);
174
- });
175
- });
250
+ this._excludedChangeEvent(this._excluded = value);
176
251
  }
177
252
  }
178
253
 
@@ -186,6 +261,11 @@ module.exports = (() => {
186
261
  }
187
262
  }
188
263
 
264
+ /**
265
+ * Causes all aggregated data to be recalculated.
266
+ *
267
+ * @public
268
+ */
189
269
  refresh() {
190
270
  const rates = this._container.getForexQuotes();
191
271
 
@@ -193,6 +273,12 @@ module.exports = (() => {
193
273
  calculatePriceData(this, rates, null, true);
194
274
  }
195
275
 
276
+ /**
277
+ * Causes the percent of the position, with respect to the parent container's
278
+ * total, to be recalculated.
279
+ *
280
+ * @public
281
+ */
196
282
  refreshMarketPercent() {
197
283
  calculateMarketPercent(this, this._container.getForexQuotes(), true);
198
284
  }
@@ -246,7 +332,7 @@ module.exports = (() => {
246
332
 
247
333
  const items = group._items;
248
334
 
249
- group._bypassCurrencyTranslation = items.some(item => item.currency !== currency);
335
+ group._bypassCurrencyTranslation = items.every(item => item.currency === currency);
250
336
 
251
337
  const translate = (item, value) => {
252
338
  let translated;
@@ -298,8 +384,11 @@ module.exports = (() => {
298
384
  if (group.single) {
299
385
  const item = group._items[0];
300
386
 
301
- format.quantity = formatDecimal(item.position.snapshot.open, 2);
302
- format.basisPrice = formatCurrency(item.data.basisPrice, currency);
387
+ actual.quantity = item.position.snapshot.open;
388
+ actual.basisPrice = item.data.basisPrice;
389
+
390
+ format.quantity = formatDecimal(actual.quantity, 2);
391
+ format.basisPrice = formatCurrency(actual.basisPrice, currency);
303
392
  }
304
393
  }
305
394
 
@@ -11,7 +11,15 @@ module.exports = (() => {
11
11
  'use strict';
12
12
 
13
13
  /**
14
+ * A container for a single position, which handles quote changes and
15
+ * notifies observers -- which are typically parent-level {@link PositionGroup}
16
+ * instances.
17
+ *
14
18
  * @public
19
+ * @param {Object} portfolio
20
+ * @param {Object} position
21
+ * @param {Object} currentSummary
22
+ * @param {Array.<Object>} previousSummaries
15
23
  */
16
24
  class PositionItem {
17
25
  constructor(portfolio, position, currentSummary, previousSummaries) {
@@ -27,10 +35,12 @@ module.exports = (() => {
27
35
  this._data.basis = null;
28
36
 
29
37
  this._currentQuote = null;
30
- this._previousQuote = null;
38
+
39
+ this._currentPrice = null;
40
+ this._previousPrice = null;
31
41
 
32
42
  this._data.currentPrice = null;
33
- this._data.previousPrice = null;
43
+ this._data.currentPricePrevious = null;
34
44
 
35
45
  this._data.market = null;
36
46
  this._data.marketChange = null;
@@ -50,74 +60,141 @@ module.exports = (() => {
50
60
  this._data.income = null;
51
61
  this._data.basisPrice = null;
52
62
 
53
- this._excluded = false;
63
+ this._data.newsExists = false;
54
64
 
55
65
  calculateStaticData(this);
56
66
  calculatePriceData(this, null);
57
67
 
58
68
  this._quoteChangedEvent = new Event(this);
59
- this._excludedChangeEvent = new Event(this);
69
+ this._newsExistsChangedEvent = new Event(this);
60
70
  }
61
71
 
72
+ /**
73
+ * The portfolio of the encapsulated position.
74
+ *
75
+ * @public
76
+ * @returns {Object}
77
+ */
62
78
  get portfolio() {
63
79
  return this._portfolio;
64
80
  }
65
81
 
82
+ /**
83
+ * The encapsulated position.
84
+ *
85
+ * @public
86
+ * @returns {Object}
87
+ */
66
88
  get position() {
67
89
  return this._position;
68
90
  }
69
91
 
92
+ /**
93
+ * The {@link Currency} of the encapsulated position.
94
+ *
95
+ * @public
96
+ * @returns {Object}
97
+ */
70
98
  get currency() {
71
99
  return this._currency;
72
100
  }
73
101
 
102
+ /**
103
+ * The year-to-date position summary of the encapsulated position.
104
+ *
105
+ * @public
106
+ * @returns {Object}
107
+ */
74
108
  get currentSummary() {
75
109
  return this._currentSummary;
76
110
  }
77
-
111
+
112
+ /**
113
+ * Previous year's summaries for the encapsulated position.
114
+ *
115
+ * @public
116
+ * @returns {Object}
117
+ */
78
118
  get previousSummaries() {
79
119
  return this._previousSummaries;
80
120
  }
81
121
 
122
+ /**
123
+ * Various data regarding the encapsulated position.
124
+ *
125
+ * @public
126
+ * @returns {*}
127
+ */
82
128
  get data() {
83
129
  return this._data;
84
130
  }
85
131
 
132
+ /**
133
+ * The current quote for the symbol of the encapsulated position.
134
+ *
135
+ * @public
136
+ * @returns {null|{Object}}
137
+ */
86
138
  get quote() {
87
139
  return this._currentQuote;
88
140
  }
89
141
 
90
- get excluded() {
91
- return this._excluded;
92
- }
93
-
142
+ /**
143
+ * Sets the current quote -- causing position-level data (e.g. market value) to
144
+ * be recalculated.
145
+ *
146
+ * @public
147
+ * @param {Object} quote
148
+ */
94
149
  setQuote(quote) {
95
150
  assert.argumentIsRequired(quote, 'quote', Object);
96
151
 
97
- if (this._previousQuote === null || this._previousQuote.lastPrice !== quote.lastPrice) {
152
+ if (this._currentPricePrevious !== quote.lastPrice) {
98
153
  calculatePriceData(this, quote.lastPrice);
99
154
 
100
- this._previousQuote = this._currentQuote;
155
+ this._currentPricePrevious = this._currentPrice;
156
+ this._currentPrice = quote.lastPrice;
157
+
101
158
  this._currentQuote = quote;
102
159
 
103
160
  this._quoteChangedEvent.fire(this._currentQuote);
104
161
  }
105
162
  }
106
163
 
107
- setExcluded(value) {
164
+ /**
165
+ * Sets a flag which indicates if news article(s) exist for the encapsulated position's
166
+ * symbol.
167
+ *
168
+ * @public
169
+ * @param {Boolean} value
170
+ */
171
+ setNewsArticleExists(value) {
108
172
  assert.argumentIsRequired(value, 'value', Boolean);
109
173
 
110
- if (this._excluded !== value) {
111
- this._excludedChangeEvent.fire(this._excluded = value);
174
+ if (this._data.newsExists !== value) {
175
+ this._newsExistsChangedEvent.fire(this._data.newsExists = value);
112
176
  }
113
177
  }
114
178
 
179
+ /**
180
+ * Registers an observer for quote changes, which is fired after internal recalculations
181
+ * of position data are complete.
182
+ *
183
+ * @public
184
+ * @param {Function} handler
185
+ */
115
186
  registerQuoteChangeHandler(handler) {
116
187
  this._quoteChangedEvent.register(handler);
117
188
  }
118
189
 
119
- registerExcludedChangeHandler(handler) {
120
- this._excludedChangeEvent.register(handler);
190
+ /**
191
+ * Registers an observer changes to the status of news existence.
192
+ *
193
+ * @public
194
+ * @param {Function} handler
195
+ */
196
+ registerNewsExistsChangeHandler(handler) {
197
+ this._newsExistsChangedEvent.register(handler);
121
198
  }
122
199
 
123
200
  toString() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barchart/portfolio-api-common",
3
- "version": "1.0.128",
3
+ "version": "1.0.132",
4
4
  "description": "Common classes used by the Portfolio system",
5
5
  "author": {
6
6
  "name": "Bryan Ingle",
@@ -738,11 +738,11 @@ module.exports = (() => {
738
738
 
739
739
  /**
740
740
  * A container for positions which groups the positions into one or more
741
- * trees for aggregation and display purposes. For example, perhaps a positions
742
- * grouped first by asset class then by position is desired.
741
+ * trees for aggregation and display purposes. For example, positions could be
742
+ * grouped first by asset class then by position.
743
743
  *
744
744
  * Furthermore, the container performs aggregation (driven primarily by price
745
- * changes) for each level of grouping in the internal tree(s).
745
+ * changes) for each level of grouping.
746
746
  *
747
747
  * @public
748
748
  * @param {Array.<PositionTreeDefinition>} definitions
@@ -824,6 +824,20 @@ module.exports = (() => {
824
824
  return map;
825
825
  }, { });
826
826
 
827
+ this._symbolsDisplay = this._items.reduce((map, item) => {
828
+ const symbol = extractSymbolForDisplay(item.position);
829
+
830
+ if (symbol) {
831
+ if (!map.hasOwnProperty(symbol)) {
832
+ map[symbol] = [ ];
833
+ }
834
+
835
+ map[symbol].push(item);
836
+ }
837
+
838
+ return map;
839
+ }, { });
840
+
827
841
  this._currencies = this._items.reduce((map, item) => {
828
842
  const position = item.position;
829
843
 
@@ -935,10 +949,14 @@ module.exports = (() => {
935
949
  }, { });
936
950
  }
937
951
 
938
- get defaultCurrency() {
939
- return this._defaultCurrency;
940
- }
941
-
952
+ /**
953
+ * Returns a distinct list of all symbols used by the positions
954
+ * within the container.
955
+ *
956
+ * @public
957
+ * @param {Boolean} display - If true, all "display" symbols are returned; otherwise Barchart symbols are returned.
958
+ * @returns {Array.<String>}
959
+ */
942
960
  getPositionSymbols(display) {
943
961
  const symbols = this._items.reduce((symbols, item) => {
944
962
  const position = item.position;
@@ -961,6 +979,14 @@ module.exports = (() => {
961
979
  return array.unique(symbols);
962
980
  }
963
981
 
982
+ /**
983
+ * Updates the quote for a single symbol; causing updates to any grouping
984
+ * level that contains the position(s) for the symbol.
985
+ *
986
+ * @public
987
+ * @param {String} symbol
988
+ * @param {Object} quote
989
+ */
964
990
  setPositionQuote(symbol, quote) {
965
991
  assert.argumentIsRequired(symbol, 'symbol', String);
966
992
  assert.argumentIsRequired(quote, 'quote', Object);
@@ -970,14 +996,34 @@ module.exports = (() => {
970
996
  }
971
997
  }
972
998
 
999
+ /**
1000
+ * Returns all forex symbols that are required to do currency translations.
1001
+ *
1002
+ * @public
1003
+ * @returns {Array.<String>}
1004
+ */
973
1005
  getForexSymbols() {
974
1006
  return this._forexSymbols;
975
1007
  }
976
1008
 
1009
+ /**
1010
+ * Returns all current forex quotes.
1011
+ *
1012
+ * @public
1013
+ * @returns {Array.<Object>}
1014
+ */
977
1015
  getForexQuotes() {
978
1016
  return this._forexQuotes;
979
1017
  }
980
1018
 
1019
+ /**
1020
+ * Updates the forex quote for a single currency pair; causing updates to
1021
+ * any grouping level that contains that requires translation.
1022
+ *
1023
+ * @public
1024
+ * @param {String} symbol
1025
+ * @param {Object} quote
1026
+ */
981
1027
  setForexQuote(symbol, quote) {
982
1028
  assert.argumentIsRequired(symbol, 'symbol', String);
983
1029
  assert.argumentIsRequired(quote, 'quote', Object);
@@ -995,10 +1041,50 @@ module.exports = (() => {
995
1041
  Object.keys(this._trees).forEach(key => this._trees[key].walk(group => group.setForexRate(rate), true, false));
996
1042
  }
997
1043
 
998
- setPositionFundamentals(data) {
1044
+ /**
1045
+ * Updates fundamental data for a single symbol.
1046
+ *
1047
+ * @public
1048
+ * @param {String} symbol
1049
+ * @param {Object} data
1050
+ */
1051
+ setPositionFundamentalData(symbol, data) {
999
1052
  return;
1000
1053
  }
1001
1054
 
1055
+ /**
1056
+ * Indicates if a news article exists for a symbol.
1057
+ *
1058
+ * @public
1059
+ * @param {String} symbol
1060
+ * @param {Boolean} display
1061
+ * @param {Boolean} exists
1062
+ */
1063
+ setNewsArticleExists(symbol, display, exists) {
1064
+ assert.argumentIsRequired(symbol, 'symbol', String);
1065
+ assert.argumentIsRequired(display, 'display', Boolean);
1066
+ assert.argumentIsRequired(exists, 'exists', Boolean);
1067
+
1068
+ let map;
1069
+
1070
+ if (display) {
1071
+ map = this._symbols;
1072
+ } else {
1073
+ map = this._symbolsDisplay;
1074
+ }
1075
+
1076
+ if (map.hasOwnProperty(symbol)) {
1077
+ map[symbol].forEach(item => item.setNewsArticleExists(exists));
1078
+ }
1079
+ }
1080
+
1081
+ /**
1082
+ * Returns a single level of grouping from one of the internal trees.
1083
+ *
1084
+ * @param {String} name
1085
+ * @param {Array.<String> keys
1086
+ * @returns {PositionGroup}
1087
+ */
1002
1088
  getGroup(name, keys) {
1003
1089
  assert.argumentIsRequired(name, 'name', String);
1004
1090
  assert.argumentIsArray(keys, 'keys', Number);
@@ -1006,6 +1092,14 @@ module.exports = (() => {
1006
1092
  return findNode(this._trees[name], keys).getValue();
1007
1093
  }
1008
1094
 
1095
+ /**
1096
+ * Returns all child groups from a level of grouping within one of
1097
+ * the internal trees.
1098
+ *
1099
+ * @param {String} name
1100
+ * @param {Array.<String> keys
1101
+ * @returns {Array.<PositionGroup>}
1102
+ */
1009
1103
  getGroups(name, keys) {
1010
1104
  assert.argumentIsRequired(name, 'name', String);
1011
1105
  assert.argumentIsArray(keys, 'keys', Number);
@@ -1017,8 +1111,6 @@ module.exports = (() => {
1017
1111
  assert.argumentIsRequired(name, 'name', String);
1018
1112
  assert.argumentIsRequired(executor, 'executor', Function);
1019
1113
 
1020
- assert.argumentIsRequired(executor, 'executor', Function);
1021
-
1022
1114
  this._trees[name].walk(group => group.setSuspended(true), false, false);
1023
1115
 
1024
1116
  executor(this);
@@ -1072,7 +1164,17 @@ module.exports = (() => {
1072
1164
  'use strict';
1073
1165
 
1074
1166
  /**
1167
+ * A grouping of {@link PositionItem} instances. The group aggregates from across
1168
+ * all the positions and performs currency translation, as necessary.
1169
+ *
1075
1170
  * @public
1171
+ * @param {PositionContainer} container
1172
+ * @param {PositionGroup|null} parent
1173
+ * @param {Array.<PositionItem>} items
1174
+ * @param {Currency} currency
1175
+ * @param {String} key
1176
+ * @param {String} description
1177
+ * @param {Boolean=} single
1076
1178
  */
1077
1179
  class PositionGroup {
1078
1180
  constructor(container, parent, items, currency, key, description, single) {
@@ -1092,15 +1194,22 @@ module.exports = (() => {
1092
1194
  this._suspended = false;
1093
1195
 
1094
1196
  this._marketPercentChangeEvent = new Event(this);
1197
+ this._excludedChangeEvent = new Event(this);
1095
1198
 
1096
1199
  this._dataFormat = { };
1097
1200
  this._dataActual = { };
1098
1201
 
1099
1202
  this._dataFormat.key = this._key;
1100
1203
  this._dataFormat.description = this._description;
1101
- this._dataFormat.c = this._currency.code;
1102
-
1204
+ this._dataFormat.newsExists = false;
1103
1205
  this._dataFormat.quantity = null;
1206
+ this._dataFormat.basisPrice = null;
1207
+
1208
+ this._dataActual.key = this._key;
1209
+ this._dataActual.description = this._description;
1210
+ this._dataActual.newsExists = false;
1211
+ this._dataActual.quantity = null;
1212
+ this._dataActual.basisPrice = null;
1104
1213
 
1105
1214
  if (this._single) {
1106
1215
  const item = items[0];
@@ -1179,35 +1288,85 @@ module.exports = (() => {
1179
1288
 
1180
1289
  calculatePriceData(this, this._container.getForexQuotes(), sender, false);
1181
1290
  });
1291
+
1292
+ if (this._single) {
1293
+ item.registerNewsExistsChangeHandler((exists, sender) => {
1294
+ this._dataFormat.newsExists = exists;
1295
+ this._dataFormat.newsExists = exists;
1296
+ });
1297
+ }
1182
1298
  });
1183
1299
 
1184
1300
  this.refresh();
1185
1301
  }
1186
1302
 
1303
+ /**
1304
+ * The key of the group.
1305
+ *
1306
+ * @public
1307
+ * @returns {String}
1308
+ */
1187
1309
  get key() {
1188
1310
  return this._key;
1189
1311
  }
1190
1312
 
1313
+ /**
1314
+ * The description of the group.
1315
+ *
1316
+ * @public
1317
+ * @returns {String}
1318
+ */
1191
1319
  get description() {
1192
1320
  return this._description;
1193
1321
  }
1194
1322
 
1323
+ /**
1324
+ * The {@link Currency} which all aggregated data is presented in.
1325
+ *
1326
+ * @public
1327
+ * @returns {Currency}
1328
+ */
1195
1329
  get currency() {
1196
1330
  return this._currency;
1197
1331
  }
1198
1332
 
1333
+ /**
1334
+ * The {@link PositionItem} instances which for which aggregated data is compiled.
1335
+ *
1336
+ * @public
1337
+ * @returns {Currency}
1338
+ */
1199
1339
  get items() {
1200
1340
  return this._items;
1201
1341
  }
1202
1342
 
1343
+ /**
1344
+ * The string-based, human-readable aggregated data for the group.
1345
+ *
1346
+ * @public
1347
+ * @returns {Object}
1348
+ */
1203
1349
  get data() {
1204
1350
  return this._dataFormat;
1205
1351
  }
1206
1352
 
1353
+ /**
1354
+ * The raw aggregated data for the group (typically {@link Decimal} instances).
1355
+ *
1356
+ * @public
1357
+ * @returns {Object}
1358
+ */
1207
1359
  get actual() {
1208
1360
  return this._dataActual;
1209
1361
  }
1210
1362
 
1363
+ /**
1364
+ * Indicates if the group will only contain one {@link PositionItem} -- that is,
1365
+ * indicates if the group represents a single position.
1366
+ *
1367
+ * @public
1368
+ * @returns {Boolean}
1369
+ */
1211
1370
  get single() {
1212
1371
  return this._single;
1213
1372
  }
@@ -1216,10 +1375,22 @@ module.exports = (() => {
1216
1375
  return this._suspended;
1217
1376
  }
1218
1377
 
1378
+ /**
1379
+ * Indicates if the group should be excluded from higher-level aggregations.
1380
+ *
1381
+ * @public
1382
+ * @returns {Boolean}
1383
+ */
1219
1384
  get excluded() {
1220
1385
  return this._excluded;
1221
1386
  }
1222
1387
 
1388
+ /**
1389
+ * Causes aggregated data to be recalculated using a new exchange rate.
1390
+ *
1391
+ * @public
1392
+ * @param {Rate} rate
1393
+ */
1223
1394
  setForexRate(rate) {
1224
1395
  if (!this._bypassCurrencyTranslation) {
1225
1396
  this.refresh();
@@ -1230,11 +1401,7 @@ module.exports = (() => {
1230
1401
  assert.argumentIsRequired(value, 'value', Boolean);
1231
1402
 
1232
1403
  if (this._excluded !== value) {
1233
- this._container.startTransaction(() => {
1234
- this._items.forEach((item) => {
1235
- item.setExcluded(value);
1236
- });
1237
- });
1404
+ this._excludedChangeEvent(this._excluded = value);
1238
1405
  }
1239
1406
  }
1240
1407
 
@@ -1248,6 +1415,11 @@ module.exports = (() => {
1248
1415
  }
1249
1416
  }
1250
1417
 
1418
+ /**
1419
+ * Causes all aggregated data to be recalculated.
1420
+ *
1421
+ * @public
1422
+ */
1251
1423
  refresh() {
1252
1424
  const rates = this._container.getForexQuotes();
1253
1425
 
@@ -1255,6 +1427,12 @@ module.exports = (() => {
1255
1427
  calculatePriceData(this, rates, null, true);
1256
1428
  }
1257
1429
 
1430
+ /**
1431
+ * Causes the percent of the position, with respect to the parent container's
1432
+ * total, to be recalculated.
1433
+ *
1434
+ * @public
1435
+ */
1258
1436
  refreshMarketPercent() {
1259
1437
  calculateMarketPercent(this, this._container.getForexQuotes(), true);
1260
1438
  }
@@ -1308,7 +1486,7 @@ module.exports = (() => {
1308
1486
 
1309
1487
  const items = group._items;
1310
1488
 
1311
- group._bypassCurrencyTranslation = items.some(item => item.currency !== currency);
1489
+ group._bypassCurrencyTranslation = items.every(item => item.currency === currency);
1312
1490
 
1313
1491
  const translate = (item, value) => {
1314
1492
  let translated;
@@ -1360,8 +1538,11 @@ module.exports = (() => {
1360
1538
  if (group.single) {
1361
1539
  const item = group._items[0];
1362
1540
 
1363
- format.quantity = formatDecimal(item.position.snapshot.open, 2);
1364
- format.basisPrice = formatCurrency(item.data.basisPrice, currency);
1541
+ actual.quantity = item.position.snapshot.open;
1542
+ actual.basisPrice = item.data.basisPrice;
1543
+
1544
+ format.quantity = formatDecimal(actual.quantity, 2);
1545
+ format.basisPrice = formatCurrency(actual.basisPrice, currency);
1365
1546
  }
1366
1547
  }
1367
1548
 
@@ -1535,7 +1716,15 @@ module.exports = (() => {
1535
1716
  'use strict';
1536
1717
 
1537
1718
  /**
1719
+ * A container for a single position, which handles quote changes and
1720
+ * notifies observers -- which are typically parent-level {@link PositionGroup}
1721
+ * instances.
1722
+ *
1538
1723
  * @public
1724
+ * @param {Object} portfolio
1725
+ * @param {Object} position
1726
+ * @param {Object} currentSummary
1727
+ * @param {Array.<Object>} previousSummaries
1539
1728
  */
1540
1729
  class PositionItem {
1541
1730
  constructor(portfolio, position, currentSummary, previousSummaries) {
@@ -1551,10 +1740,12 @@ module.exports = (() => {
1551
1740
  this._data.basis = null;
1552
1741
 
1553
1742
  this._currentQuote = null;
1554
- this._previousQuote = null;
1743
+
1744
+ this._currentPrice = null;
1745
+ this._previousPrice = null;
1555
1746
 
1556
1747
  this._data.currentPrice = null;
1557
- this._data.previousPrice = null;
1748
+ this._data.currentPricePrevious = null;
1558
1749
 
1559
1750
  this._data.market = null;
1560
1751
  this._data.marketChange = null;
@@ -1574,74 +1765,141 @@ module.exports = (() => {
1574
1765
  this._data.income = null;
1575
1766
  this._data.basisPrice = null;
1576
1767
 
1577
- this._excluded = false;
1768
+ this._data.newsExists = false;
1578
1769
 
1579
1770
  calculateStaticData(this);
1580
1771
  calculatePriceData(this, null);
1581
1772
 
1582
1773
  this._quoteChangedEvent = new Event(this);
1583
- this._excludedChangeEvent = new Event(this);
1774
+ this._newsExistsChangedEvent = new Event(this);
1584
1775
  }
1585
1776
 
1777
+ /**
1778
+ * The portfolio of the encapsulated position.
1779
+ *
1780
+ * @public
1781
+ * @returns {Object}
1782
+ */
1586
1783
  get portfolio() {
1587
1784
  return this._portfolio;
1588
1785
  }
1589
1786
 
1787
+ /**
1788
+ * The encapsulated position.
1789
+ *
1790
+ * @public
1791
+ * @returns {Object}
1792
+ */
1590
1793
  get position() {
1591
1794
  return this._position;
1592
1795
  }
1593
1796
 
1797
+ /**
1798
+ * The {@link Currency} of the encapsulated position.
1799
+ *
1800
+ * @public
1801
+ * @returns {Object}
1802
+ */
1594
1803
  get currency() {
1595
1804
  return this._currency;
1596
1805
  }
1597
1806
 
1807
+ /**
1808
+ * The year-to-date position summary of the encapsulated position.
1809
+ *
1810
+ * @public
1811
+ * @returns {Object}
1812
+ */
1598
1813
  get currentSummary() {
1599
1814
  return this._currentSummary;
1600
1815
  }
1601
-
1816
+
1817
+ /**
1818
+ * Previous year's summaries for the encapsulated position.
1819
+ *
1820
+ * @public
1821
+ * @returns {Object}
1822
+ */
1602
1823
  get previousSummaries() {
1603
1824
  return this._previousSummaries;
1604
1825
  }
1605
1826
 
1827
+ /**
1828
+ * Various data regarding the encapsulated position.
1829
+ *
1830
+ * @public
1831
+ * @returns {*}
1832
+ */
1606
1833
  get data() {
1607
1834
  return this._data;
1608
1835
  }
1609
1836
 
1837
+ /**
1838
+ * The current quote for the symbol of the encapsulated position.
1839
+ *
1840
+ * @public
1841
+ * @returns {null|{Object}}
1842
+ */
1610
1843
  get quote() {
1611
1844
  return this._currentQuote;
1612
1845
  }
1613
1846
 
1614
- get excluded() {
1615
- return this._excluded;
1616
- }
1617
-
1847
+ /**
1848
+ * Sets the current quote -- causing position-level data (e.g. market value) to
1849
+ * be recalculated.
1850
+ *
1851
+ * @public
1852
+ * @param {Object} quote
1853
+ */
1618
1854
  setQuote(quote) {
1619
1855
  assert.argumentIsRequired(quote, 'quote', Object);
1620
1856
 
1621
- if (this._previousQuote === null || this._previousQuote.lastPrice !== quote.lastPrice) {
1857
+ if (this._currentPricePrevious !== quote.lastPrice) {
1622
1858
  calculatePriceData(this, quote.lastPrice);
1623
1859
 
1624
- this._previousQuote = this._currentQuote;
1860
+ this._currentPricePrevious = this._currentPrice;
1861
+ this._currentPrice = quote.lastPrice;
1862
+
1625
1863
  this._currentQuote = quote;
1626
1864
 
1627
1865
  this._quoteChangedEvent.fire(this._currentQuote);
1628
1866
  }
1629
1867
  }
1630
1868
 
1631
- setExcluded(value) {
1869
+ /**
1870
+ * Sets a flag which indicates if news article(s) exist for the encapsulated position's
1871
+ * symbol.
1872
+ *
1873
+ * @public
1874
+ * @param {Boolean} value
1875
+ */
1876
+ setNewsArticleExists(value) {
1632
1877
  assert.argumentIsRequired(value, 'value', Boolean);
1633
1878
 
1634
- if (this._excluded !== value) {
1635
- this._excludedChangeEvent.fire(this._excluded = value);
1879
+ if (this._data.newsExists !== value) {
1880
+ this._newsExistsChangedEvent.fire(this._data.newsExists = value);
1636
1881
  }
1637
1882
  }
1638
1883
 
1884
+ /**
1885
+ * Registers an observer for quote changes, which is fired after internal recalculations
1886
+ * of position data are complete.
1887
+ *
1888
+ * @public
1889
+ * @param {Function} handler
1890
+ */
1639
1891
  registerQuoteChangeHandler(handler) {
1640
1892
  this._quoteChangedEvent.register(handler);
1641
1893
  }
1642
1894
 
1643
- registerExcludedChangeHandler(handler) {
1644
- this._excludedChangeEvent.register(handler);
1895
+ /**
1896
+ * Registers an observer changes to the status of news existence.
1897
+ *
1898
+ * @public
1899
+ * @param {Function} handler
1900
+ */
1901
+ registerNewsExistsChangeHandler(handler) {
1902
+ this._newsExistsChangedEvent.register(handler);
1645
1903
  }
1646
1904
 
1647
1905
  toString() {