Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Investments tracking #110

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
d996a3d
feat: Add models and migrations
letehaha Jan 29, 2024
5f0039c
feat: Improve models typing
letehaha Jan 29, 2024
03764c7
feat: Add load/sync securities implementation
letehaha Jan 29, 2024
9b68a1e
chore: Investments progress
letehaha Feb 4, 2024
b7e207e
feat: Add securities syncing
letehaha Apr 7, 2024
eeede34
chore: Remove securities mocks from .gitignore & optimize their size
letehaha Apr 8, 2024
7e942ec
feat: Add fetch and create of Holdings
letehaha Apr 8, 2024
020fc04
feat: Extend auth token to 24h
letehaha Apr 9, 2024
1a12384
fix: `HoldingAttributes` definition
letehaha Apr 9, 2024
14a07d9
feat: Load securities by query
letehaha Apr 12, 2024
c9d4124
feat: Improve securities sync and add investment tx creation
letehaha Apr 13, 2024
ac4adfe
feat: Holding data calculation on new invest tx
letehaha Apr 13, 2024
53a1012
feat: Update shared-types
letehaha Apr 14, 2024
9a337e4
feat(docs): Add comments to `SecurityModel`
letehaha Apr 14, 2024
905bdaf
feat: Load investment transactions list
letehaha Apr 19, 2024
fe63ce4
fix: refCostBasis calculation
letehaha Apr 21, 2024
4b47dca
chore: Organize holdings service better
letehaha Apr 21, 2024
ee9c56d
chore: Load securities list to global tests scope
letehaha Jun 11, 2024
c9b6629
feat: Add holdings creation e2e tests and restructure code a bit
letehaha Jun 11, 2024
91a619e
chore: Add holdings get/create tests helpers
letehaha Jun 11, 2024
989d5e3
feat: Holdings loading controller
letehaha Jun 11, 2024
72bd0c9
feat: Allow nullish value for `name` of `InvestmentTransaction`
letehaha Jun 11, 2024
6c1918f
feat: Add basic test for investment transaction creation
letehaha Jun 11, 2024
888d0e9
fix: Migrations order
letehaha Sep 22, 2024
1d66468
chore: prettier
letehaha Sep 25, 2024
37197d2
feat: Improve securities syncing
letehaha Sep 26, 2024
dbcec50
refactor: Balances table updation on tx deletion
letehaha Sep 26, 2024
ab8cef2
refactor: Split bunch of accounts services into separate files
letehaha Sep 26, 2024
3fe2232
refactor: split more accounts services into files
letehaha Sep 26, 2024
26218a1
refactor: split accounts controller into files and add input validation
letehaha Sep 26, 2024
4357c03
chore: Adjust jest timeout for investments tests
letehaha Sep 26, 2024
b05226b
chore: remove logging when running tests
letehaha Sep 26, 2024
e0ce28e
refactor: Move balances updation out of tx onBeforeDestroy hook
letehaha Sep 27, 2024
9d097e1
chore: cleanup
letehaha Sep 27, 2024
f20e0e6
chore: cleanup useless code
letehaha Sep 27, 2024
7b72357
refactor: Account creation balance updation
letehaha Sep 28, 2024
fff2db2
refactor: improve account updation service params
letehaha Sep 28, 2024
65abce3
fix: Wrong balances table updation + add tests for it
letehaha Sep 28, 2024
0de98fb
feat: Extend balances updation tests
letehaha Sep 29, 2024
3848745
chore: Move balances tests to a correct place
letehaha Sep 29, 2024
428561e
chore: Add checls for account `refCurrentBalance`
letehaha Sep 29, 2024
af44d94
refactor: Move account updation to a separate service out of Transact…
letehaha Sep 29, 2024
0d78d86
feat: Upgrade TS version
letehaha Sep 29, 2024
9dc983c
refactor: Move balances updation out of Transactions model
letehaha Sep 29, 2024
a769d32
feat: Add investments tx controller and more tests
letehaha Sep 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
358 changes: 320 additions & 38 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
"jsonwebtoken": "9.0.2",
"locale": "0.1.0",
"morgan": "1.10.0",
"@polygon.io/client-js": "^7.3.2",
"lodash": "^4.17.21",
"p-queue": "6.6.2",
"passport": "0.6.0",
"passport-jwt": "4.0.1",
Expand All @@ -80,7 +82,7 @@
"sequelize-cli": "6.6.2",
"sequelize-typescript": "2.1.6",
"ts-node": "10.9.2",
"typescript": "5.4.5",
"typescript": "5.6.2",
"uuid": "9.0.1",
"winston": "3.13.0",
"zod": "^3.23.8"
Expand Down Expand Up @@ -109,7 +111,8 @@
"nodemon": "3.1.0",
"prettier": "3.2.5",
"supertest": "6.3.4",
"ts-jest": "29.1.2",
"tslint": "6.1.3"
"ts-jest": "29.2.5",
"tslint": "6.1.3",
"@types/sequelize": "^4.28.20"
}
}
1 change: 1 addition & 0 deletions shared-types/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum API_ERROR_CODES {
conflict = 'CONFLICT',
forbidden = 'FORBIDDEN',
BadRequest = 'BAD_REQUEST',
locked = 'LOCKED',

// auth
unauthorized = 'UNAUTHENTICATED',
Expand Down
244 changes: 244 additions & 0 deletions shared-types/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,247 @@ export interface UserExchangeRatesModel extends ExchangeRatesModel {
userId: number;
custom?: boolean;
}

export enum SECURITY_PROVIDER {
polygon = 'polygon',
other = 'other',
}

export enum ASSET_CLASS {
cash = 'cash',
crypto = 'crypto',
fixed_income = 'fixed_income',
options = 'options',
stocks = 'stocks',
other = 'other',
}

export interface SecurityModel {
id: number;
/**
* The name of the security, typically the official name of the stock, bond,
* or other financial instrument.
*/
name?: string;

/**
* The trading symbol or ticker associated with the security, used to uniquely
* identify it on stock exchanges.
*/
symbol?: string;

/**
* The CUSIP number (Committee on Uniform Securities Identification Procedures) is a unique identifier
* assigned to U.S. and Canadian securities for the purposes of facilitating clearing and settlement of trades.
*/
cusip?: string;

/**
* The ISIN number (International Securities Identification Number) is a unique code assigned to securities
* internationally for uniform identification, which helps in reducing the risk of ambiguities in international trading.
*/
isin?: string;

/**
* (Applicable for derivative securities) The number of shares or units
* represented by a single contract, commonly used in options and futures trading.
*/
sharesPerContract?: string;

/**
* The ISO currency code representing the currency in which the transactions
* will be conducted. For cryptocurrencies, this code refers to
* the specific currency linked to it (e.g., USD for BTC-USD, EUR for BTC-EUR).
*/
currencyCode: string;

/**
* Crypto currency code for crypto securities. Since ticker represents not
* just a crypto token, but an actual pair, we need to store symbol separately
* (e.g., BTC for BTC-USD, BTC for BTC-EUR).
*/
cryptoCurrencyCode?: string;

/**
* The timestamp indicating the last time the pricing information for this
* security was updated.
*/
pricingLastSyncedAt?: Date;

/**
* A flag indicating whether the security is considered as cash within a brokerage account.
* This is often used for cash management in investment portfolios.
*/
isBrokerageCash: boolean;

/**
* The acronym or shorthand for the exchange where this security is traded,
* which provides an easy reference to identify the trading platform.
*
* Example:
* NYSE – New York Stock Exchange
*/
exchangeAcronym?: string;

/**
* The Market Identifier Code (MIC) as per ISO standard, representing the exchange where the security is traded.
* MIC is a unique identification code used to identify securities trading exchanges and market platforms globally.
* Read more: https://www.investopedia.com/terms/m/mic.asp
*/
exchangeMic?: string;

/**
* The full name of the exchange where the security is listed and traded.
* This helps in clearly identifying the specific market platform.
*/
exchangeName?: string;

/**
* The name of the data provider or the source from which the security's information is obtained.
* Enumerated values represent various recognized data providers.
*
* Useful for next cases:
* 1. If more providers will be added and table will be expanded, by this field
* we can identify provider-specific fields and features.
* 2. Easy to refresh data when multiple providers exists.
* 3. Service price, licensing, auditing, data source verification – help for
* any legal cases
*/
providerName: SECURITY_PROVIDER;

/**
* The category of assets to which this security belongs.
* Enumerated values represent different classes of assets such as stocks, bonds, etc.
*/
assetClass: ASSET_CLASS;
createdAt: Date;
updatedAt: Date;

holdings?: HoldingModel[];
investmentTransactions?: InvestmentTransactionModel[];
pricing?: SecurityPricingModel[];
}

export interface HoldingModel {
accountId: number;
securityId: number;
value: string;
refValue: string;
quantity: string;
costBasis: string;
refCostBasis: string;
currencyCode: string;
excluded: boolean;
account?: AccountModel;
security?: SecurityModel;
createdAt: Date;
updatedAt: Date;
}

export enum INVESTMENT_TRANSACTION_CATEGORY {
buy = 'buy',
sell = 'sell',
dividend = 'dividend',
transfer = 'transfer',
tax = 'tax',
fee = 'fee',
cancel = 'cancel',
other = 'other',
}

export interface InvestmentTransactionModel {
id: number;
/**
* The identifier of the account associated with this transaction.
* It links the transaction to a specific investment account.
*/
accountId: number;
securityId: number;
transactionType: TRANSACTION_TYPES;
date: string;
/**
* A descriptive name or title for the investment transaction, providing a
* quick overview of the transaction's nature. Same as `note` in `Transactions`
*/
name: string | null;
/**
* The monetary value involved in the transaction. Depending on the context,
* this could represent the cost, sale proceeds, or other financial values
* associated with the transaction. Basically quantity * price
*/
amount: string;
refAmount: string;

fees: string;
refFees: string;

/**
* * The quantity of the security involved in the transaction. This is crucial
* for tracking the changes in holdings as a result of the transaction.
*/
quantity: string;

/**
* The price per unit of the security at the time of the transaction.
* This is used to calculate the total transaction amount and update the cost
* basis of the holding.
*/
price: string;
refPrice: string;

/**
* The ISO currency code or standard cryptocurrency code representing the currency
* in which the transaction was conducted. For cryptocurrencies, this code refers to
* the specific cryptocurrency involved (e.g., BTC for Bitcoin, ETH for Ethereum).
*/
currencyCode: string;
/**
* A category that classifies the nature of the investment transaction.
* This could include types like 'buy', 'sell', 'dividend', 'interest', etc.,
* providing a clear context for the transaction's purpose and impact on the investment portfolio.
*/
category: INVESTMENT_TRANSACTION_CATEGORY;
/**
* "transferNature" and "transferId" are used to move funds between different
* accounts and don't affect income/expense stats.
*/
transferNature: TRANSACTION_TRANSFER_NATURE;
// (hash, used to connect two transactions)
transferId: string;
updatedAt: Date;
createdAt: Date;

security?: SecurityModel;
account?: AccountModel;
}

export interface SecurityPricingModel {
securityId: number;
/**
* The date for which this pricing information is applicable. This field is crucial for tracking
* the historical prices of securities and allows for analysis of price trends over time.
* dd-mm-yyyy
*/
date: Date;
/**
* The closing price of the security on the specified date. Closing prices are typically used in
* financial analysis and reporting as they represent the final price at which the security was traded
* during the trading session.
*/
priceClose: string;
/**
* (Optional) The timestamp indicating the specific time the priceClose was recorded. This is particularly
* useful when multiple price updates occur within a single day or for real-time price tracking.
*/
priceAsOf: Date;
/**
* (Optional) A field indicating the source of the pricing information. This could be the name of the
* data provider or the market/exchange from which the price was obtained. This field helps in
* tracking the reliability and origin of the data.
*/
source: string;

updatedAt: Date;
createdAt: Date;
security?: SecurityModel;
}
5 changes: 0 additions & 5 deletions shared-types/routes/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ import { QueryPayload } from './index';

export interface GetTransactionsQuery extends QueryPayload {
sort?: SORT_DIRECTIONS;
includeUser?: boolean;
includeAccount?: boolean;
includeCategory?: boolean;
includeAll?: boolean;
nestedInclude?: boolean;
limit?: number;
from?: number;
type?: TRANSACTION_TYPES;
Expand Down
2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import modelsCurrenciesRoutes from './routes/currencies.route';
import monobankRoutes from './routes/banks/monobank.route';
import binanceRoutes from './routes/crypto/binance.route';
import statsRoutes from './routes/stats.route';
import investmentsRoutes from './routes/investments.route';

import { supportedLocales } from './translations';

Expand Down Expand Up @@ -78,6 +79,7 @@ app.use(`${apiPrefix}/models/currencies`, modelsCurrenciesRoutes);
app.use(`${apiPrefix}/banks/monobank`, monobankRoutes);
app.use(`${apiPrefix}/crypto/binance`, binanceRoutes);
app.use(`${apiPrefix}/stats`, statsRoutes);
app.use(`${apiPrefix}/investing`, investmentsRoutes);

// Cause some tests can be parallelized, the port might be in use, so we need to allow dynamic port
export const serverInstance = app.listen(
Expand Down
9 changes: 7 additions & 2 deletions src/common/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ export const getQueryBooleanValue = (value: string): boolean => {
// To wait until `fn` returns true
export const until = async <T>(
fn: () => Promise<T> | T,
timeout: number = 30_000,
interval: number = 500,
{
timeout = 30_000,
interval = 500,
}: {
timeout?: number;
interval?: number;
} = {},
): Promise<void> => {
const startTime = Date.now();

Expand Down
Loading
Loading