@barchart/portfolio-api-common 1.0.129 → 1.0.133

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,55 @@ 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) {
283
- return;
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) {
336
+ assert.argumentIsRequired(symbol, 'symbol', String);
337
+ assert.argumentIsRequired(data, 'data', Object);
338
+
339
+ if (this._symbols.hasOwnProperty(symbol)) {
340
+ this._symbols[symbol].forEach(item => item.setPositionFundamentalData(data));
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Indicates if a news article exists for a symbol.
346
+ *
347
+ * @public
348
+ * @param {String} symbol
349
+ * @param {Boolean} display
350
+ * @param {Boolean} exists
351
+ */
352
+ setNewsArticleExists(symbol, display, exists) {
353
+ assert.argumentIsRequired(symbol, 'symbol', String);
354
+ assert.argumentIsRequired(display, 'display', Boolean);
355
+ assert.argumentIsRequired(exists, 'exists', Boolean);
356
+
357
+ let map;
358
+
359
+ if (display) {
360
+ map = this._symbols;
361
+ } else {
362
+ map = this._symbolsDisplay;
363
+ }
364
+
365
+ if (map.hasOwnProperty(symbol)) {
366
+ map[symbol].forEach(item => item.setNewsArticleExists(exists));
367
+ }
284
368
  }
285
369
 
370
+ /**
371
+ * Returns a single level of grouping from one of the internal trees.
372
+ *
373
+ * @param {String} name
374
+ * @param {Array.<String> keys
375
+ * @returns {PositionGroup}
376
+ */
286
377
  getGroup(name, keys) {
287
378
  assert.argumentIsRequired(name, 'name', String);
288
379
  assert.argumentIsArray(keys, 'keys', Number);
@@ -290,6 +381,14 @@ module.exports = (() => {
290
381
  return findNode(this._trees[name], keys).getValue();
291
382
  }
292
383
 
384
+ /**
385
+ * Returns all child groups from a level of grouping within one of
386
+ * the internal trees.
387
+ *
388
+ * @param {String} name
389
+ * @param {Array.<String> keys
390
+ * @returns {Array.<PositionGroup>}
391
+ */
293
392
  getGroups(name, keys) {
294
393
  assert.argumentIsRequired(name, 'name', String);
295
394
  assert.argumentIsArray(keys, 'keys', Number);
@@ -301,8 +400,6 @@ module.exports = (() => {
301
400
  assert.argumentIsRequired(name, 'name', String);
302
401
  assert.argumentIsRequired(executor, 'executor', Function);
303
402
 
304
- assert.argumentIsRequired(executor, 'executor', Function);
305
-
306
403
  this._trees[name].walk(group => group.setSuspended(true), false, false);
307
404
 
308
405
  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];
@@ -46,10 +63,12 @@ module.exports = (() => {
46
63
  this._dataFormat.portfolio = item.portfolio.portfolio;
47
64
  this._dataFormat.position = item.position.position;
48
65
  this._dataFormat.instrument = item.position.instrument;
66
+ this._dataFormat.fundamental = item.fundamental || { };
49
67
  } else {
50
68
  this._dataFormat.portfolio = null;
51
69
  this._dataFormat.position = null;
52
70
  this._dataFormat.instrument = null;
71
+ this._dataFormat.fundamental = { };
53
72
  }
54
73
 
55
74
  this._dataFormat.quoteLast = null;
@@ -117,35 +136,89 @@ module.exports = (() => {
117
136
 
118
137
  calculatePriceData(this, this._container.getForexQuotes(), sender, false);
119
138
  });
139
+
140
+ if (this._single) {
141
+ item.registerNewsExistsChangeHandler((exists, sender) => {
142
+ this._dataActual.newsExists = exists;
143
+ this._dataFormat.newsExists = exists;
144
+ });
145
+
146
+ item._fundamentalDataChangeEvent((data, sender) => {
147
+ this._dataFormat.fundamental = data;
148
+ });
149
+ }
120
150
  });
121
151
 
122
152
  this.refresh();
123
153
  }
124
154
 
155
+ /**
156
+ * The key of the group.
157
+ *
158
+ * @public
159
+ * @returns {String}
160
+ */
125
161
  get key() {
126
162
  return this._key;
127
163
  }
128
164
 
165
+ /**
166
+ * The description of the group.
167
+ *
168
+ * @public
169
+ * @returns {String}
170
+ */
129
171
  get description() {
130
172
  return this._description;
131
173
  }
132
174
 
175
+ /**
176
+ * The {@link Currency} which all aggregated data is presented in.
177
+ *
178
+ * @public
179
+ * @returns {Currency}
180
+ */
133
181
  get currency() {
134
182
  return this._currency;
135
183
  }
136
184
 
185
+ /**
186
+ * The {@link PositionItem} instances which for which aggregated data is compiled.
187
+ *
188
+ * @public
189
+ * @returns {Currency}
190
+ */
137
191
  get items() {
138
192
  return this._items;
139
193
  }
140
194
 
195
+ /**
196
+ * The string-based, human-readable aggregated data for the group.
197
+ *
198
+ * @public
199
+ * @returns {Object}
200
+ */
141
201
  get data() {
142
202
  return this._dataFormat;
143
203
  }
144
204
 
205
+ /**
206
+ * The raw aggregated data for the group (typically {@link Decimal} instances).
207
+ *
208
+ * @public
209
+ * @returns {Object}
210
+ */
145
211
  get actual() {
146
212
  return this._dataActual;
147
213
  }
148
214
 
215
+ /**
216
+ * Indicates if the group will only contain one {@link PositionItem} -- that is,
217
+ * indicates if the group represents a single position.
218
+ *
219
+ * @public
220
+ * @returns {Boolean}
221
+ */
149
222
  get single() {
150
223
  return this._single;
151
224
  }
@@ -154,10 +227,22 @@ module.exports = (() => {
154
227
  return this._suspended;
155
228
  }
156
229
 
230
+ /**
231
+ * Indicates if the group should be excluded from higher-level aggregations.
232
+ *
233
+ * @public
234
+ * @returns {Boolean}
235
+ */
157
236
  get excluded() {
158
237
  return this._excluded;
159
238
  }
160
239
 
240
+ /**
241
+ * Causes aggregated data to be recalculated using a new exchange rate.
242
+ *
243
+ * @public
244
+ * @param {Rate} rate
245
+ */
161
246
  setForexRate(rate) {
162
247
  if (!this._bypassCurrencyTranslation) {
163
248
  this.refresh();
@@ -168,11 +253,7 @@ module.exports = (() => {
168
253
  assert.argumentIsRequired(value, 'value', Boolean);
169
254
 
170
255
  if (this._excluded !== value) {
171
- this._container.startTransaction(() => {
172
- this._items.forEach((item) => {
173
- item.setExcluded(value);
174
- });
175
- });
256
+ this._excludedChangeEvent(this._excluded = value);
176
257
  }
177
258
  }
178
259
 
@@ -186,6 +267,11 @@ module.exports = (() => {
186
267
  }
187
268
  }
188
269
 
270
+ /**
271
+ * Causes all aggregated data to be recalculated.
272
+ *
273
+ * @public
274
+ */
189
275
  refresh() {
190
276
  const rates = this._container.getForexQuotes();
191
277
 
@@ -193,6 +279,12 @@ module.exports = (() => {
193
279
  calculatePriceData(this, rates, null, true);
194
280
  }
195
281
 
282
+ /**
283
+ * Causes the percent of the position, with respect to the parent container's
284
+ * total, to be recalculated.
285
+ *
286
+ * @public
287
+ */
196
288
  refreshMarketPercent() {
197
289
  calculateMarketPercent(this, this._container.getForexQuotes(), true);
198
290
  }
@@ -298,8 +390,11 @@ module.exports = (() => {
298
390
  if (group.single) {
299
391
  const item = group._items[0];
300
392
 
301
- format.quantity = formatDecimal(item.position.snapshot.open, 2);
302
- format.basisPrice = formatCurrency(item.data.basisPrice, currency);
393
+ actual.quantity = item.position.snapshot.open;
394
+ actual.basisPrice = item.data.basisPrice;
395
+
396
+ format.quantity = formatDecimal(actual.quantity, 2);
397
+ format.basisPrice = formatCurrency(actual.basisPrice, currency);
303
398
  }
304
399
  }
305
400
 
@@ -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;
@@ -40,7 +50,7 @@ module.exports = (() => {
40
50
 
41
51
  this._data.unrealized = null;
42
52
  this._data.unrealizedChange = null;
43
-
53
+
44
54
  this._data.summaryTotalCurrent = null;
45
55
  this._data.summaryTotalCurrentChange = null;
46
56
 
@@ -50,74 +60,165 @@ 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;
64
+ this._data.fundamental = { };
54
65
 
55
66
  calculateStaticData(this);
56
67
  calculatePriceData(this, null);
57
68
 
58
69
  this._quoteChangedEvent = new Event(this);
59
- this._excludedChangeEvent = new Event(this);
70
+ this._newsExistsChangedEvent = new Event(this);
71
+ this._fundamentalDataChangeEvent = new Event(this);
60
72
  }
61
73
 
74
+ /**
75
+ * The portfolio of the encapsulated position.
76
+ *
77
+ * @public
78
+ * @returns {Object}
79
+ */
62
80
  get portfolio() {
63
81
  return this._portfolio;
64
82
  }
65
83
 
84
+ /**
85
+ * The encapsulated position.
86
+ *
87
+ * @public
88
+ * @returns {Object}
89
+ */
66
90
  get position() {
67
91
  return this._position;
68
92
  }
69
93
 
94
+ /**
95
+ * The {@link Currency} of the encapsulated position.
96
+ *
97
+ * @public
98
+ * @returns {Object}
99
+ */
70
100
  get currency() {
71
101
  return this._currency;
72
102
  }
73
103
 
104
+ /**
105
+ * The year-to-date position summary of the encapsulated position.
106
+ *
107
+ * @public
108
+ * @returns {Object}
109
+ */
74
110
  get currentSummary() {
75
111
  return this._currentSummary;
76
112
  }
77
-
113
+
114
+ /**
115
+ * Previous year's summaries for the encapsulated position.
116
+ *
117
+ * @public
118
+ * @returns {Object}
119
+ */
78
120
  get previousSummaries() {
79
121
  return this._previousSummaries;
80
122
  }
81
123
 
124
+ /**
125
+ * Various data regarding the encapsulated position.
126
+ *
127
+ * @public
128
+ * @returns {*}
129
+ */
82
130
  get data() {
83
131
  return this._data;
84
132
  }
85
133
 
134
+ /**
135
+ * The current quote for the symbol of the encapsulated position.
136
+ *
137
+ * @public
138
+ * @returns {null|{Object}}
139
+ */
86
140
  get quote() {
87
141
  return this._currentQuote;
88
142
  }
89
143
 
90
- get excluded() {
91
- return this._excluded;
92
- }
93
-
144
+ /**
145
+ * Sets the current quote -- causing position-level data (e.g. market value) to
146
+ * be recalculated.
147
+ *
148
+ * @public
149
+ * @param {Object} quote
150
+ */
94
151
  setQuote(quote) {
95
152
  assert.argumentIsRequired(quote, 'quote', Object);
96
153
 
97
- if (this._previousQuote === null || this._previousQuote.lastPrice !== quote.lastPrice) {
154
+ if (this._currentPricePrevious !== quote.lastPrice) {
98
155
  calculatePriceData(this, quote.lastPrice);
99
156
 
100
- this._previousQuote = this._currentQuote;
157
+ this._currentPricePrevious = this._currentPrice;
158
+ this._currentPrice = quote.lastPrice;
159
+
101
160
  this._currentQuote = quote;
102
161
 
103
162
  this._quoteChangedEvent.fire(this._currentQuote);
104
163
  }
105
164
  }
106
165
 
107
- setExcluded(value) {
166
+ /**
167
+ * Sets fundamental data for the position.
168
+ *
169
+ * @public
170
+ * @param {Object} data
171
+ */
172
+ setPositionFundamentalData(data) {
173
+ assert.argumentIsRequired(data, 'data', Object);
174
+
175
+ this._fundamentalDataChangeEvent(this._data.fundamental = data);
176
+ }
177
+
178
+ /**
179
+ * Sets a flag which indicates if news article(s) exist for the encapsulated position's
180
+ * symbol.
181
+ *
182
+ * @public
183
+ * @param {Boolean} value
184
+ */
185
+ setNewsArticleExists(value) {
108
186
  assert.argumentIsRequired(value, 'value', Boolean);
109
187
 
110
- if (this._excluded !== value) {
111
- this._excludedChangeEvent.fire(this._excluded = value);
188
+ if (this._data.newsExists !== value) {
189
+ this._newsExistsChangedEvent.fire(this._data.newsExists = value);
112
190
  }
113
191
  }
114
192
 
193
+ /**
194
+ * Registers an observer for quote changes, which is fired after internal recalculations
195
+ * of position data are complete.
196
+ *
197
+ * @public
198
+ * @param {Function} handler
199
+ */
115
200
  registerQuoteChangeHandler(handler) {
116
201
  this._quoteChangedEvent.register(handler);
117
202
  }
118
203
 
119
- registerExcludedChangeHandler(handler) {
120
- this._excludedChangeEvent.register(handler);
204
+ /**
205
+ * Registers an observer for fundamental data changes.
206
+ *
207
+ * @public
208
+ * @param {Function} handler
209
+ */
210
+ registerFundamentalDataChangeHandler(handler) {
211
+ this._fundamentalDataChangeEvent.register(handler);
212
+ }
213
+
214
+ /**
215
+ * Registers an observer changes to the status of news existence.
216
+ *
217
+ * @public
218
+ * @param {Function} handler
219
+ */
220
+ registerNewsExistsChangeHandler(handler) {
221
+ this._newsExistsChangedEvent.register(handler);
121
222
  }
122
223
 
123
224
  toString() {
@@ -244,10 +345,10 @@ module.exports = (() => {
244
345
  data.unrealizedChange = Decimal.ZERO;
245
346
  }
246
347
  }
247
-
348
+
248
349
  function calculateSummaryTotal(summary) {
249
350
  let returnRef;
250
-
351
+
251
352
  if (summary) {
252
353
  const period = summary.period;
253
354
 
@@ -255,7 +356,7 @@ module.exports = (() => {
255
356
  } else {
256
357
  returnRef = Decimal.ZERO;
257
358
  }
258
-
359
+
259
360
  return returnRef;
260
361
  }
261
362
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barchart/portfolio-api-common",
3
- "version": "1.0.129",
3
+ "version": "1.0.133",
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,55 @@ 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) {
999
- return;
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) {
1052
+ assert.argumentIsRequired(symbol, 'symbol', String);
1053
+ assert.argumentIsRequired(data, 'data', Object);
1054
+
1055
+ if (this._symbols.hasOwnProperty(symbol)) {
1056
+ this._symbols[symbol].forEach(item => item.setPositionFundamentalData(data));
1057
+ }
1058
+ }
1059
+
1060
+ /**
1061
+ * Indicates if a news article exists for a symbol.
1062
+ *
1063
+ * @public
1064
+ * @param {String} symbol
1065
+ * @param {Boolean} display
1066
+ * @param {Boolean} exists
1067
+ */
1068
+ setNewsArticleExists(symbol, display, exists) {
1069
+ assert.argumentIsRequired(symbol, 'symbol', String);
1070
+ assert.argumentIsRequired(display, 'display', Boolean);
1071
+ assert.argumentIsRequired(exists, 'exists', Boolean);
1072
+
1073
+ let map;
1074
+
1075
+ if (display) {
1076
+ map = this._symbols;
1077
+ } else {
1078
+ map = this._symbolsDisplay;
1079
+ }
1080
+
1081
+ if (map.hasOwnProperty(symbol)) {
1082
+ map[symbol].forEach(item => item.setNewsArticleExists(exists));
1083
+ }
1000
1084
  }
1001
1085
 
1086
+ /**
1087
+ * Returns a single level of grouping from one of the internal trees.
1088
+ *
1089
+ * @param {String} name
1090
+ * @param {Array.<String> keys
1091
+ * @returns {PositionGroup}
1092
+ */
1002
1093
  getGroup(name, keys) {
1003
1094
  assert.argumentIsRequired(name, 'name', String);
1004
1095
  assert.argumentIsArray(keys, 'keys', Number);
@@ -1006,6 +1097,14 @@ module.exports = (() => {
1006
1097
  return findNode(this._trees[name], keys).getValue();
1007
1098
  }
1008
1099
 
1100
+ /**
1101
+ * Returns all child groups from a level of grouping within one of
1102
+ * the internal trees.
1103
+ *
1104
+ * @param {String} name
1105
+ * @param {Array.<String> keys
1106
+ * @returns {Array.<PositionGroup>}
1107
+ */
1009
1108
  getGroups(name, keys) {
1010
1109
  assert.argumentIsRequired(name, 'name', String);
1011
1110
  assert.argumentIsArray(keys, 'keys', Number);
@@ -1017,8 +1116,6 @@ module.exports = (() => {
1017
1116
  assert.argumentIsRequired(name, 'name', String);
1018
1117
  assert.argumentIsRequired(executor, 'executor', Function);
1019
1118
 
1020
- assert.argumentIsRequired(executor, 'executor', Function);
1021
-
1022
1119
  this._trees[name].walk(group => group.setSuspended(true), false, false);
1023
1120
 
1024
1121
  executor(this);
@@ -1072,7 +1169,17 @@ module.exports = (() => {
1072
1169
  'use strict';
1073
1170
 
1074
1171
  /**
1172
+ * A grouping of {@link PositionItem} instances. The group aggregates from across
1173
+ * all the positions and performs currency translation, as necessary.
1174
+ *
1075
1175
  * @public
1176
+ * @param {PositionContainer} container
1177
+ * @param {PositionGroup|null} parent
1178
+ * @param {Array.<PositionItem>} items
1179
+ * @param {Currency} currency
1180
+ * @param {String} key
1181
+ * @param {String} description
1182
+ * @param {Boolean=} single
1076
1183
  */
1077
1184
  class PositionGroup {
1078
1185
  constructor(container, parent, items, currency, key, description, single) {
@@ -1092,15 +1199,22 @@ module.exports = (() => {
1092
1199
  this._suspended = false;
1093
1200
 
1094
1201
  this._marketPercentChangeEvent = new Event(this);
1202
+ this._excludedChangeEvent = new Event(this);
1095
1203
 
1096
1204
  this._dataFormat = { };
1097
1205
  this._dataActual = { };
1098
1206
 
1099
1207
  this._dataFormat.key = this._key;
1100
1208
  this._dataFormat.description = this._description;
1101
- this._dataFormat.c = this._currency.code;
1102
-
1209
+ this._dataFormat.newsExists = false;
1103
1210
  this._dataFormat.quantity = null;
1211
+ this._dataFormat.basisPrice = null;
1212
+
1213
+ this._dataActual.key = this._key;
1214
+ this._dataActual.description = this._description;
1215
+ this._dataActual.newsExists = false;
1216
+ this._dataActual.quantity = null;
1217
+ this._dataActual.basisPrice = null;
1104
1218
 
1105
1219
  if (this._single) {
1106
1220
  const item = items[0];
@@ -1108,10 +1222,12 @@ module.exports = (() => {
1108
1222
  this._dataFormat.portfolio = item.portfolio.portfolio;
1109
1223
  this._dataFormat.position = item.position.position;
1110
1224
  this._dataFormat.instrument = item.position.instrument;
1225
+ this._dataFormat.fundamental = item.fundamental || { };
1111
1226
  } else {
1112
1227
  this._dataFormat.portfolio = null;
1113
1228
  this._dataFormat.position = null;
1114
1229
  this._dataFormat.instrument = null;
1230
+ this._dataFormat.fundamental = { };
1115
1231
  }
1116
1232
 
1117
1233
  this._dataFormat.quoteLast = null;
@@ -1179,35 +1295,89 @@ module.exports = (() => {
1179
1295
 
1180
1296
  calculatePriceData(this, this._container.getForexQuotes(), sender, false);
1181
1297
  });
1298
+
1299
+ if (this._single) {
1300
+ item.registerNewsExistsChangeHandler((exists, sender) => {
1301
+ this._dataActual.newsExists = exists;
1302
+ this._dataFormat.newsExists = exists;
1303
+ });
1304
+
1305
+ item._fundamentalDataChangeEvent((data, sender) => {
1306
+ this._dataFormat.fundamental = data;
1307
+ });
1308
+ }
1182
1309
  });
1183
1310
 
1184
1311
  this.refresh();
1185
1312
  }
1186
1313
 
1314
+ /**
1315
+ * The key of the group.
1316
+ *
1317
+ * @public
1318
+ * @returns {String}
1319
+ */
1187
1320
  get key() {
1188
1321
  return this._key;
1189
1322
  }
1190
1323
 
1324
+ /**
1325
+ * The description of the group.
1326
+ *
1327
+ * @public
1328
+ * @returns {String}
1329
+ */
1191
1330
  get description() {
1192
1331
  return this._description;
1193
1332
  }
1194
1333
 
1334
+ /**
1335
+ * The {@link Currency} which all aggregated data is presented in.
1336
+ *
1337
+ * @public
1338
+ * @returns {Currency}
1339
+ */
1195
1340
  get currency() {
1196
1341
  return this._currency;
1197
1342
  }
1198
1343
 
1344
+ /**
1345
+ * The {@link PositionItem} instances which for which aggregated data is compiled.
1346
+ *
1347
+ * @public
1348
+ * @returns {Currency}
1349
+ */
1199
1350
  get items() {
1200
1351
  return this._items;
1201
1352
  }
1202
1353
 
1354
+ /**
1355
+ * The string-based, human-readable aggregated data for the group.
1356
+ *
1357
+ * @public
1358
+ * @returns {Object}
1359
+ */
1203
1360
  get data() {
1204
1361
  return this._dataFormat;
1205
1362
  }
1206
1363
 
1364
+ /**
1365
+ * The raw aggregated data for the group (typically {@link Decimal} instances).
1366
+ *
1367
+ * @public
1368
+ * @returns {Object}
1369
+ */
1207
1370
  get actual() {
1208
1371
  return this._dataActual;
1209
1372
  }
1210
1373
 
1374
+ /**
1375
+ * Indicates if the group will only contain one {@link PositionItem} -- that is,
1376
+ * indicates if the group represents a single position.
1377
+ *
1378
+ * @public
1379
+ * @returns {Boolean}
1380
+ */
1211
1381
  get single() {
1212
1382
  return this._single;
1213
1383
  }
@@ -1216,10 +1386,22 @@ module.exports = (() => {
1216
1386
  return this._suspended;
1217
1387
  }
1218
1388
 
1389
+ /**
1390
+ * Indicates if the group should be excluded from higher-level aggregations.
1391
+ *
1392
+ * @public
1393
+ * @returns {Boolean}
1394
+ */
1219
1395
  get excluded() {
1220
1396
  return this._excluded;
1221
1397
  }
1222
1398
 
1399
+ /**
1400
+ * Causes aggregated data to be recalculated using a new exchange rate.
1401
+ *
1402
+ * @public
1403
+ * @param {Rate} rate
1404
+ */
1223
1405
  setForexRate(rate) {
1224
1406
  if (!this._bypassCurrencyTranslation) {
1225
1407
  this.refresh();
@@ -1230,11 +1412,7 @@ module.exports = (() => {
1230
1412
  assert.argumentIsRequired(value, 'value', Boolean);
1231
1413
 
1232
1414
  if (this._excluded !== value) {
1233
- this._container.startTransaction(() => {
1234
- this._items.forEach((item) => {
1235
- item.setExcluded(value);
1236
- });
1237
- });
1415
+ this._excludedChangeEvent(this._excluded = value);
1238
1416
  }
1239
1417
  }
1240
1418
 
@@ -1248,6 +1426,11 @@ module.exports = (() => {
1248
1426
  }
1249
1427
  }
1250
1428
 
1429
+ /**
1430
+ * Causes all aggregated data to be recalculated.
1431
+ *
1432
+ * @public
1433
+ */
1251
1434
  refresh() {
1252
1435
  const rates = this._container.getForexQuotes();
1253
1436
 
@@ -1255,6 +1438,12 @@ module.exports = (() => {
1255
1438
  calculatePriceData(this, rates, null, true);
1256
1439
  }
1257
1440
 
1441
+ /**
1442
+ * Causes the percent of the position, with respect to the parent container's
1443
+ * total, to be recalculated.
1444
+ *
1445
+ * @public
1446
+ */
1258
1447
  refreshMarketPercent() {
1259
1448
  calculateMarketPercent(this, this._container.getForexQuotes(), true);
1260
1449
  }
@@ -1360,8 +1549,11 @@ module.exports = (() => {
1360
1549
  if (group.single) {
1361
1550
  const item = group._items[0];
1362
1551
 
1363
- format.quantity = formatDecimal(item.position.snapshot.open, 2);
1364
- format.basisPrice = formatCurrency(item.data.basisPrice, currency);
1552
+ actual.quantity = item.position.snapshot.open;
1553
+ actual.basisPrice = item.data.basisPrice;
1554
+
1555
+ format.quantity = formatDecimal(actual.quantity, 2);
1556
+ format.basisPrice = formatCurrency(actual.basisPrice, currency);
1365
1557
  }
1366
1558
  }
1367
1559
 
@@ -1535,7 +1727,15 @@ module.exports = (() => {
1535
1727
  'use strict';
1536
1728
 
1537
1729
  /**
1730
+ * A container for a single position, which handles quote changes and
1731
+ * notifies observers -- which are typically parent-level {@link PositionGroup}
1732
+ * instances.
1733
+ *
1538
1734
  * @public
1735
+ * @param {Object} portfolio
1736
+ * @param {Object} position
1737
+ * @param {Object} currentSummary
1738
+ * @param {Array.<Object>} previousSummaries
1539
1739
  */
1540
1740
  class PositionItem {
1541
1741
  constructor(portfolio, position, currentSummary, previousSummaries) {
@@ -1551,10 +1751,12 @@ module.exports = (() => {
1551
1751
  this._data.basis = null;
1552
1752
 
1553
1753
  this._currentQuote = null;
1554
- this._previousQuote = null;
1754
+
1755
+ this._currentPrice = null;
1756
+ this._previousPrice = null;
1555
1757
 
1556
1758
  this._data.currentPrice = null;
1557
- this._data.previousPrice = null;
1759
+ this._data.currentPricePrevious = null;
1558
1760
 
1559
1761
  this._data.market = null;
1560
1762
  this._data.marketChange = null;
@@ -1564,7 +1766,7 @@ module.exports = (() => {
1564
1766
 
1565
1767
  this._data.unrealized = null;
1566
1768
  this._data.unrealizedChange = null;
1567
-
1769
+
1568
1770
  this._data.summaryTotalCurrent = null;
1569
1771
  this._data.summaryTotalCurrentChange = null;
1570
1772
 
@@ -1574,74 +1776,165 @@ module.exports = (() => {
1574
1776
  this._data.income = null;
1575
1777
  this._data.basisPrice = null;
1576
1778
 
1577
- this._excluded = false;
1779
+ this._data.newsExists = false;
1780
+ this._data.fundamental = { };
1578
1781
 
1579
1782
  calculateStaticData(this);
1580
1783
  calculatePriceData(this, null);
1581
1784
 
1582
1785
  this._quoteChangedEvent = new Event(this);
1583
- this._excludedChangeEvent = new Event(this);
1786
+ this._newsExistsChangedEvent = new Event(this);
1787
+ this._fundamentalDataChangeEvent = new Event(this);
1584
1788
  }
1585
1789
 
1790
+ /**
1791
+ * The portfolio of the encapsulated position.
1792
+ *
1793
+ * @public
1794
+ * @returns {Object}
1795
+ */
1586
1796
  get portfolio() {
1587
1797
  return this._portfolio;
1588
1798
  }
1589
1799
 
1800
+ /**
1801
+ * The encapsulated position.
1802
+ *
1803
+ * @public
1804
+ * @returns {Object}
1805
+ */
1590
1806
  get position() {
1591
1807
  return this._position;
1592
1808
  }
1593
1809
 
1810
+ /**
1811
+ * The {@link Currency} of the encapsulated position.
1812
+ *
1813
+ * @public
1814
+ * @returns {Object}
1815
+ */
1594
1816
  get currency() {
1595
1817
  return this._currency;
1596
1818
  }
1597
1819
 
1820
+ /**
1821
+ * The year-to-date position summary of the encapsulated position.
1822
+ *
1823
+ * @public
1824
+ * @returns {Object}
1825
+ */
1598
1826
  get currentSummary() {
1599
1827
  return this._currentSummary;
1600
1828
  }
1601
-
1829
+
1830
+ /**
1831
+ * Previous year's summaries for the encapsulated position.
1832
+ *
1833
+ * @public
1834
+ * @returns {Object}
1835
+ */
1602
1836
  get previousSummaries() {
1603
1837
  return this._previousSummaries;
1604
1838
  }
1605
1839
 
1840
+ /**
1841
+ * Various data regarding the encapsulated position.
1842
+ *
1843
+ * @public
1844
+ * @returns {*}
1845
+ */
1606
1846
  get data() {
1607
1847
  return this._data;
1608
1848
  }
1609
1849
 
1850
+ /**
1851
+ * The current quote for the symbol of the encapsulated position.
1852
+ *
1853
+ * @public
1854
+ * @returns {null|{Object}}
1855
+ */
1610
1856
  get quote() {
1611
1857
  return this._currentQuote;
1612
1858
  }
1613
1859
 
1614
- get excluded() {
1615
- return this._excluded;
1616
- }
1617
-
1860
+ /**
1861
+ * Sets the current quote -- causing position-level data (e.g. market value) to
1862
+ * be recalculated.
1863
+ *
1864
+ * @public
1865
+ * @param {Object} quote
1866
+ */
1618
1867
  setQuote(quote) {
1619
1868
  assert.argumentIsRequired(quote, 'quote', Object);
1620
1869
 
1621
- if (this._previousQuote === null || this._previousQuote.lastPrice !== quote.lastPrice) {
1870
+ if (this._currentPricePrevious !== quote.lastPrice) {
1622
1871
  calculatePriceData(this, quote.lastPrice);
1623
1872
 
1624
- this._previousQuote = this._currentQuote;
1873
+ this._currentPricePrevious = this._currentPrice;
1874
+ this._currentPrice = quote.lastPrice;
1875
+
1625
1876
  this._currentQuote = quote;
1626
1877
 
1627
1878
  this._quoteChangedEvent.fire(this._currentQuote);
1628
1879
  }
1629
1880
  }
1630
1881
 
1631
- setExcluded(value) {
1882
+ /**
1883
+ * Sets fundamental data for the position.
1884
+ *
1885
+ * @public
1886
+ * @param {Object} data
1887
+ */
1888
+ setPositionFundamentalData(data) {
1889
+ assert.argumentIsRequired(data, 'data', Object);
1890
+
1891
+ this._fundamentalDataChangeEvent(this._data.fundamental = data);
1892
+ }
1893
+
1894
+ /**
1895
+ * Sets a flag which indicates if news article(s) exist for the encapsulated position's
1896
+ * symbol.
1897
+ *
1898
+ * @public
1899
+ * @param {Boolean} value
1900
+ */
1901
+ setNewsArticleExists(value) {
1632
1902
  assert.argumentIsRequired(value, 'value', Boolean);
1633
1903
 
1634
- if (this._excluded !== value) {
1635
- this._excludedChangeEvent.fire(this._excluded = value);
1904
+ if (this._data.newsExists !== value) {
1905
+ this._newsExistsChangedEvent.fire(this._data.newsExists = value);
1636
1906
  }
1637
1907
  }
1638
1908
 
1909
+ /**
1910
+ * Registers an observer for quote changes, which is fired after internal recalculations
1911
+ * of position data are complete.
1912
+ *
1913
+ * @public
1914
+ * @param {Function} handler
1915
+ */
1639
1916
  registerQuoteChangeHandler(handler) {
1640
1917
  this._quoteChangedEvent.register(handler);
1641
1918
  }
1642
1919
 
1643
- registerExcludedChangeHandler(handler) {
1644
- this._excludedChangeEvent.register(handler);
1920
+ /**
1921
+ * Registers an observer for fundamental data changes.
1922
+ *
1923
+ * @public
1924
+ * @param {Function} handler
1925
+ */
1926
+ registerFundamentalDataChangeHandler(handler) {
1927
+ this._fundamentalDataChangeEvent.register(handler);
1928
+ }
1929
+
1930
+ /**
1931
+ * Registers an observer changes to the status of news existence.
1932
+ *
1933
+ * @public
1934
+ * @param {Function} handler
1935
+ */
1936
+ registerNewsExistsChangeHandler(handler) {
1937
+ this._newsExistsChangedEvent.register(handler);
1645
1938
  }
1646
1939
 
1647
1940
  toString() {
@@ -1768,10 +2061,10 @@ module.exports = (() => {
1768
2061
  data.unrealizedChange = Decimal.ZERO;
1769
2062
  }
1770
2063
  }
1771
-
2064
+
1772
2065
  function calculateSummaryTotal(summary) {
1773
2066
  let returnRef;
1774
-
2067
+
1775
2068
  if (summary) {
1776
2069
  const period = summary.period;
1777
2070
 
@@ -1779,7 +2072,7 @@ module.exports = (() => {
1779
2072
  } else {
1780
2073
  returnRef = Decimal.ZERO;
1781
2074
  }
1782
-
2075
+
1783
2076
  return returnRef;
1784
2077
  }
1785
2078