mirror of https://github.com/ghostfolio/ghostfolio
				
				
			
							committed by
							
								
								Thomas
							
						
					
				
				 4 changed files with 0 additions and 1416 deletions
			
			
		@ -1,478 +0,0 @@ | 
				
			|||
import { AccountService } from '@ghostfolio/api/app/account/account.service'; | 
				
			|||
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config'; | 
				
			|||
import { DATE_FORMAT, getUtc, getYesterday } from '@ghostfolio/common/helper'; | 
				
			|||
import { | 
				
			|||
  AccountType, | 
				
			|||
  Currency, | 
				
			|||
  DataSource, | 
				
			|||
  Role, | 
				
			|||
  Type, | 
				
			|||
  ViewMode | 
				
			|||
} from '@prisma/client'; | 
				
			|||
import { format } from 'date-fns'; | 
				
			|||
 | 
				
			|||
import { DataProviderService } from '../services/data-provider.service'; | 
				
			|||
import { ExchangeRateDataService } from '../services/exchange-rate-data.service'; | 
				
			|||
import { MarketState } from '../services/interfaces/interfaces'; | 
				
			|||
import { RulesService } from '../services/rules.service'; | 
				
			|||
import { Portfolio } from './portfolio'; | 
				
			|||
 | 
				
			|||
jest.mock('../app/account/account.service', () => { | 
				
			|||
  return { | 
				
			|||
    AccountService: jest.fn().mockImplementation(() => { | 
				
			|||
      return { | 
				
			|||
        getCashDetails: () => Promise.resolve({ accounts: [], balance: 0 }) | 
				
			|||
      }; | 
				
			|||
    }) | 
				
			|||
  }; | 
				
			|||
}); | 
				
			|||
 | 
				
			|||
jest.mock('../services/data-provider.service', () => { | 
				
			|||
  return { | 
				
			|||
    DataProviderService: jest.fn().mockImplementation(() => { | 
				
			|||
      const today = format(new Date(), DATE_FORMAT); | 
				
			|||
      const yesterday = format(getYesterday(), DATE_FORMAT); | 
				
			|||
 | 
				
			|||
      return { | 
				
			|||
        get: () => { | 
				
			|||
          return Promise.resolve({ | 
				
			|||
            BTCUSD: { | 
				
			|||
              currency: Currency.USD, | 
				
			|||
              dataSource: DataSource.YAHOO, | 
				
			|||
              exchange: UNKNOWN_KEY, | 
				
			|||
              marketPrice: 57973.008, | 
				
			|||
              marketState: MarketState.open, | 
				
			|||
              name: 'Bitcoin USD', | 
				
			|||
              type: 'Cryptocurrency' | 
				
			|||
            }, | 
				
			|||
            ETHUSD: { | 
				
			|||
              currency: Currency.USD, | 
				
			|||
              dataSource: DataSource.YAHOO, | 
				
			|||
              exchange: UNKNOWN_KEY, | 
				
			|||
              marketPrice: 3915.337, | 
				
			|||
              marketState: MarketState.open, | 
				
			|||
              name: 'Ethereum USD', | 
				
			|||
              type: 'Cryptocurrency' | 
				
			|||
            } | 
				
			|||
          }); | 
				
			|||
        }, | 
				
			|||
        getHistorical: () => { | 
				
			|||
          return Promise.resolve({ | 
				
			|||
            BTCUSD: { | 
				
			|||
              [yesterday]: 56710.122, | 
				
			|||
              [today]: 57973.008 | 
				
			|||
            }, | 
				
			|||
            ETHUSD: { | 
				
			|||
              [yesterday]: 3641.984, | 
				
			|||
              [today]: 3915.337 | 
				
			|||
            } | 
				
			|||
          }); | 
				
			|||
        } | 
				
			|||
      }; | 
				
			|||
    }) | 
				
			|||
  }; | 
				
			|||
}); | 
				
			|||
 | 
				
			|||
jest.mock('../services/exchange-rate-data.service', () => { | 
				
			|||
  return { | 
				
			|||
    ExchangeRateDataService: jest.fn().mockImplementation(() => { | 
				
			|||
      return { | 
				
			|||
        initialize: () => Promise.resolve(), | 
				
			|||
        toCurrency: (value: number) => { | 
				
			|||
          return 1 * value; | 
				
			|||
        } | 
				
			|||
      }; | 
				
			|||
    }) | 
				
			|||
  }; | 
				
			|||
}); | 
				
			|||
 | 
				
			|||
jest.mock('../services/rules.service'); | 
				
			|||
 | 
				
			|||
const DEFAULT_ACCOUNT_ID = '693a834b-eb89-42c9-ae47-35196c25d269'; | 
				
			|||
const USER_ID = 'ca6ce867-5d31-495a-bce9-5942bbca9237'; | 
				
			|||
 | 
				
			|||
describe('Portfolio', () => { | 
				
			|||
  let accountService: AccountService; | 
				
			|||
  let dataProviderService: DataProviderService; | 
				
			|||
  let exchangeRateDataService: ExchangeRateDataService; | 
				
			|||
  let portfolio: Portfolio; | 
				
			|||
  let rulesService: RulesService; | 
				
			|||
 | 
				
			|||
  beforeAll(async () => { | 
				
			|||
    accountService = new AccountService(null, null, null); | 
				
			|||
    dataProviderService = new DataProviderService( | 
				
			|||
      null, | 
				
			|||
      null, | 
				
			|||
      null, | 
				
			|||
      null, | 
				
			|||
      null, | 
				
			|||
      null | 
				
			|||
    ); | 
				
			|||
    exchangeRateDataService = new ExchangeRateDataService(null); | 
				
			|||
    rulesService = new RulesService(); | 
				
			|||
 | 
				
			|||
    await exchangeRateDataService.initialize(); | 
				
			|||
 | 
				
			|||
    portfolio = new Portfolio( | 
				
			|||
      accountService, | 
				
			|||
      dataProviderService, | 
				
			|||
      exchangeRateDataService, | 
				
			|||
      rulesService | 
				
			|||
    ); | 
				
			|||
    portfolio.setUser({ | 
				
			|||
      accessToken: null, | 
				
			|||
      Account: [ | 
				
			|||
        { | 
				
			|||
          accountType: AccountType.SECURITIES, | 
				
			|||
          balance: 0, | 
				
			|||
          createdAt: new Date(), | 
				
			|||
          currency: Currency.USD, | 
				
			|||
          id: DEFAULT_ACCOUNT_ID, | 
				
			|||
          isDefault: true, | 
				
			|||
          name: 'Default Account', | 
				
			|||
          platformId: null, | 
				
			|||
          updatedAt: new Date(), | 
				
			|||
          userId: USER_ID | 
				
			|||
        } | 
				
			|||
      ], | 
				
			|||
      alias: 'Test', | 
				
			|||
      authChallenge: null, | 
				
			|||
      createdAt: new Date(), | 
				
			|||
      id: USER_ID, | 
				
			|||
      provider: null, | 
				
			|||
      role: Role.USER, | 
				
			|||
      Settings: { | 
				
			|||
        currency: Currency.CHF, | 
				
			|||
        updatedAt: new Date(), | 
				
			|||
        userId: USER_ID, | 
				
			|||
        viewMode: ViewMode.DEFAULT | 
				
			|||
      }, | 
				
			|||
      thirdPartyId: null, | 
				
			|||
      updatedAt: new Date() | 
				
			|||
    }); | 
				
			|||
  }); | 
				
			|||
 | 
				
			|||
  describe('works with no orders', () => { | 
				
			|||
    it('should return []', () => { | 
				
			|||
      expect(portfolio.get(new Date())).toEqual([]); | 
				
			|||
      expect(portfolio.getFees()).toEqual(0); | 
				
			|||
      expect(portfolio.getPositions(new Date())).toEqual({}); | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    it('should return empty details', async () => { | 
				
			|||
      const details = await portfolio.getDetails('1d'); | 
				
			|||
      expect(details).toMatchObject({ | 
				
			|||
        _GF_CASH: { | 
				
			|||
          accounts: {}, | 
				
			|||
          allocationCurrent: NaN, // TODO
 | 
				
			|||
          allocationInvestment: NaN, // TODO
 | 
				
			|||
          countries: [], | 
				
			|||
          currency: 'CHF', | 
				
			|||
          grossPerformance: 0, | 
				
			|||
          grossPerformancePercent: 0, | 
				
			|||
          investment: 0, | 
				
			|||
          marketPrice: 0, | 
				
			|||
          marketState: 'open', | 
				
			|||
          name: 'Cash', | 
				
			|||
          quantity: 0, | 
				
			|||
          sectors: [], | 
				
			|||
          symbol: '_GF_CASH', | 
				
			|||
          transactionCount: 0, | 
				
			|||
          type: 'Cash', | 
				
			|||
          value: 0 | 
				
			|||
        } | 
				
			|||
      }); | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    it('should return empty details', async () => { | 
				
			|||
      const details = await portfolio.getDetails('max'); | 
				
			|||
      expect(details).toMatchObject({ | 
				
			|||
        _GF_CASH: { | 
				
			|||
          accounts: {}, | 
				
			|||
          allocationCurrent: NaN, // TODO
 | 
				
			|||
          allocationInvestment: NaN, // TODO
 | 
				
			|||
          countries: [], | 
				
			|||
          currency: 'CHF', | 
				
			|||
          grossPerformance: 0, | 
				
			|||
          grossPerformancePercent: 0, | 
				
			|||
          investment: 0, | 
				
			|||
          marketPrice: 0, | 
				
			|||
          marketState: 'open', | 
				
			|||
          name: 'Cash', | 
				
			|||
          quantity: 0, | 
				
			|||
          sectors: [], | 
				
			|||
          symbol: '_GF_CASH', | 
				
			|||
          transactionCount: 0, | 
				
			|||
          type: 'Cash', | 
				
			|||
          value: 0 | 
				
			|||
        } | 
				
			|||
      }); | 
				
			|||
    }); | 
				
			|||
  }); | 
				
			|||
 | 
				
			|||
  describe('works with orders', () => { | 
				
			|||
    it('should return ["ETHUSD"]', async () => { | 
				
			|||
      await portfolio.setOrders([ | 
				
			|||
        { | 
				
			|||
          accountId: DEFAULT_ACCOUNT_ID, | 
				
			|||
          accountUserId: USER_ID, | 
				
			|||
          createdAt: null, | 
				
			|||
          currency: Currency.USD, | 
				
			|||
          dataSource: DataSource.YAHOO, | 
				
			|||
          fee: 0, | 
				
			|||
          date: new Date(getUtc('2018-01-05')), | 
				
			|||
          id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb', | 
				
			|||
          quantity: 0.2, | 
				
			|||
          symbol: 'ETHUSD', | 
				
			|||
          symbolProfileId: null, | 
				
			|||
          type: Type.BUY, | 
				
			|||
          unitPrice: 991.49, | 
				
			|||
          updatedAt: null, | 
				
			|||
          userId: USER_ID | 
				
			|||
        } | 
				
			|||
      ]); | 
				
			|||
 | 
				
			|||
      expect(portfolio.getFees()).toEqual(0); | 
				
			|||
 | 
				
			|||
      expect(portfolio.getPositions(getYesterday())).toMatchObject({ | 
				
			|||
        ETHUSD: { | 
				
			|||
          averagePrice: 991.49, | 
				
			|||
          currency: Currency.USD, | 
				
			|||
          firstBuyDate: '2018-01-05T00:00:00.000Z', | 
				
			|||
          investment: exchangeRateDataService.toCurrency( | 
				
			|||
            0.2 * 991.49, | 
				
			|||
            Currency.USD, | 
				
			|||
            baseCurrency | 
				
			|||
          ), | 
				
			|||
          investmentInOriginalCurrency: 0.2 * 991.49, | 
				
			|||
          // marketPrice: 3915.337,
 | 
				
			|||
          quantity: 0.2 | 
				
			|||
        } | 
				
			|||
      }); | 
				
			|||
 | 
				
			|||
      expect(portfolio.getSymbols(getYesterday())).toEqual(['ETHUSD']); | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    it('should return ["ETHUSD"]', async () => { | 
				
			|||
      await portfolio.setOrders([ | 
				
			|||
        { | 
				
			|||
          accountId: DEFAULT_ACCOUNT_ID, | 
				
			|||
          accountUserId: USER_ID, | 
				
			|||
          createdAt: null, | 
				
			|||
          currency: Currency.USD, | 
				
			|||
          dataSource: DataSource.YAHOO, | 
				
			|||
          fee: 0, | 
				
			|||
          date: new Date(getUtc('2018-01-05')), | 
				
			|||
          id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb', | 
				
			|||
          quantity: 0.2, | 
				
			|||
          symbol: 'ETHUSD', | 
				
			|||
          symbolProfileId: null, | 
				
			|||
          type: Type.BUY, | 
				
			|||
          unitPrice: 991.49, | 
				
			|||
          updatedAt: null, | 
				
			|||
          userId: USER_ID | 
				
			|||
        }, | 
				
			|||
        { | 
				
			|||
          accountId: DEFAULT_ACCOUNT_ID, | 
				
			|||
          accountUserId: USER_ID, | 
				
			|||
          createdAt: null, | 
				
			|||
          currency: Currency.USD, | 
				
			|||
          dataSource: DataSource.YAHOO, | 
				
			|||
          fee: 0, | 
				
			|||
          date: new Date(getUtc('2018-01-28')), | 
				
			|||
          id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc', | 
				
			|||
          quantity: 0.3, | 
				
			|||
          symbol: 'ETHUSD', | 
				
			|||
          symbolProfileId: null, | 
				
			|||
          type: Type.BUY, | 
				
			|||
          unitPrice: 1050, | 
				
			|||
          updatedAt: null, | 
				
			|||
          userId: USER_ID | 
				
			|||
        } | 
				
			|||
      ]); | 
				
			|||
 | 
				
			|||
      expect(portfolio.getFees()).toEqual(0); | 
				
			|||
 | 
				
			|||
      expect(portfolio.getPositions(getYesterday())).toMatchObject({ | 
				
			|||
        ETHUSD: { | 
				
			|||
          averagePrice: (0.2 * 991.49 + 0.3 * 1050) / (0.2 + 0.3), | 
				
			|||
          currency: Currency.USD, | 
				
			|||
          firstBuyDate: '2018-01-05T00:00:00.000Z', | 
				
			|||
          investment: | 
				
			|||
            exchangeRateDataService.toCurrency( | 
				
			|||
              0.2 * 991.49, | 
				
			|||
              Currency.USD, | 
				
			|||
              baseCurrency | 
				
			|||
            ) + | 
				
			|||
            exchangeRateDataService.toCurrency( | 
				
			|||
              0.3 * 1050, | 
				
			|||
              Currency.USD, | 
				
			|||
              baseCurrency | 
				
			|||
            ), | 
				
			|||
          investmentInOriginalCurrency: 0.2 * 991.49 + 0.3 * 1050, | 
				
			|||
          // marketPrice: 3641.984,
 | 
				
			|||
          quantity: 0.5 | 
				
			|||
        } | 
				
			|||
      }); | 
				
			|||
 | 
				
			|||
      expect(portfolio.getSymbols(getYesterday())).toEqual(['ETHUSD']); | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    it('should return ["BTCUSD", "ETHUSD"]', async () => { | 
				
			|||
      await portfolio.setOrders([ | 
				
			|||
        { | 
				
			|||
          accountId: DEFAULT_ACCOUNT_ID, | 
				
			|||
          accountUserId: USER_ID, | 
				
			|||
          createdAt: null, | 
				
			|||
          currency: Currency.EUR, | 
				
			|||
          dataSource: DataSource.YAHOO, | 
				
			|||
          date: new Date(getUtc('2017-08-16')), | 
				
			|||
          fee: 2.99, | 
				
			|||
          id: 'd96795b2-6ae6-420e-aa21-fabe5e45d475', | 
				
			|||
          quantity: 0.05614682, | 
				
			|||
          symbol: 'BTCUSD', | 
				
			|||
          symbolProfileId: null, | 
				
			|||
          type: Type.BUY, | 
				
			|||
          unitPrice: 3562.089535970158, | 
				
			|||
          updatedAt: null, | 
				
			|||
          userId: USER_ID | 
				
			|||
        }, | 
				
			|||
        { | 
				
			|||
          accountId: DEFAULT_ACCOUNT_ID, | 
				
			|||
          accountUserId: USER_ID, | 
				
			|||
          createdAt: null, | 
				
			|||
          currency: Currency.USD, | 
				
			|||
          dataSource: DataSource.YAHOO, | 
				
			|||
          fee: 2.99, | 
				
			|||
          date: new Date(getUtc('2018-01-05')), | 
				
			|||
          id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb', | 
				
			|||
          quantity: 0.2, | 
				
			|||
          symbol: 'ETHUSD', | 
				
			|||
          symbolProfileId: null, | 
				
			|||
          type: Type.BUY, | 
				
			|||
          unitPrice: 991.49, | 
				
			|||
          updatedAt: null, | 
				
			|||
          userId: USER_ID | 
				
			|||
        } | 
				
			|||
      ]); | 
				
			|||
 | 
				
			|||
      expect(portfolio.getFees()).toEqual( | 
				
			|||
        exchangeRateDataService.toCurrency(2.99, Currency.EUR, baseCurrency) + | 
				
			|||
          exchangeRateDataService.toCurrency(2.99, Currency.USD, baseCurrency) | 
				
			|||
      ); | 
				
			|||
 | 
				
			|||
      expect(portfolio.getPositions(getYesterday())).toMatchObject({ | 
				
			|||
        BTCUSD: { | 
				
			|||
          averagePrice: 3562.089535970158, | 
				
			|||
          currency: Currency.EUR, | 
				
			|||
          firstBuyDate: '2017-08-16T00:00:00.000Z', | 
				
			|||
          investment: exchangeRateDataService.toCurrency( | 
				
			|||
            0.05614682 * 3562.089535970158, | 
				
			|||
            Currency.EUR, | 
				
			|||
            baseCurrency | 
				
			|||
          ), | 
				
			|||
          investmentInOriginalCurrency: 0.05614682 * 3562.089535970158, | 
				
			|||
          // marketPrice: 0,
 | 
				
			|||
          quantity: 0.05614682 | 
				
			|||
        }, | 
				
			|||
        ETHUSD: { | 
				
			|||
          averagePrice: 991.49, | 
				
			|||
          currency: Currency.USD, | 
				
			|||
          firstBuyDate: '2018-01-05T00:00:00.000Z', | 
				
			|||
          investment: exchangeRateDataService.toCurrency( | 
				
			|||
            0.2 * 991.49, | 
				
			|||
            Currency.USD, | 
				
			|||
            baseCurrency | 
				
			|||
          ), | 
				
			|||
          investmentInOriginalCurrency: 0.2 * 991.49, | 
				
			|||
          // marketPrice: 0,
 | 
				
			|||
          quantity: 0.2 | 
				
			|||
        } | 
				
			|||
      }); | 
				
			|||
 | 
				
			|||
      expect(portfolio.getSymbols(getYesterday())).toEqual([ | 
				
			|||
        'BTCUSD', | 
				
			|||
        'ETHUSD' | 
				
			|||
      ]); | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    it('should work with buy and sell', async () => { | 
				
			|||
      await portfolio.setOrders([ | 
				
			|||
        { | 
				
			|||
          accountId: DEFAULT_ACCOUNT_ID, | 
				
			|||
          accountUserId: USER_ID, | 
				
			|||
          createdAt: null, | 
				
			|||
          currency: Currency.USD, | 
				
			|||
          dataSource: DataSource.YAHOO, | 
				
			|||
          fee: 1.0, | 
				
			|||
          date: new Date(getUtc('2018-01-05')), | 
				
			|||
          id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb', | 
				
			|||
          quantity: 0.2, | 
				
			|||
          symbol: 'ETHUSD', | 
				
			|||
          symbolProfileId: null, | 
				
			|||
          type: Type.BUY, | 
				
			|||
          unitPrice: 991.49, | 
				
			|||
          updatedAt: null, | 
				
			|||
          userId: USER_ID | 
				
			|||
        }, | 
				
			|||
        { | 
				
			|||
          accountId: DEFAULT_ACCOUNT_ID, | 
				
			|||
          accountUserId: USER_ID, | 
				
			|||
          createdAt: null, | 
				
			|||
          currency: Currency.USD, | 
				
			|||
          dataSource: DataSource.YAHOO, | 
				
			|||
          fee: 1.0, | 
				
			|||
          date: new Date(getUtc('2018-01-28')), | 
				
			|||
          id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc', | 
				
			|||
          quantity: 0.1, | 
				
			|||
          symbol: 'ETHUSD', | 
				
			|||
          symbolProfileId: null, | 
				
			|||
          type: Type.SELL, | 
				
			|||
          unitPrice: 1050, | 
				
			|||
          updatedAt: null, | 
				
			|||
          userId: USER_ID | 
				
			|||
        }, | 
				
			|||
        { | 
				
			|||
          accountId: DEFAULT_ACCOUNT_ID, | 
				
			|||
          accountUserId: USER_ID, | 
				
			|||
          createdAt: null, | 
				
			|||
          currency: Currency.USD, | 
				
			|||
          dataSource: DataSource.YAHOO, | 
				
			|||
          fee: 1.0, | 
				
			|||
          date: new Date(getUtc('2018-01-31')), | 
				
			|||
          id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc', | 
				
			|||
          quantity: 0.2, | 
				
			|||
          symbol: 'ETHUSD', | 
				
			|||
          symbolProfileId: null, | 
				
			|||
          type: Type.BUY, | 
				
			|||
          unitPrice: 1050, | 
				
			|||
          updatedAt: null, | 
				
			|||
          userId: USER_ID | 
				
			|||
        } | 
				
			|||
      ]); | 
				
			|||
 | 
				
			|||
      expect(portfolio.getFees()).toEqual( | 
				
			|||
        exchangeRateDataService.toCurrency(3, Currency.USD, baseCurrency) | 
				
			|||
      ); | 
				
			|||
 | 
				
			|||
      expect(portfolio.getPositions(getYesterday())).toMatchObject({ | 
				
			|||
        ETHUSD: { | 
				
			|||
          averagePrice: | 
				
			|||
            (0.2 * 991.49 - 0.1 * 1050 + 0.2 * 1050) / (0.2 - 0.1 + 0.2), | 
				
			|||
          currency: Currency.USD, | 
				
			|||
          firstBuyDate: '2018-01-05T00:00:00.000Z', | 
				
			|||
          investment: exchangeRateDataService.toCurrency( | 
				
			|||
            0.2 * 991.49 - 0.1 * 1050 + 0.2 * 1050, | 
				
			|||
            Currency.USD, | 
				
			|||
            baseCurrency | 
				
			|||
          ), | 
				
			|||
          investmentInOriginalCurrency: 0.2 * 991.49 - 0.1 * 1050 + 0.2 * 1050, | 
				
			|||
          // marketPrice: 0,
 | 
				
			|||
          quantity: 0.2 - 0.1 + 0.2 | 
				
			|||
        } | 
				
			|||
      }); | 
				
			|||
 | 
				
			|||
      expect(portfolio.getSymbols(getYesterday())).toEqual(['ETHUSD']); | 
				
			|||
    }); | 
				
			|||
  }); | 
				
			|||
}); | 
				
			|||
@ -1,872 +0,0 @@ | 
				
			|||
import { AccountService } from '@ghostfolio/api/app/account/account.service'; | 
				
			|||
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface'; | 
				
			|||
import { UNKNOWN_KEY, ghostfolioCashSymbol } from '@ghostfolio/common/config'; | 
				
			|||
import { | 
				
			|||
  DATE_FORMAT, | 
				
			|||
  getToday, | 
				
			|||
  getYesterday, | 
				
			|||
  resetHours | 
				
			|||
} from '@ghostfolio/common/helper'; | 
				
			|||
import { | 
				
			|||
  PortfolioItem, | 
				
			|||
  PortfolioPerformance, | 
				
			|||
  PortfolioPosition, | 
				
			|||
  PortfolioReport, | 
				
			|||
  Position, | 
				
			|||
  UserWithSettings | 
				
			|||
} from '@ghostfolio/common/interfaces'; | 
				
			|||
import { Country } from '@ghostfolio/common/interfaces/country.interface'; | 
				
			|||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; | 
				
			|||
import { DateRange, OrderWithAccount } from '@ghostfolio/common/types'; | 
				
			|||
import { Currency, Prisma } from '@prisma/client'; | 
				
			|||
import { continents, countries } from 'countries-list'; | 
				
			|||
import { | 
				
			|||
  add, | 
				
			|||
  format, | 
				
			|||
  getDate, | 
				
			|||
  getMonth, | 
				
			|||
  getYear, | 
				
			|||
  isAfter, | 
				
			|||
  isBefore, | 
				
			|||
  isSameDay, | 
				
			|||
  isToday, | 
				
			|||
  isYesterday, | 
				
			|||
  parseISO, | 
				
			|||
  setDate, | 
				
			|||
  setMonth, | 
				
			|||
  sub | 
				
			|||
} from 'date-fns'; | 
				
			|||
import { cloneDeep, isEmpty } from 'lodash'; | 
				
			|||
import * as roundTo from 'round-to'; | 
				
			|||
 | 
				
			|||
import { DataProviderService } from '../services/data-provider.service'; | 
				
			|||
import { ExchangeRateDataService } from '../services/exchange-rate-data.service'; | 
				
			|||
import { IOrder, MarketState, Type } from '../services/interfaces/interfaces'; | 
				
			|||
import { RulesService } from '../services/rules.service'; | 
				
			|||
import { PortfolioInterface } from './interfaces/portfolio.interface'; | 
				
			|||
import { Order } from './order'; | 
				
			|||
import { OrderType } from './order-type'; | 
				
			|||
import { AccountClusterRiskCurrentInvestment } from './rules/account-cluster-risk/current-investment'; | 
				
			|||
import { AccountClusterRiskInitialInvestment } from './rules/account-cluster-risk/initial-investment'; | 
				
			|||
import { AccountClusterRiskSingleAccount } from './rules/account-cluster-risk/single-account'; | 
				
			|||
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from './rules/currency-cluster-risk/base-currency-current-investment'; | 
				
			|||
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from './rules/currency-cluster-risk/base-currency-initial-investment'; | 
				
			|||
import { CurrencyClusterRiskCurrentInvestment } from './rules/currency-cluster-risk/current-investment'; | 
				
			|||
import { CurrencyClusterRiskInitialInvestment } from './rules/currency-cluster-risk/initial-investment'; | 
				
			|||
import { FeeRatioInitialInvestment } from './rules/fees/fee-ratio-initial-investment'; | 
				
			|||
 | 
				
			|||
export class Portfolio implements PortfolioInterface { | 
				
			|||
  private orders: Order[] = []; | 
				
			|||
  private portfolioItems: PortfolioItem[] = []; | 
				
			|||
  private user: UserWithSettings; | 
				
			|||
 | 
				
			|||
  public constructor( | 
				
			|||
    private accountService: AccountService, | 
				
			|||
    private dataProviderService: DataProviderService, | 
				
			|||
    private exchangeRateDataService: ExchangeRateDataService, | 
				
			|||
    private rulesService: RulesService | 
				
			|||
  ) {} | 
				
			|||
 | 
				
			|||
  public async addCurrentPortfolioItems() { | 
				
			|||
    const currentData = await this.dataProviderService.get(this.getSymbols()); | 
				
			|||
 | 
				
			|||
    const currentDate = new Date(); | 
				
			|||
 | 
				
			|||
    const year = getYear(currentDate); | 
				
			|||
    const month = getMonth(currentDate); | 
				
			|||
    const day = getDate(currentDate); | 
				
			|||
 | 
				
			|||
    const today = new Date(Date.UTC(year, month, day)); | 
				
			|||
    const yesterday = getYesterday(); | 
				
			|||
 | 
				
			|||
    const [portfolioItemsYesterday] = this.get(yesterday); | 
				
			|||
 | 
				
			|||
    const positions: { [symbol: string]: Position } = {}; | 
				
			|||
 | 
				
			|||
    this.getSymbols().forEach((symbol) => { | 
				
			|||
      positions[symbol] = { | 
				
			|||
        symbol, | 
				
			|||
        averagePrice: portfolioItemsYesterday?.positions[symbol]?.averagePrice, | 
				
			|||
        currency: portfolioItemsYesterday?.positions[symbol]?.currency, | 
				
			|||
        firstBuyDate: portfolioItemsYesterday?.positions[symbol]?.firstBuyDate, | 
				
			|||
        investment: portfolioItemsYesterday?.positions[symbol]?.investment, | 
				
			|||
        investmentInOriginalCurrency: | 
				
			|||
          portfolioItemsYesterday?.positions[symbol] | 
				
			|||
            ?.investmentInOriginalCurrency, | 
				
			|||
        marketPrice: | 
				
			|||
          currentData[symbol]?.marketPrice ?? | 
				
			|||
          portfolioItemsYesterday.positions[symbol]?.marketPrice, | 
				
			|||
        quantity: portfolioItemsYesterday?.positions[symbol]?.quantity, | 
				
			|||
        transactionCount: | 
				
			|||
          portfolioItemsYesterday?.positions[symbol]?.transactionCount | 
				
			|||
      }; | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    if (portfolioItemsYesterday?.investment) { | 
				
			|||
      const portfolioItemsLength = this.portfolioItems.push( | 
				
			|||
        cloneDeep({ | 
				
			|||
          date: today.toISOString(), | 
				
			|||
          grossPerformancePercent: 0, | 
				
			|||
          investment: portfolioItemsYesterday?.investment, | 
				
			|||
          positions: positions, | 
				
			|||
          value: 0 | 
				
			|||
        }) | 
				
			|||
      ); | 
				
			|||
 | 
				
			|||
      // Set value after pushing today's portfolio items
 | 
				
			|||
      this.portfolioItems[portfolioItemsLength - 1].value = | 
				
			|||
        this.getValue(today); | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    return this; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public async addFuturePortfolioItems() { | 
				
			|||
    let investment = this.getInvestment(new Date()); | 
				
			|||
 | 
				
			|||
    this.getOrders() | 
				
			|||
      .filter((order) => order.getIsDraft() === true) | 
				
			|||
      .forEach((order) => { | 
				
			|||
        investment += this.exchangeRateDataService.toCurrency( | 
				
			|||
          order.getTotal(), | 
				
			|||
          order.getCurrency(), | 
				
			|||
          this.user.Settings.currency | 
				
			|||
        ); | 
				
			|||
 | 
				
			|||
        const portfolioItem = this.portfolioItems.find((item) => { | 
				
			|||
          return item.date === order.getDate(); | 
				
			|||
        }); | 
				
			|||
 | 
				
			|||
        if (portfolioItem) { | 
				
			|||
          portfolioItem.investment = investment; | 
				
			|||
        } else { | 
				
			|||
          this.portfolioItems.push({ | 
				
			|||
            investment, | 
				
			|||
            date: order.getDate(), | 
				
			|||
            grossPerformancePercent: 0, | 
				
			|||
            positions: {}, | 
				
			|||
            value: 0 | 
				
			|||
          }); | 
				
			|||
        } | 
				
			|||
      }); | 
				
			|||
 | 
				
			|||
    return this; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public createFromData({ | 
				
			|||
    orders, | 
				
			|||
    portfolioItems, | 
				
			|||
    user | 
				
			|||
  }: { | 
				
			|||
    orders: IOrder[]; | 
				
			|||
    portfolioItems: PortfolioItem[]; | 
				
			|||
    user: UserWithSettings; | 
				
			|||
  }): Portfolio { | 
				
			|||
    orders.forEach( | 
				
			|||
      ({ | 
				
			|||
        account, | 
				
			|||
        currency, | 
				
			|||
        fee, | 
				
			|||
        date, | 
				
			|||
        id, | 
				
			|||
        quantity, | 
				
			|||
        symbol, | 
				
			|||
        symbolProfile, | 
				
			|||
        type, | 
				
			|||
        unitPrice | 
				
			|||
      }) => { | 
				
			|||
        this.orders.push( | 
				
			|||
          new Order({ | 
				
			|||
            account, | 
				
			|||
            currency, | 
				
			|||
            fee, | 
				
			|||
            date, | 
				
			|||
            id, | 
				
			|||
            quantity, | 
				
			|||
            symbol, | 
				
			|||
            symbolProfile, | 
				
			|||
            type, | 
				
			|||
            unitPrice | 
				
			|||
          }) | 
				
			|||
        ); | 
				
			|||
      } | 
				
			|||
    ); | 
				
			|||
 | 
				
			|||
    portfolioItems.forEach( | 
				
			|||
      ({ date, grossPerformancePercent, investment, positions, value }) => { | 
				
			|||
        this.portfolioItems.push({ | 
				
			|||
          date, | 
				
			|||
          grossPerformancePercent, | 
				
			|||
          investment, | 
				
			|||
          positions, | 
				
			|||
          value | 
				
			|||
        }); | 
				
			|||
      } | 
				
			|||
    ); | 
				
			|||
 | 
				
			|||
    this.setUser(user); | 
				
			|||
 | 
				
			|||
    return this; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public get(aDate?: Date): PortfolioItem[] { | 
				
			|||
    if (aDate) { | 
				
			|||
      const filteredPortfolio = this.portfolioItems.find((item) => { | 
				
			|||
        return isSameDay(aDate, new Date(item.date)); | 
				
			|||
      }); | 
				
			|||
 | 
				
			|||
      if (filteredPortfolio) { | 
				
			|||
        return [cloneDeep(filteredPortfolio)]; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      return []; | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    return cloneDeep(this.portfolioItems); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public async getDetails( | 
				
			|||
    aDateRange: DateRange = 'max' | 
				
			|||
  ): Promise<{ [symbol: string]: PortfolioPosition }> { | 
				
			|||
    const dateRangeDate = this.convertDateRangeToDate( | 
				
			|||
      aDateRange, | 
				
			|||
      this.getMinDate() | 
				
			|||
    ); | 
				
			|||
 | 
				
			|||
    const [portfolioItemsBefore] = this.get(dateRangeDate); | 
				
			|||
 | 
				
			|||
    const [portfolioItemsNow] = await this.get(new Date()); | 
				
			|||
 | 
				
			|||
    const cashDetails = await this.accountService.getCashDetails( | 
				
			|||
      this.user.id, | 
				
			|||
      this.user.Settings.currency | 
				
			|||
    ); | 
				
			|||
    const investment = this.getInvestment(new Date()) + cashDetails.balance; | 
				
			|||
    const portfolioItems = this.get(new Date()); | 
				
			|||
    const symbols = this.getSymbols(new Date()); | 
				
			|||
    const value = this.getValue() + cashDetails.balance; | 
				
			|||
 | 
				
			|||
    const details: { [symbol: string]: PortfolioPosition } = {}; | 
				
			|||
 | 
				
			|||
    const data = await this.dataProviderService.get(symbols); | 
				
			|||
 | 
				
			|||
    symbols.forEach((symbol) => { | 
				
			|||
      const accounts: PortfolioPosition['accounts'] = {}; | 
				
			|||
      let countriesOfSymbol: Country[]; | 
				
			|||
      let sectorsOfSymbol: Sector[]; | 
				
			|||
      const [portfolioItem] = portfolioItems; | 
				
			|||
 | 
				
			|||
      const ordersBySymbol = this.getOrders().filter((order) => { | 
				
			|||
        return order.getSymbol() === symbol; | 
				
			|||
      }); | 
				
			|||
 | 
				
			|||
      ordersBySymbol.forEach((orderOfSymbol) => { | 
				
			|||
        let currentValueOfSymbol = this.exchangeRateDataService.toCurrency( | 
				
			|||
          orderOfSymbol.getQuantity() * | 
				
			|||
            portfolioItemsNow.positions[symbol].marketPrice, | 
				
			|||
          orderOfSymbol.getCurrency(), | 
				
			|||
          this.user.Settings.currency | 
				
			|||
        ); | 
				
			|||
        let originalValueOfSymbol = this.exchangeRateDataService.toCurrency( | 
				
			|||
          orderOfSymbol.getQuantity() * orderOfSymbol.getUnitPrice(), | 
				
			|||
          orderOfSymbol.getCurrency(), | 
				
			|||
          this.user.Settings.currency | 
				
			|||
        ); | 
				
			|||
 | 
				
			|||
        if (orderOfSymbol.getType() === 'SELL') { | 
				
			|||
          currentValueOfSymbol *= -1; | 
				
			|||
          originalValueOfSymbol *= -1; | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
        if ( | 
				
			|||
          accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY]?.current | 
				
			|||
        ) { | 
				
			|||
          accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY].current += | 
				
			|||
            currentValueOfSymbol; | 
				
			|||
          accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY].original += | 
				
			|||
            originalValueOfSymbol; | 
				
			|||
        } else { | 
				
			|||
          accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY] = { | 
				
			|||
            current: currentValueOfSymbol, | 
				
			|||
            original: originalValueOfSymbol | 
				
			|||
          }; | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
        countriesOfSymbol = ( | 
				
			|||
          (orderOfSymbol.getSymbolProfile()?.countries as Prisma.JsonArray) ?? | 
				
			|||
          [] | 
				
			|||
        ).map((country) => { | 
				
			|||
          const { code, weight } = country as Prisma.JsonObject; | 
				
			|||
 | 
				
			|||
          return { | 
				
			|||
            code: code as string, | 
				
			|||
            continent: | 
				
			|||
              continents[countries[code as string]?.continent] ?? UNKNOWN_KEY, | 
				
			|||
            name: countries[code as string]?.name ?? UNKNOWN_KEY, | 
				
			|||
            weight: weight as number | 
				
			|||
          }; | 
				
			|||
        }); | 
				
			|||
 | 
				
			|||
        sectorsOfSymbol = ( | 
				
			|||
          (orderOfSymbol.getSymbolProfile()?.sectors as Prisma.JsonArray) ?? [] | 
				
			|||
        ).map((sector) => { | 
				
			|||
          const { name, weight } = sector as Prisma.JsonObject; | 
				
			|||
 | 
				
			|||
          return { | 
				
			|||
            name: (name as string) ?? UNKNOWN_KEY, | 
				
			|||
            weight: weight as number | 
				
			|||
          }; | 
				
			|||
        }); | 
				
			|||
      }); | 
				
			|||
 | 
				
			|||
      let now = portfolioItemsNow.positions[symbol].marketPrice; | 
				
			|||
 | 
				
			|||
      // 1d
 | 
				
			|||
      let before = portfolioItemsBefore?.positions[symbol].marketPrice; | 
				
			|||
 | 
				
			|||
      if (aDateRange === 'ytd') { | 
				
			|||
        before = | 
				
			|||
          portfolioItemsBefore.positions[symbol].marketPrice || | 
				
			|||
          portfolioItemsNow.positions[symbol].averagePrice; | 
				
			|||
      } else if ( | 
				
			|||
        aDateRange === '1y' || | 
				
			|||
        aDateRange === '5y' || | 
				
			|||
        aDateRange === 'max' | 
				
			|||
      ) { | 
				
			|||
        before = portfolioItemsNow.positions[symbol].averagePrice; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      if ( | 
				
			|||
        !isBefore( | 
				
			|||
          parseISO(portfolioItemsNow.positions[symbol].firstBuyDate), | 
				
			|||
          parseISO(portfolioItemsBefore?.date) | 
				
			|||
        ) | 
				
			|||
      ) { | 
				
			|||
        // Trade was not before the date of portfolioItemsBefore, then override it with average price
 | 
				
			|||
        // (e.g. on same day)
 | 
				
			|||
        before = portfolioItemsNow.positions[symbol].averagePrice; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      if (isToday(parseISO(portfolioItemsNow.positions[symbol].firstBuyDate))) { | 
				
			|||
        now = portfolioItemsNow.positions[symbol].averagePrice; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      details[symbol] = { | 
				
			|||
        ...data[symbol], | 
				
			|||
        accounts, | 
				
			|||
        symbol, | 
				
			|||
        allocationCurrent: | 
				
			|||
          this.exchangeRateDataService.toCurrency( | 
				
			|||
            portfolioItem.positions[symbol].quantity * now, | 
				
			|||
            data[symbol]?.currency, | 
				
			|||
            this.user.Settings.currency | 
				
			|||
          ) / value, | 
				
			|||
        allocationInvestment: | 
				
			|||
          portfolioItem.positions[symbol].investment / investment, | 
				
			|||
        countries: countriesOfSymbol, | 
				
			|||
        grossPerformance: roundTo( | 
				
			|||
          portfolioItemsNow.positions[symbol].quantity * (now - before), | 
				
			|||
          2 | 
				
			|||
        ), | 
				
			|||
        grossPerformancePercent: roundTo((now - before) / before, 4), | 
				
			|||
        investment: portfolioItem.positions[symbol].investment, | 
				
			|||
        quantity: portfolioItem.positions[symbol].quantity, | 
				
			|||
        sectors: sectorsOfSymbol, | 
				
			|||
        transactionCount: portfolioItem.positions[symbol].transactionCount, | 
				
			|||
        value: this.exchangeRateDataService.toCurrency( | 
				
			|||
          portfolioItem.positions[symbol].quantity * now, | 
				
			|||
          data[symbol]?.currency, | 
				
			|||
          this.user.Settings.currency | 
				
			|||
        ) | 
				
			|||
      }; | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    details[ghostfolioCashSymbol] = await this.getCashPosition({ | 
				
			|||
      cashDetails, | 
				
			|||
      investment, | 
				
			|||
      value | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    return details; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public getFees(aDate = new Date(0)) { | 
				
			|||
    return this.orders | 
				
			|||
      .filter((order) => { | 
				
			|||
        // Filter out all orders before given date
 | 
				
			|||
        return isBefore(aDate, new Date(order.getDate())); | 
				
			|||
      }) | 
				
			|||
      .map((order) => { | 
				
			|||
        return this.exchangeRateDataService.toCurrency( | 
				
			|||
          order.getFee(), | 
				
			|||
          order.getCurrency(), | 
				
			|||
          this.user.Settings.currency | 
				
			|||
        ); | 
				
			|||
      }) | 
				
			|||
      .reduce((previous, current) => previous + current, 0); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public getInvestment(aDate: Date): number { | 
				
			|||
    return this.get(aDate)[0]?.investment || 0; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public getMinDate() { | 
				
			|||
    const orders = this.getOrders().filter( | 
				
			|||
      (order) => order.getIsDraft() === false | 
				
			|||
    ); | 
				
			|||
 | 
				
			|||
    if (orders.length > 0) { | 
				
			|||
      return new Date(this.orders[0].getDate()); | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    return null; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public getPositions(aDate: Date) { | 
				
			|||
    const [portfolioItem] = this.get(aDate); | 
				
			|||
 | 
				
			|||
    if (portfolioItem) { | 
				
			|||
      return portfolioItem.positions; | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    return {}; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public getPortfolioItems() { | 
				
			|||
    return this.portfolioItems; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public getSymbols(aDate?: Date) { | 
				
			|||
    let symbols: string[] = []; | 
				
			|||
 | 
				
			|||
    if (aDate) { | 
				
			|||
      const positions = this.getPositions(aDate); | 
				
			|||
 | 
				
			|||
      for (const symbol in positions) { | 
				
			|||
        if (positions[symbol].quantity > 0) { | 
				
			|||
          symbols.push(symbol); | 
				
			|||
        } | 
				
			|||
      } | 
				
			|||
    } else { | 
				
			|||
      symbols = this.orders | 
				
			|||
        .filter((order) => order.getIsDraft() === false) | 
				
			|||
        .map((order) => { | 
				
			|||
          return order.getSymbol(); | 
				
			|||
        }); | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    // unique values
 | 
				
			|||
    return Array.from(new Set(symbols)); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public getTotalBuy() { | 
				
			|||
    return this.orders | 
				
			|||
      .filter( | 
				
			|||
        (order) => order.getIsDraft() === false && order.getType() === 'BUY' | 
				
			|||
      ) | 
				
			|||
      .map((order) => { | 
				
			|||
        return this.exchangeRateDataService.toCurrency( | 
				
			|||
          order.getTotal(), | 
				
			|||
          order.getCurrency(), | 
				
			|||
          this.user.Settings.currency | 
				
			|||
        ); | 
				
			|||
      }) | 
				
			|||
      .reduce((previous, current) => previous + current, 0); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public getTotalSell() { | 
				
			|||
    return this.orders | 
				
			|||
      .filter( | 
				
			|||
        (order) => order.getIsDraft() === false && order.getType() === 'SELL' | 
				
			|||
      ) | 
				
			|||
      .map((order) => { | 
				
			|||
        return this.exchangeRateDataService.toCurrency( | 
				
			|||
          order.getTotal(), | 
				
			|||
          order.getCurrency(), | 
				
			|||
          this.user.Settings.currency | 
				
			|||
        ); | 
				
			|||
      }) | 
				
			|||
      .reduce((previous, current) => previous + current, 0); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public getOrders(aSymbol?: string) { | 
				
			|||
    if (aSymbol) { | 
				
			|||
      return this.orders.filter((order) => { | 
				
			|||
        return order.getSymbol() === aSymbol; | 
				
			|||
      }); | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    return this.orders; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public getValue(aDate = getToday()) { | 
				
			|||
    const positions = this.getPositions(aDate); | 
				
			|||
    let value = 0; | 
				
			|||
 | 
				
			|||
    const [portfolioItem] = this.get(aDate); | 
				
			|||
 | 
				
			|||
    for (const symbol in positions) { | 
				
			|||
      if (portfolioItem.positions[symbol]?.quantity > 0) { | 
				
			|||
        if ( | 
				
			|||
          isBefore( | 
				
			|||
            aDate, | 
				
			|||
            parseISO(portfolioItem.positions[symbol]?.firstBuyDate) | 
				
			|||
          ) || | 
				
			|||
          portfolioItem.positions[symbol]?.marketPrice === 0 | 
				
			|||
        ) { | 
				
			|||
          value += this.exchangeRateDataService.toCurrency( | 
				
			|||
            portfolioItem.positions[symbol]?.quantity * | 
				
			|||
              portfolioItem.positions[symbol]?.averagePrice, | 
				
			|||
            portfolioItem.positions[symbol]?.currency, | 
				
			|||
            this.user.Settings.currency | 
				
			|||
          ); | 
				
			|||
        } else { | 
				
			|||
          value += this.exchangeRateDataService.toCurrency( | 
				
			|||
            portfolioItem.positions[symbol]?.quantity * | 
				
			|||
              portfolioItem.positions[symbol]?.marketPrice, | 
				
			|||
            portfolioItem.positions[symbol]?.currency, | 
				
			|||
            this.user.Settings.currency | 
				
			|||
          ); | 
				
			|||
        } | 
				
			|||
      } | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    return isFinite(value) ? value : null; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public async setOrders(aOrders: OrderWithAccount[]) { | 
				
			|||
    this.orders = []; | 
				
			|||
 | 
				
			|||
    // Map data
 | 
				
			|||
    aOrders.forEach((order) => { | 
				
			|||
      this.orders.push( | 
				
			|||
        new Order({ | 
				
			|||
          account: order.Account, | 
				
			|||
          currency: order.currency, | 
				
			|||
          date: order.date.toISOString(), | 
				
			|||
          fee: order.fee, | 
				
			|||
          quantity: order.quantity, | 
				
			|||
          symbol: order.symbol, | 
				
			|||
          symbolProfile: order.SymbolProfile, | 
				
			|||
          type: <OrderType>order.type, | 
				
			|||
          unitPrice: order.unitPrice | 
				
			|||
        }) | 
				
			|||
      ); | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    await this.update(); | 
				
			|||
 | 
				
			|||
    return this; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public setUser(aUser: UserWithSettings) { | 
				
			|||
    this.user = aUser; | 
				
			|||
 | 
				
			|||
    return this; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  private async getCashPosition({ | 
				
			|||
    cashDetails, | 
				
			|||
    investment, | 
				
			|||
    value | 
				
			|||
  }: { | 
				
			|||
    cashDetails: CashDetails; | 
				
			|||
    investment: number; | 
				
			|||
    value: number; | 
				
			|||
  }) { | 
				
			|||
    const accounts = {}; | 
				
			|||
    const cashValue = cashDetails.balance; | 
				
			|||
 | 
				
			|||
    cashDetails.accounts.forEach((account) => { | 
				
			|||
      accounts[account.name] = { | 
				
			|||
        current: account.balance, | 
				
			|||
        original: account.balance | 
				
			|||
      }; | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    return { | 
				
			|||
      accounts, | 
				
			|||
      allocationCurrent: cashValue / value, | 
				
			|||
      allocationInvestment: cashValue / investment, | 
				
			|||
      countries: [], | 
				
			|||
      currency: Currency.CHF, | 
				
			|||
      grossPerformance: 0, | 
				
			|||
      grossPerformancePercent: 0, | 
				
			|||
      investment: cashValue, | 
				
			|||
      marketPrice: 0, | 
				
			|||
      marketState: MarketState.open, | 
				
			|||
      name: Type.Cash, | 
				
			|||
      quantity: 0, | 
				
			|||
      sectors: [], | 
				
			|||
      symbol: ghostfolioCashSymbol, | 
				
			|||
      type: Type.Cash, | 
				
			|||
      transactionCount: 0, | 
				
			|||
      value: cashValue | 
				
			|||
    }; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  /** | 
				
			|||
   * TODO: Refactor | 
				
			|||
   */ | 
				
			|||
  private async update() { | 
				
			|||
    this.portfolioItems = []; | 
				
			|||
 | 
				
			|||
    let currentDate = this.getMinDate(); | 
				
			|||
 | 
				
			|||
    if (!currentDate) { | 
				
			|||
      return; | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    // Set current date to first of month
 | 
				
			|||
    currentDate = setDate(currentDate, 1); | 
				
			|||
 | 
				
			|||
    const historicalData = await this.dataProviderService.getHistorical( | 
				
			|||
      this.getSymbols(), | 
				
			|||
      'month', | 
				
			|||
      currentDate, | 
				
			|||
      new Date() | 
				
			|||
    ); | 
				
			|||
 | 
				
			|||
    while (isBefore(currentDate, Date.now())) { | 
				
			|||
      const positions: { [symbol: string]: Position } = {}; | 
				
			|||
      this.getSymbols().forEach((symbol) => { | 
				
			|||
        positions[symbol] = { | 
				
			|||
          symbol, | 
				
			|||
          averagePrice: 0, | 
				
			|||
          currency: undefined, | 
				
			|||
          firstBuyDate: null, | 
				
			|||
          investment: 0, | 
				
			|||
          investmentInOriginalCurrency: 0, | 
				
			|||
          marketPrice: | 
				
			|||
            historicalData[symbol]?.[format(currentDate, DATE_FORMAT)] | 
				
			|||
              ?.marketPrice || 0, | 
				
			|||
          quantity: 0, | 
				
			|||
          transactionCount: 0 | 
				
			|||
        }; | 
				
			|||
      }); | 
				
			|||
 | 
				
			|||
      if (!isYesterday(currentDate) && !isToday(currentDate)) { | 
				
			|||
        // Add to portfolio (ignore yesterday and today because they are added later)
 | 
				
			|||
        this.portfolioItems.push( | 
				
			|||
          cloneDeep({ | 
				
			|||
            date: currentDate.toISOString(), | 
				
			|||
            grossPerformancePercent: 0, | 
				
			|||
            investment: 0, | 
				
			|||
            positions: positions, | 
				
			|||
            value: 0 | 
				
			|||
          }) | 
				
			|||
        ); | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      const year = getYear(currentDate); | 
				
			|||
      const month = getMonth(currentDate); | 
				
			|||
      const day = getDate(currentDate); | 
				
			|||
 | 
				
			|||
      // Count month one up for iteration
 | 
				
			|||
      currentDate = new Date(Date.UTC(year, month + 1, day, 0)); | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    const yesterday = getYesterday(); | 
				
			|||
 | 
				
			|||
    const positions: { [symbol: string]: Position } = {}; | 
				
			|||
 | 
				
			|||
    if (isAfter(yesterday, this.getMinDate())) { | 
				
			|||
      // Add yesterday
 | 
				
			|||
      this.getSymbols().forEach((symbol) => { | 
				
			|||
        positions[symbol] = { | 
				
			|||
          symbol, | 
				
			|||
          averagePrice: 0, | 
				
			|||
          currency: undefined, | 
				
			|||
          firstBuyDate: null, | 
				
			|||
          investment: 0, | 
				
			|||
          investmentInOriginalCurrency: 0, | 
				
			|||
          marketPrice: | 
				
			|||
            historicalData[symbol]?.[format(yesterday, DATE_FORMAT)] | 
				
			|||
              ?.marketPrice || 0, | 
				
			|||
          name: '', | 
				
			|||
          quantity: 0, | 
				
			|||
          transactionCount: 0 | 
				
			|||
        }; | 
				
			|||
      }); | 
				
			|||
 | 
				
			|||
      this.portfolioItems.push( | 
				
			|||
        cloneDeep({ | 
				
			|||
          positions, | 
				
			|||
          date: yesterday.toISOString(), | 
				
			|||
          grossPerformancePercent: 0, | 
				
			|||
          investment: 0, | 
				
			|||
          value: 0 | 
				
			|||
        }) | 
				
			|||
      ); | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    this.updatePortfolioItems(); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  private convertDateRangeToDate(aDateRange: DateRange, aMinDate: Date) { | 
				
			|||
    let currentDate = new Date(); | 
				
			|||
 | 
				
			|||
    const normalizedMinDate = | 
				
			|||
      getDate(aMinDate) === 1 | 
				
			|||
        ? aMinDate | 
				
			|||
        : add(setDate(aMinDate, 1), { months: 1 }); | 
				
			|||
 | 
				
			|||
    const year = getYear(currentDate); | 
				
			|||
    const month = getMonth(currentDate); | 
				
			|||
    const day = getDate(currentDate); | 
				
			|||
 | 
				
			|||
    currentDate = new Date(Date.UTC(year, month, day, 0)); | 
				
			|||
 | 
				
			|||
    switch (aDateRange) { | 
				
			|||
      case '1d': | 
				
			|||
        return sub(currentDate, { | 
				
			|||
          days: 1 | 
				
			|||
        }); | 
				
			|||
      case 'ytd': | 
				
			|||
        currentDate = setDate(currentDate, 1); | 
				
			|||
        currentDate = setMonth(currentDate, 0); | 
				
			|||
        return isAfter(currentDate, normalizedMinDate) | 
				
			|||
          ? currentDate | 
				
			|||
          : undefined; | 
				
			|||
      case '1y': | 
				
			|||
        currentDate = setDate(currentDate, 1); | 
				
			|||
        currentDate = sub(currentDate, { | 
				
			|||
          years: 1 | 
				
			|||
        }); | 
				
			|||
        return isAfter(currentDate, normalizedMinDate) | 
				
			|||
          ? currentDate | 
				
			|||
          : undefined; | 
				
			|||
      case '5y': | 
				
			|||
        currentDate = setDate(currentDate, 1); | 
				
			|||
        currentDate = sub(currentDate, { | 
				
			|||
          years: 5 | 
				
			|||
        }); | 
				
			|||
        return isAfter(currentDate, normalizedMinDate) | 
				
			|||
          ? currentDate | 
				
			|||
          : undefined; | 
				
			|||
      default: | 
				
			|||
        // Gets handled as all data
 | 
				
			|||
        return undefined; | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  private updatePortfolioItems() { | 
				
			|||
    let currentDate = new Date(); | 
				
			|||
 | 
				
			|||
    const year = getYear(currentDate); | 
				
			|||
    const month = getMonth(currentDate); | 
				
			|||
    const day = getDate(currentDate); | 
				
			|||
 | 
				
			|||
    currentDate = new Date(Date.UTC(year, month, day, 0)); | 
				
			|||
 | 
				
			|||
    if (this.portfolioItems?.length === 1) { | 
				
			|||
      // At least one portfolio items is needed, keep it but change the date to today.
 | 
				
			|||
      // This happens if there are only orders from today
 | 
				
			|||
      this.portfolioItems[0].date = currentDate.toISOString(); | 
				
			|||
    } else { | 
				
			|||
      // Only keep entries which are not before first buy date
 | 
				
			|||
      this.portfolioItems = this.portfolioItems.filter((portfolioItem) => { | 
				
			|||
        return ( | 
				
			|||
          isSameDay(parseISO(portfolioItem.date), this.getMinDate()) || | 
				
			|||
          isAfter(parseISO(portfolioItem.date), this.getMinDate()) | 
				
			|||
        ); | 
				
			|||
      }); | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    this.orders.forEach((order) => { | 
				
			|||
      if (order.getIsDraft() === false) { | 
				
			|||
        let index = this.portfolioItems.findIndex((item) => { | 
				
			|||
          const dateOfOrder = setDate(parseISO(order.getDate()), 1); | 
				
			|||
          return isSameDay(parseISO(item.date), dateOfOrder); | 
				
			|||
        }); | 
				
			|||
 | 
				
			|||
        if (index === -1) { | 
				
			|||
          // if not found, we only have one order, which means we do not loop below
 | 
				
			|||
          index = 0; | 
				
			|||
        } | 
				
			|||
 | 
				
			|||
        for (let i = index; i < this.portfolioItems.length; i++) { | 
				
			|||
          // Set currency
 | 
				
			|||
          this.portfolioItems[i].positions[order.getSymbol()].currency = | 
				
			|||
            order.getCurrency(); | 
				
			|||
 | 
				
			|||
          this.portfolioItems[i].positions[ | 
				
			|||
            order.getSymbol() | 
				
			|||
          ].transactionCount += 1; | 
				
			|||
 | 
				
			|||
          if (order.getType() === 'BUY') { | 
				
			|||
            if ( | 
				
			|||
              !this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate | 
				
			|||
            ) { | 
				
			|||
              this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate = | 
				
			|||
                resetHours(parseISO(order.getDate())).toISOString(); | 
				
			|||
            } | 
				
			|||
 | 
				
			|||
            this.portfolioItems[i].positions[order.getSymbol()].quantity += | 
				
			|||
              order.getQuantity(); | 
				
			|||
            this.portfolioItems[i].positions[order.getSymbol()].investment += | 
				
			|||
              this.exchangeRateDataService.toCurrency( | 
				
			|||
                order.getTotal(), | 
				
			|||
                order.getCurrency(), | 
				
			|||
                this.user.Settings.currency | 
				
			|||
              ); | 
				
			|||
            this.portfolioItems[i].positions[ | 
				
			|||
              order.getSymbol() | 
				
			|||
            ].investmentInOriginalCurrency += order.getTotal(); | 
				
			|||
 | 
				
			|||
            this.portfolioItems[i].investment += | 
				
			|||
              this.exchangeRateDataService.toCurrency( | 
				
			|||
                order.getTotal(), | 
				
			|||
                order.getCurrency(), | 
				
			|||
                this.user.Settings.currency | 
				
			|||
              ); | 
				
			|||
          } else if (order.getType() === 'SELL') { | 
				
			|||
            this.portfolioItems[i].positions[order.getSymbol()].quantity -= | 
				
			|||
              order.getQuantity(); | 
				
			|||
 | 
				
			|||
            if ( | 
				
			|||
              this.portfolioItems[i].positions[order.getSymbol()].quantity === 0 | 
				
			|||
            ) { | 
				
			|||
              this.portfolioItems[i].positions[ | 
				
			|||
                order.getSymbol() | 
				
			|||
              ].investment = 0; | 
				
			|||
              this.portfolioItems[i].positions[ | 
				
			|||
                order.getSymbol() | 
				
			|||
              ].investmentInOriginalCurrency = 0; | 
				
			|||
            } else { | 
				
			|||
              this.portfolioItems[i].positions[order.getSymbol()].investment -= | 
				
			|||
                this.exchangeRateDataService.toCurrency( | 
				
			|||
                  order.getTotal(), | 
				
			|||
                  order.getCurrency(), | 
				
			|||
                  this.user.Settings.currency | 
				
			|||
                ); | 
				
			|||
              this.portfolioItems[i].positions[ | 
				
			|||
                order.getSymbol() | 
				
			|||
              ].investmentInOriginalCurrency -= order.getTotal(); | 
				
			|||
            } | 
				
			|||
 | 
				
			|||
            this.portfolioItems[i].investment -= | 
				
			|||
              this.exchangeRateDataService.toCurrency( | 
				
			|||
                order.getTotal(), | 
				
			|||
                order.getCurrency(), | 
				
			|||
                this.user.Settings.currency | 
				
			|||
              ); | 
				
			|||
          } | 
				
			|||
 | 
				
			|||
          this.portfolioItems[i].positions[order.getSymbol()].averagePrice = | 
				
			|||
            this.portfolioItems[i].positions[order.getSymbol()] | 
				
			|||
              .investmentInOriginalCurrency / | 
				
			|||
            this.portfolioItems[i].positions[order.getSymbol()].quantity; | 
				
			|||
 | 
				
			|||
          const currentValue = this.getValue( | 
				
			|||
            parseISO(this.portfolioItems[i].date) | 
				
			|||
          ); | 
				
			|||
 | 
				
			|||
          this.portfolioItems[i].grossPerformancePercent = | 
				
			|||
            currentValue / this.portfolioItems[i].investment - 1 || 0; | 
				
			|||
          this.portfolioItems[i].value = currentValue; | 
				
			|||
        } | 
				
			|||
      } | 
				
			|||
    }); | 
				
			|||
  } | 
				
			|||
} | 
				
			|||
					Loading…
					
					
				
		Reference in new issue