From 159c3d08eda829982a9064c04e0d393838c19c3b Mon Sep 17 00:00:00 2001 From: Baozier Date: Wed, 13 Sep 2023 17:40:48 +0800 Subject: [PATCH 1/5] rewrite sync function --- interfaces/models/dailyTickers.ts | 5 +- interfaces/models/traderEnv.ts | 6 +- server/cli.ts | 10 ++-- server/logics/transaction.ts | 53 ++++++++++-------- server/models/dailyTickers.ts | 9 ++- server/models/ticker.ts | 5 +- server/models/tickerDaily.ts | 7 ++- server/models/traderEnv.ts | 9 ++- server/package.json | 2 +- server/services/calcTraders.ts | 91 ++++++++++++++++--------------- 10 files changed, 113 insertions(+), 84 deletions(-) diff --git a/interfaces/models/dailyTickers.ts b/interfaces/models/dailyTickers.ts index d45e9f5d..4e671a80 100644 --- a/interfaces/models/dailyTickers.ts +++ b/interfaces/models/dailyTickers.ts @@ -15,9 +15,12 @@ export type TickerInfoKey = TickerMovementKey | TickerCompareKey export type TickerInfo = { [key in TickerInfoKey]?: number | string | null; +} & { + splitMultiplier: number; + closePrice: number; } -export type TickerInfos = { +export interface TickerInfos { [key: number]: TickerInfo; } diff --git a/interfaces/models/traderEnv.ts b/interfaces/models/traderEnv.ts index 09b71c24..c2c156a4 100644 --- a/interfaces/models/traderEnv.ts +++ b/interfaces/models/traderEnv.ts @@ -3,7 +3,7 @@ export interface Record { entityId: number; activeTotal: number; startDate: string; - tickerIds: number[] | null; + tickerIds: number[]; } export interface Identity extends Record { @@ -15,12 +15,12 @@ export interface Raw { entityId: number; activeTotal: number; startDate: string; - tickerIds: string | null; + tickerIds: string; } export interface Create { entityId: number; activeTotal: number; startDate: string; - tickerIds: string | null; + tickerIds: string; } diff --git a/server/cli.ts b/server/cli.ts index 067a0bbf..8a681f02 100644 --- a/server/cli.ts +++ b/server/cli.ts @@ -52,17 +52,17 @@ export const run = async () => { await emailTask.sendPendingEmails() break } - - case taskEnum.Name.calcTraderAccessHashs: { - await calcTask.calcTraderAccessHashs() - break - } case taskEnum.Name.calcTraderPerformances: { const forceRecheck = process.argv[3] === 'true' || false const checkAll = process.argv[4] === 'true' || false await calcTask.calcTraderPerformances(forceRecheck, checkAll) break } + + case taskEnum.Name.calcTraderAccessHashs: { + await calcTask.calcTraderAccessHashs() + break + } case taskEnum.Name.calcTraderDescendants: { await calcTask.calcTraderDescendants() break diff --git a/server/logics/transaction.ts b/server/logics/transaction.ts index 988afe5c..6e423e39 100644 --- a/server/logics/transaction.ts +++ b/server/logics/transaction.ts @@ -2,35 +2,37 @@ import * as interfaces from '@shared/interfaces' const getItemHoldingValue = ( shares: number, - defaultValue: number, - tickerDaily: interfaces.tickerDailyModel.Record | undefined, + closePrice: number, + splitMultiplier: number, ) => { - return tickerDaily - ? shares * tickerDaily.closePrice * tickerDaily.splitMultiplier - : defaultValue + return shares * closePrice * splitMultiplier } export const refreshHoldingItemValue = ( item: interfaces.traderHoldingModel.Item, - dailyTicker: interfaces.dailyTickersModel.TickerInfo | null, + tickerInfo: interfaces.dailyTickersModel.TickerInfo | undefined, ): interfaces.traderHoldingModel.Item => { - return item - // const matchedDaily = dailyTicker?.daily - // const holdingValue = getItemHoldingValue(item.shares, item.value, matchedDaily) - // const splitMultiplier = matchedDaily?.splitMultiplier || item.splitMultiplier - // const updatedHolding = { - // tickerId: item.tickerId, - // shares: item.shares, - // value: holdingValue, - // splitMultiplier, - // } - // return updatedHolding + const holdingValue = tickerInfo + ? getItemHoldingValue( + item.shares, + tickerInfo.closePrice, + tickerInfo.splitMultiplier, + ) + : item.value + const splitMultiplier = tickerInfo?.splitMultiplier ?? item.splitMultiplier + const updatedHolding = { + tickerId: item.tickerId, + shares: item.shares, + value: holdingValue, + splitMultiplier, + } + return updatedHolding } export const detailFromCashAndItems = ( totalCash: number, items: interfaces.traderHoldingModel.Item[], - dailyTickers: interfaces.dailyTickersModel.TickerInfos, + tickerInfos: interfaces.dailyTickersModel.TickerInfos, tradeDate: string, delistedLastPrices: DelistedLastPrices, ): interfaces.traderHoldingModel.Detail => { @@ -38,16 +40,23 @@ export const detailFromCashAndItems = ( totalValue: totalCash, totalCash, items: [], date: '', } return items.reduce((details, item) => { - const isDelisted = delistedLastPrices[item.tickerId] && delistedLastPrices[item.tickerId].date <= tradeDate + const matchedRecord = delistedLastPrices[item.tickerId] + const isDelisted = matchedRecord && matchedRecord.date <= tradeDate if (isDelisted) { - const holdingValue = getItemHoldingValue(item.shares, item.value, delistedLastPrices[item.tickerId]) + const holdingValue = matchedRecord + ? getItemHoldingValue( + item.shares, + matchedRecord.closePrice, + matchedRecord.splitMultiplier, + ) + : item.value details.totalValue += holdingValue details.totalCash += holdingValue return details } - const matched = dailyTickers[item.tickerId] || null - const refreshedHolding = refreshHoldingItemValue(item, matched) + const matchedInfo = tickerInfos[item.tickerId] + const refreshedHolding = refreshHoldingItemValue(item, matchedInfo) details.totalValue += refreshedHolding.value details.items.push(refreshedHolding) return details diff --git a/server/models/dailyTickers.ts b/server/models/dailyTickers.ts index 9f82a137..5f78db51 100644 --- a/server/models/dailyTickers.ts +++ b/server/models/dailyTickers.ts @@ -6,10 +6,15 @@ import { Knex } from 'knex' const TableName = adapterEnum.DatabaseTable.DailyTickers -export const getLatestDate = async (): Promise => { +export const getLatestDate = async ( + entityId: number, +): Promise => { const record = await databaseAdapter.findOne({ tableName: TableName, orderBy: [{ column: 'date', order: 'desc' }], + conditions: [ + { key: 'entityId', value: entityId }, + ], select: ['date'], }) return record ? record.date : dateTool.getInitialDate() @@ -31,10 +36,8 @@ export const getLast = async ( export const getByUK = async ( entityId: number, date: string, - select?: ('tickers' | 'indicators' | 'nearestPrices')[], ): Promise => { const record = await databaseAdapter.findOne({ - select, tableName: TableName, conditions: [ { key: 'entityId', value: entityId }, diff --git a/server/models/ticker.ts b/server/models/ticker.ts index eea94a89..8909109b 100644 --- a/server/models/ticker.ts +++ b/server/models/ticker.ts @@ -44,11 +44,14 @@ export const getAllByEntity = async ( return tickers } -export const getAllDelisted = async (): Promise => { +export const getAllDelisted = async ( + entityId: number, +): Promise => { const delisted = await databaseAdapter.findAll({ tableName: TableName, conditions: [ { key: 'isDelisted', value: true }, + { key: 'entityId', value: entityId }, ], }) return delisted diff --git a/server/models/tickerDaily.ts b/server/models/tickerDaily.ts index 07de95f5..b2fada73 100644 --- a/server/models/tickerDaily.ts +++ b/server/models/tickerDaily.ts @@ -44,9 +44,14 @@ export const getPreviousOne = async ( return tickerDaily ? convertToRecord(tickerDaily) : null } -export const getLatest = async (): Promise => { +export const getLatest = async ( + tickerId?: number, +): Promise => { const record = await databaseAdapter.findOne({ tableName: TableName, + conditions: tickerId + ? [{ key: 'tickerId', value: tickerId }] + : undefined, orderBy: [{ column: 'date', order: 'desc' }], }) return record ? convertToRecord(record) : undefined diff --git a/server/models/traderEnv.ts b/server/models/traderEnv.ts index 42342c69..7a66bbee 100644 --- a/server/models/traderEnv.ts +++ b/server/models/traderEnv.ts @@ -9,13 +9,18 @@ const convertToRecord = ( raw: interfaces.traderEnvModel.Raw, ): interfaces.traderEnvModel.Record => { const record: any = raw - record.tickerIds = raw.tickerIds ? raw.tickerIds.split(',').map((tickerId) => parseInt(tickerId)) : null + record.tickerIds = raw.tickerIds.split(',').map((tickerId) => parseInt(tickerId)) return record } -export const getAll = async (): Promise => { +export const getAll = async ( + entityId?: number, +): Promise => { const envs = await databaseAdapter.findAll({ tableName: TableName, + conditions: entityId + ? [{ key: 'entityId', value: entityId }] + : undefined, }) return envs.map((env) => convertToRecord(env)) } diff --git a/server/package.json b/server/package.json index 7b316dfe..744bc741 100644 --- a/server/package.json +++ b/server/package.json @@ -23,9 +23,9 @@ "calcDailyIndicators": "node -r ../scripts/devOps/pathBootstrap -r dotenv/config ./dist/cli calcDailyIndicators", "generateSystemCaches": "node -r ../scripts/devOps/pathBootstrap -r dotenv/config ./dist/cli generateSystemCaches", "sendPendingEmails": "node -r ../scripts/devOps/pathBootstrap -r dotenv/config ./dist/cli sendPendingEmails", + "calcTraderPerformances": "node -r ../scripts/devOps/pathBootstrap -r dotenv/config ./dist/cli calcTraderPerformances", "calcTraderAccessHashs": "node -r ../scripts/devOps/pathBootstrap -r dotenv/config ./dist/cli calcTraderAccessHashs", - "calcTraderPerformances": "node -r ../scripts/devOps/pathBootstrap -r dotenv/config ./dist/cli calcTraderPerformances", "calcTraderDescendants": "node -r ../scripts/devOps/pathBootstrap -r dotenv/config ./dist/cli calcTraderDescendants" }, "dependencies": { diff --git a/server/services/calcTraders.ts b/server/services/calcTraders.ts index 8ec80025..93b5310a 100644 --- a/server/services/calcTraders.ts +++ b/server/services/calcTraders.ts @@ -5,6 +5,7 @@ import * as dailyTickersModel from 'models/dailyTickers' import * as databaseAdapter from 'adapters/database' import * as dateTool from 'tools/date' import * as errorEnums from 'enums/error' +import * as entityModel from 'models/entity' import * as evaluationLogic from 'logics/evaluation' import * as generateTool from 'tools/generate' import * as helpers from '@shared/helpers' @@ -66,7 +67,7 @@ const getCachedDailyTickers = async (entityId: number, date: string) => { cacheAge: '1d', cacheKey: cacheTool.generateDailyTickersKey(entityId, date), buildFunction: async () => { - const dailyTickers = await dailyTickersModel.getByUK(entityId, date, ['tickers', 'indicators']) + const dailyTickers = await dailyTickersModel.getByUK(entityId, date) return dailyTickers }, preferLocal: true, @@ -120,22 +121,19 @@ const calcTraderPerformance = async ( continue } - const dailyTickers = dailyTickersRecord.tickerInfos || {} + const tickerInfos = dailyTickersRecord.tickerInfos || {} const indicatorInfo = {} - const emptyDailyTickers: interfaces.dailyTickersModel.TickerInfos = {} - const availableTargets = env.tickerIds - ? env.tickerIds.reduce((tickers, tickerId) => { - tickers[tickerId] = dailyTickers[tickerId] - return tickers - }, emptyDailyTickers) - : dailyTickers + const availableTickerInfos = env.tickerIds.reduce((tickers, tickerId) => { + tickers[tickerId] = tickerInfos[tickerId] + return tickers + }, {} as interfaces.dailyTickersModel.TickerInfos) const totalCash = holding ? holding.totalCash : constants.Trader.Initial.Cash const items = holding ? holding.items : [] const detailsAfterUpdate = transactionLogic.detailFromCashAndItems( - totalCash, items, availableTargets, tradeDate, delistedLastPrices, + totalCash, items, availableTickerInfos, tradeDate, delistedLastPrices, ) const shouldRebalance = @@ -277,45 +275,48 @@ export const calcAllTraderPerformances = async ( forceRecheck: boolean, checkAll: boolean, ) => { - const envs = await traderEnvModel.getAll() + const entities = await entityModel.getAll() + + await runTool.asyncForEach(entities, async (entity: interfaces.entityModel.Record) => { + const latestDate = await dailyTickersModel.getLatestDate(entity.id) + const delistedTickers = await tickerModel.getAllDelisted(entity.id) + const delistedTickerIds = delistedTickers.map((ticker) => ticker.id) + const latestPrices = await runTool.asyncMap(delistedTickerIds, async (tickerId: number) => { + return tickerDailyModel.getLatest(tickerId) + }) + const delistedLastPrices = latestPrices.reduce((lastPrices, tickerDaily) => { + lastPrices[tickerDaily.tickerId] = tickerDaily + return lastPrices + }, {} as transactionLogic.DelistedLastPrices) - const delistedTickers = await tickerModel.getAllDelisted() - const delistedTickerIds = delistedTickers.map((ticker) => ticker.id) - const latestPrices = await runTool.asyncMap(delistedTickerIds, async (tickerId: number) => { - return tickerDailyModel.getLatest() - }) - const initLastPrices: transactionLogic.DelistedLastPrices = {} - const delistedLastPrices = latestPrices.reduce((lastPrices, tickerDaily) => { - lastPrices[tickerDaily.tickerId] = tickerDaily - return lastPrices - }, initLastPrices) - const latestDate = await dailyTickersModel.getLatestDate() + const envs = await traderEnvModel.getAll(entity.id) - await runTool.asyncForEach(envs, async (env: interfaces.traderEnvModel.Record) => { - console.info(`Checking Env:${env.id}`) - const traders = checkAll - ? await traderModel.getAllByEnvId(env.id) - : await traderModel.getActives(env.id) + await runTool.asyncForEach(envs, async (env: interfaces.traderEnvModel.Record) => { + console.info(`Checking Env:${env.id}`) + const traders = checkAll + ? await traderModel.getAllByEnvId(env.id) + : await traderModel.getActives(env.id) - await runTool.asyncForEach(traders, async (trader: interfaces.traderModel.Record) => { - await calcTraderPerformance(trader, env, forceRecheck, latestDate, delistedLastPrices) - }) - const deactivateTarget = await traderModel.getByRank(env.id, env.activeTotal) - const inactiveRankingNumber = deactivateTarget?.rankingNumber - if (inactiveRankingNumber) { - await databaseAdapter.runWithTransaction(async (transaction) => { - await traderModel.activateAllByRankingNumber( - env.id, - inactiveRankingNumber, - transaction, - ) - await traderModel.deactivateAllByRankingNumber( - env.id, - inactiveRankingNumber, - transaction, - ) + await runTool.asyncForEach(traders, async (trader: interfaces.traderModel.Record) => { + await calcTraderPerformance(trader, env, forceRecheck, latestDate, delistedLastPrices) }) - } + // const deactivateTarget = await traderModel.getByRank(env.id, env.activeTotal) + // const inactiveRankingNumber = deactivateTarget?.rankingNumber + // if (inactiveRankingNumber) { + // await databaseAdapter.runWithTransaction(async (transaction) => { + // await traderModel.activateAllByRankingNumber( + // env.id, + // inactiveRankingNumber, + // transaction, + // ) + // await traderModel.deactivateAllByRankingNumber( + // env.id, + // inactiveRankingNumber, + // transaction, + // ) + // }) + // } + }) }) } From eb77b6088f24bcac0695eefe1c21d0bd3b8b2cf9 Mon Sep 17 00:00:00 2001 From: Baozier Date: Mon, 9 Oct 2023 21:23:07 -0400 Subject: [PATCH 2/5] save --- constants/src/behaviorValue.ts | 20 -- constants/src/ticker.ts | 2 +- interfaces/models/tickerYearly.ts | 2 +- package.json | 6 +- server/enums/adapter.ts | 1 + server/logics/evaluation.ts | 335 ++++++++++-------- server/logics/transaction.ts | 101 +++--- ...31009130000_rename_yearly_ratio_columns.js | 17 + server/services/calcTraders.ts | 84 +++-- server/tools/cache.ts | 4 + 10 files changed, 322 insertions(+), 250 deletions(-) create mode 100644 server/migrations/20231009130000_rename_yearly_ratio_columns.js diff --git a/constants/src/behaviorValue.ts b/constants/src/behaviorValue.ts index 3959ecfc..9c59ff78 100644 --- a/constants/src/behaviorValue.ts +++ b/constants/src/behaviorValue.ts @@ -1,26 +1,6 @@ export const Preference = Object.freeze({ HigherPrice: 1, LowerPrice: 2, - HigherQuarterEPS: 3, - LowerQuarterEPS: 4, - HigherQuarterEBITDA: 5, - LowerQuarterEBITDA: 6, - HigherQuarterIncome: 7, - LowerQuarterIncome: 8, - HigherQuarterProfit: 9, - LowerQuarterProfit: 10, - HigherQuarterRevenue: 11, - LowerQuarterRevenue: 12, - HigherYearEPS: 13, - LowerYearEPS: 14, - HigherYearEBITDA: 15, - LowerYearEBITDA: 16, - HigherYearIncome: 17, - LowerYearIncome: 18, - HigherYearProfit: 19, - LowerYearProfit: 20, - HigherYearRevenue: 21, - LowerYearRevenue: 22, }) const ContinuousMovement = [1, 2, 3, 5] diff --git a/constants/src/ticker.ts b/constants/src/ticker.ts index b0f5ea8e..f0f0b2da 100644 --- a/constants/src/ticker.ts +++ b/constants/src/ticker.ts @@ -9,7 +9,7 @@ export const DailyMovementKeys: interfaces.tickerDailyModel.MovementKey[] = [ ] export const YearlyCompareKeys: interfaces.tickerYearlyModel.CompareKey[] = [ - 'peRatio', 'pbRatio', 'psRatio', + 'annualPeRatio', 'annualPbRatio', 'annualPsRatio', ] export const YearlyMovementKeys: interfaces.tickerYearlyModel.MovementKey[] = [ diff --git a/interfaces/models/tickerYearly.ts b/interfaces/models/tickerYearly.ts index 9e19dc5a..d16d5884 100644 --- a/interfaces/models/tickerYearly.ts +++ b/interfaces/models/tickerYearly.ts @@ -14,7 +14,7 @@ export type MovementDecreaseKey = export type MovementKey = MovementIncreaseKey | MovementDecreaseKey -export type CompareKey = 'peRatio' | 'pbRatio' | 'psRatio' +export type CompareKey = 'annualPeRatio' | 'annualPbRatio' | 'annualPsRatio' interface Common { id: number; diff --git a/package.json b/package.json index 765e8601..a00f5c29 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,10 @@ "url": "git+https://github.com/ValueMelody/melody-app" }, "scripts": { - "basic": "npm run build --workspace=constants && npm run build --workspace=helpers", + "shared": "npm run build --workspace=constants && npm run build --workspace=helpers", "dev": "concurrently \"npm run dev --workspace=client\" \"npm run dev --workspace=server\"", - "build:client": "npm run basic && npm run build --workspace=client", - "build:server": "npm run basic && npm run build --workspace=server", + "build:client": "npm run shared && npm run build --workspace=client", + "build:server": "npm run shared && npm run build --workspace=server", "lint": "npm run lint --workspace=interfaces && npm run lint --workspace=constants && npm run lint --workspace=helpers && npm run lint --workspace=client && npm run lint --workspace=server", "lint:fix": "npm run lint:fix --workspace=interfaces && npm run lint:fix --workspace=constants && npm run lint:fix --workspace=helpers && npm run lint:fix --workspace=client && npm run lint:fix --workspace=server", diff --git a/server/enums/adapter.ts b/server/enums/adapter.ts index 78dd27de..d4ac71e1 100644 --- a/server/enums/adapter.ts +++ b/server/enums/adapter.ts @@ -54,6 +54,7 @@ export const CacheConfig = Object.freeze({ export const CacheKey = Object.freeze({ DailyTickers: 'dailyTickers', + DailyIndicators: 'dailyIndicators', TickerPrices: 'tickerPrices', SysemEndpoint: 'systemEndpoint', }) diff --git a/server/logics/evaluation.ts b/server/logics/evaluation.ts index 7012262d..1054750b 100644 --- a/server/logics/evaluation.ts +++ b/server/logics/evaluation.ts @@ -1,46 +1,14 @@ import * as constants from '@shared/constants' import * as interfaces from '@shared/interfaces' -export const getTickerPreferValue = ( +export const getTickerEqualWeightPreferValue = ( Preference: number, - tickerDaily: interfaces.tickerDailyModel.Record, - tickerQuarterly: interfaces.tickerQuarterlyModel.Record | null, - tickerYearly: interfaces.tickerYearlyModel.Record | null, + tickerInfo: interfaces.dailyTickersModel.TickerInfo, ): number | null => { switch (Preference) { case constants.BehaviorValue.Preference.HigherPrice: case constants.BehaviorValue.Preference.LowerPrice: - return tickerDaily.closePrice - case constants.BehaviorValue.Preference.HigherQuarterEPS: - case constants.BehaviorValue.Preference.LowerQuarterEPS: - return tickerQuarterly ? tickerQuarterly.eps : null - case constants.BehaviorValue.Preference.HigherQuarterEBITDA: - case constants.BehaviorValue.Preference.LowerQuarterEBITDA: - return tickerQuarterly ? tickerQuarterly.ebitda : null - case constants.BehaviorValue.Preference.HigherQuarterIncome: - case constants.BehaviorValue.Preference.LowerQuarterIncome: - return tickerQuarterly ? tickerQuarterly.netIncome : null - case constants.BehaviorValue.Preference.HigherQuarterProfit: - case constants.BehaviorValue.Preference.LowerQuarterProfit: - return tickerQuarterly ? tickerQuarterly.grossProfit : null - case constants.BehaviorValue.Preference.HigherQuarterRevenue: - case constants.BehaviorValue.Preference.LowerQuarterRevenue: - return tickerQuarterly ? tickerQuarterly.totalRevenue : null - case constants.BehaviorValue.Preference.HigherYearEPS: - case constants.BehaviorValue.Preference.LowerYearEPS: - return tickerYearly ? tickerYearly.eps : null - case constants.BehaviorValue.Preference.HigherYearEBITDA: - case constants.BehaviorValue.Preference.LowerYearEBITDA: - return tickerYearly ? tickerYearly.ebitda : null - case constants.BehaviorValue.Preference.HigherYearIncome: - case constants.BehaviorValue.Preference.LowerYearIncome: - return tickerYearly ? tickerYearly.netIncome : null - case constants.BehaviorValue.Preference.HigherYearProfit: - case constants.BehaviorValue.Preference.LowerYearProfit: - return tickerYearly ? tickerYearly.grossProfit : null - case constants.BehaviorValue.Preference.HigherYearRevenue: - case constants.BehaviorValue.Preference.LowerYearRevenue: - return tickerYearly ? tickerYearly.totalRevenue : null + return tickerInfo.closePrice default: return null } @@ -159,46 +127,50 @@ export const TickerMovementTriggers: { freeCashFlowYearlyDecreaseSell: 'freeCashFlowYearlyDecrease', } -// table.smallint('peRatioQuarterlyAboveBuy') -// table.smallint('peRatioQuarterlyAboveSell') -// table.smallint('peRatioQuarterlyBelowBuy') -// table.smallint('peRatioQuarterlyBelowSell') -// table.smallint('pbRatioQuarterlyAboveBuy') -// table.smallint('pbRatioQuarterlyAboveSell') -// table.smallint('pbRatioQuarterlyBelowBuy') -// table.smallint('pbRatioQuarterlyBelowSell') -// table.smallint('psRatioQuarterlyAboveBuy') -// table.smallint('psRatioQuarterlyAboveSell') -// table.smallint('psRatioQuarterlyBelowBuy') -// table.smallint('psRatioQuarterlyBelowSell') -// table.smallint('roaQuarterlyAboveBuy') -// table.smallint('roaQuarterlyAboveSell') -// table.smallint('roaQuarterlyBelowBuy') -// table.smallint('roaQuarterlyBelowSell') -// table.smallint('roeQuarterlyAboveBuy') -// table.smallint('roeQuarterlyAboveSell') -// table.smallint('roeQuarterlyBelowBuy') -// table.smallint('roeQuarterlyBelowSell') -// table.smallint('grossMarginQuarterlyAboveBuy') -// table.smallint('grossMarginQuarterlyAboveSell') -// table.smallint('grossMarginQuarterlyBelowBuy') -// table.smallint('grossMarginQuarterlyBelowSell') -// table.smallint('debtEquityQuarterlyAboveBuy') -// table.smallint('debtEquityQuarterlyAboveSell') -// table.smallint('debtEquityQuarterlyBelowBuy') -// table.smallint('debtEquityQuarterlyBelowSell') -// table.smallint('peRatioYearlyAboveBuy') -// table.smallint('peRatioYearlyAboveSell') -// table.smallint('peRatioYearlyBelowBuy') -// table.smallint('peRatioYearlyBelowSell') -// table.smallint('pbRatioYearlyAboveBuy') -// table.smallint('pbRatioYearlyAboveSell') -// table.smallint('pbRatioYearlyBelowBuy') -// table.smallint('pbRatioYearlyBelowSell') -// table.smallint('psRatioYearlyAboveBuy') -// table.smallint('psRatioYearlyAboveSell') -// table.smallint('psRatioYearlyBelowBuy') -// table.smallint('psRatioYearlyBelowSell') +export const TickerCompareTriggers: { + [key in interfaces.traderPatternModel.TickerCompareBehavior]: interfaces.dailyTickersModel.TickerCompareKey +} = { + peRatioQuarterlyAboveBuy: 'peRatio', + peRatioQuarterlyBelowBuy: 'peRatio', + pbRatioQuarterlyAboveBuy: 'pbRatio', + pbRatioQuarterlyBelowBuy: 'pbRatio', + psRatioQuarterlyAboveBuy: 'psRatio', + psRatioQuarterlyBelowBuy: 'psRatio', + roaQuarterlyAboveBuy: 'roa', + roaQuarterlyBelowBuy: 'roa', + roeQuarterlyAboveBuy: 'roe', + roeQuarterlyBelowBuy: 'roe', + grossMarginQuarterlyAboveBuy: 'grossMargin', + grossMarginQuarterlyBelowBuy: 'grossMargin', + debtEquityQuarterlyAboveBuy: 'debtEquity', + debtEquityQuarterlyBelowBuy: 'debtEquity', + peRatioYearlyAboveBuy: 'annualPeRatio', + peRatioYearlyBelowBuy: 'annualPeRatio', + pbRatioYearlyAboveBuy: 'annualPbRatio', + pbRatioYearlyBelowBuy: 'annualPbRatio', + psRatioYearlyAboveBuy: 'annualPsRatio', + psRatioYearlyBelowBuy: 'annualPsRatio', + peRatioQuarterlyAboveSell: 'peRatio', + peRatioQuarterlyBelowSell: 'peRatio', + pbRatioQuarterlyAboveSell: 'pbRatio', + pbRatioQuarterlyBelowSell: 'pbRatio', + psRatioQuarterlyAboveSell: 'psRatio', + psRatioQuarterlyBelowSell: 'psRatio', + roaQuarterlyAboveSell: 'roa', + roaQuarterlyBelowSell: 'roa', + roeQuarterlyAboveSell: 'roe', + roeQuarterlyBelowSell: 'roe', + grossMarginQuarterlyAboveSell: 'grossMargin', + grossMarginQuarterlyBelowSell: 'grossMargin', + debtEquityQuarterlyAboveSell: 'debtEquity', + debtEquityQuarterlyBelowSell: 'debtEquity', + peRatioYearlyAboveSell: 'annualPeRatio', + peRatioYearlyBelowSell: 'annualPeRatio', + pbRatioYearlyAboveSell: 'annualPbRatio', + pbRatioYearlyBelowSell: 'annualPbRatio', + psRatioYearlyAboveSell: 'annualPsRatio', + psRatioYearlyBelowSell: 'annualPsRatio', +} export const IndicatorMovementTriggers: { [key in interfaces.traderPatternModel.IndicatorMovementBehavior]: interfaces.dailyIndicatorsModel.IndicatorMovementKey @@ -294,100 +266,144 @@ export const IndicatorCompareTriggers: { inflationYearlyBelowSell: 'annualInflation', } -export const getTickerMovementWeight = ( - tickerInfo: interfaces.dailyTickersModel.TickerInfo, - tickerKey: interfaces.dailyTickersModel.TickerMovementKey, - pattern: interfaces.traderPatternModel.Record, - behavior: interfaces.traderPatternModel.MovementBehavior, -): number => { - const tickerValue = Number(tickerInfo[tickerKey]) - const patternValue = pattern[behavior] - - if (patternValue === null || patternValue === undefined) return 1 - if (tickerValue === null || tickerValue === undefined || tickerValue < patternValue) return 0 - return tickerValue - patternValue + 2 -} - -export const getIndicatorMovementMatch = ( +export const isIndicatorMovementBehaviorFitPattern = ( indicatorInfo: interfaces.dailyIndicatorsModel.IndicatorInfo, indicatorKey: interfaces.dailyIndicatorsModel.IndicatorMovementKey, pattern: interfaces.traderPatternModel.Record, behavior: interfaces.traderPatternModel.MovementBehavior, ): boolean => { - const indicatorValue = Number(indicatorInfo[indicatorKey]) + const indicatorValue = indicatorInfo[indicatorKey] const patternValue = pattern[behavior] + // If pattern do not care about this indicator, then treat as fit if (patternValue === null || patternValue === undefined) return true - return indicatorValue !== null && indicatorValue >= patternValue + // If indicator do not contain a value for this behavior, then treat as no fit + if (indicatorValue === null || indicatorValue === undefined) return false + return indicatorValue >= patternValue } -export const getIndicatorCompareMatch = ( +export const isIndicatorCompareBehaviorFitPattern = ( indicatorInfo: interfaces.dailyIndicatorsModel.IndicatorInfo, compareKey: interfaces.dailyIndicatorsModel.IndicatorCompareKey, pattern: interfaces.traderPatternModel.Record, behavior: interfaces.traderPatternModel.CompareBehavior, ): boolean => { - const indicatorValue = Number(indicatorInfo[compareKey]) + const indicatorValue = indicatorInfo[compareKey] const patternValue = pattern[behavior] + // If pattern do not care about this indicator, then treat as fit if (patternValue === null || patternValue === undefined) return true - if (indicatorValue === null) return false + // If indicator do not contain a value for this behavior, then treat as no fit + if (indicatorValue === null || indicatorValue === undefined) return false - if (behavior.includes('Above') && indicatorValue > patternValue) { - return true - } + if (behavior.includes('Above') && indicatorValue > patternValue) return true + if (behavior.includes('Below') && indicatorValue < patternValue) return true + return false +} - if (behavior.includes('Below') && indicatorValue < patternValue) { - return true - } +export const isIndicatorFitPatternBehaviors = ( + pattern: interfaces.traderPatternModel.Record, + indicatorInfo: interfaces.dailyIndicatorsModel.IndicatorInfo, + movementBehaviors: interfaces.traderPatternModel.IndicatorMovementBehavior[], + compareBehaviors: interfaces.traderPatternModel.indicatorCompareBehavior[], +): boolean => { + // Make sure every movement behaviors in one trader pattern fit current indicator + const movementFit = movementBehaviors.every((behavior) => { + const indicatorKey = IndicatorMovementTriggers[behavior] + return isIndicatorMovementBehaviorFitPattern( + indicatorInfo, indicatorKey, pattern, behavior, + ) + }) + if (!movementFit) return false - return false + // make sure every compare behaviors in one trader pattern fit current indicator + const compareFit = compareBehaviors.every((trigger) => { + const indicatorKey = IndicatorCompareTriggers[trigger] + return isIndicatorCompareBehaviorFitPattern( + indicatorInfo, indicatorKey, pattern, trigger, + ) + }) + return compareFit } -export const getTickerMovementWeights = ( +export const getTickerMovementWeight = ( + tickerInfo: interfaces.dailyTickersModel.TickerInfo, + tickerKey: interfaces.dailyTickersModel.TickerMovementKey, + pattern: interfaces.traderPatternModel.Record, + behavior: interfaces.traderPatternModel.MovementBehavior, +): number => { + const tickerValue = tickerInfo[tickerKey] + const patternValue = pattern[behavior] + + // If pattern has no value one on one behavior, weight 1 + if (patternValue === null || patternValue === undefined) return 1 + + const tickerNumValue = Number(tickerValue) + // If ticker has no value on one behavior or less than expected pattern value, treat as no weight + if (tickerValue === null || tickerValue === undefined || tickerNumValue < patternValue) return 0 + + // weight start from 2 if ticker value is larger than or equal to pattern value + return tickerNumValue - patternValue + 2 +} + +export const getTickerCompareWeight = ( + tickerInfo: interfaces.dailyTickersModel.TickerInfo, + tickerKey: interfaces.dailyTickersModel.TickerCompareKey, + pattern: interfaces.traderPatternModel.Record, + behavior: interfaces.traderPatternModel.CompareBehavior, +): number => { + const tickerValue = tickerInfo[tickerKey] + const patternValue = pattern[behavior] + + // If pattern has no value one on one behavior, weight 1 + if (patternValue === null || patternValue === undefined) return 1 + + const tickerNumValue = Number(tickerValue) + // If ticker has no value on one behavior or equal to expected pattern value, treat as no weight + if (tickerValue === null || tickerValue === undefined || patternValue === tickerNumValue) return 0 + + if (behavior.includes('Above') && tickerNumValue > patternValue) return 2 + if (behavior.includes('Below') && tickerNumValue < patternValue) return 2 + return 0 +} + +export const getTickerWeights = ( pattern: interfaces.traderPatternModel.Record, tickerInfo: interfaces.dailyTickersModel.TickerInfo, movementBehaviors: interfaces.traderPatternModel.TickerMovementBehavior[], + compareBehaviors: interfaces.traderPatternModel.TickerCompareBehavior[], ) => { - const movementWeights = movementBehaviors.reduce(( - weights: number, behavior, - ): number => { - if (!weights) return 0 + let weights = 1 + + // Aggregate ticker movement weights + movementBehaviors.some((behavior) => { const tickerKey = TickerMovementTriggers[behavior] - const currentWeight = getTickerMovementWeight( + const movementWeight = getTickerMovementWeight( tickerInfo, tickerKey, pattern, behavior, ) - return weights * currentWeight - }, 1) + // Multple weight for each behaviors as the final weight + weights = weights * movementWeight - return movementWeights -} - -export const getIndicatorMovementAndCompareMatches = ( - pattern: interfaces.traderPatternModel.Record, - indicatorInfo: interfaces.dailyIndicatorsModel.IndicatorInfo, - movementBehaviors: interfaces.traderPatternModel.IndicatorMovementBehavior[], - compareTriggers: interfaces.traderPatternModel.indicatorCompareBehavior[], -): boolean => { - const movementMatches = movementBehaviors.every((behavior) => { - const indicatorKey = IndicatorMovementTriggers[behavior] - const isMatch = getIndicatorMovementMatch( - indicatorInfo, indicatorKey, pattern, behavior, - ) - return isMatch + // If weigth equals to 0, exist loop + return !movementWeight }) - if (!movementMatches) return false + if (!weights) return weights - const compareMatches = compareTriggers.every((trigger) => { - const indicatorKey = IndicatorCompareTriggers[trigger] - const isMatch = getIndicatorCompareMatch( - indicatorInfo, indicatorKey, pattern, trigger, + // Aggregate ticker compare weights + compareBehaviors.some((behavior) => { + const tickerKey = TickerCompareTriggers[behavior] + const compareWeight = getTickerCompareWeight( + tickerInfo, tickerKey, pattern, behavior, ) - return isMatch + // Multple weight for each behaviors as the final weight + weights = weights * compareWeight + + // If weigth equals to 0, exist loop + return !compareWeight }) - return compareMatches + return weights } interface TickerWithEvaluation { @@ -399,26 +415,32 @@ interface TickerWithEvaluation { export const getTickerWithSellEvaluation = ( tickerId: number, pattern: interfaces.traderPatternModel.Record, - dailyTicker: interfaces.dailyTickersModel.TickerInfo | null, + tickerInfo: interfaces.dailyTickersModel.TickerInfo | null, ): TickerWithEvaluation | null => { - if (!dailyTicker) return null + // When no available info for this tickerId, then do not sell + if (!tickerInfo) return null - return null - // const preferValue = getTickerPreferValue( - // pattern.sellPreference, dailyTicker.daily, dailyTicker.quarterly, dailyTicker.yearly, - // ) - // if (!preferValue && preferValue !== 0) return null + // Check current patterns sell preference if tickers are equal weight + const preferValue = getTickerEqualWeightPreferValue( + pattern.sellPreference, tickerInfo, + ) + if (preferValue === null) return null - // const weight = getTickerMovementWeights( - // pattern, - // dailyTicker.info, - // constants.Behavior.TickerMovementSellBehaviors, - // ) - // if (!weight) return null + // Get current tickers sell weight + const weight = getTickerWeights( + pattern, + tickerInfo, + constants.Behavior.TickerMovementSellBehaviors, + constants.Behavior.TickerCompareSellBehaviors, + ) - // return { - // tickerId, preferValue, weight, - // } + if (!weight) return null + + return { + tickerId, + preferValue, + weight, + } } export const getTickerWithBuyEvaluation = ( @@ -453,6 +475,7 @@ export const getOrderedTickerEvaluations = ( ) => { const preferenceEntry = Object.entries(constants.BehaviorValue.Preference).find((entry) => entry[1] === preference) if (!preferenceEntry) throw new Error('Wrong preference provided') + const key = preferenceEntry[0] return evaluations.sort((first, second) => { if (first.weight > second.weight) return -1 @@ -492,11 +515,12 @@ export const getIndicatorBuyMatches = ( return shouldBuy } -export const getIndicatorSellMatches = ( +export const shouldSellBasedOnIndicator = ( pattern: interfaces.traderPatternModel.Record, indicatorInfo: interfaces.dailyIndicatorsModel.IndicatorInfo, ): boolean => { - const shouldSell = getIndicatorMovementAndCompareMatches( + // Should sell if indicator matches all pattern behaviors + const shouldSell = isIndicatorFitPatternBehaviors( pattern, indicatorInfo, constants.Behavior.IndicatorMovementSellBehaviors, @@ -505,19 +529,20 @@ export const getIndicatorSellMatches = ( return shouldSell } -export const getTickerSellEaluations = ( +export const getTickerSellEvalutions = ( tickerIds: number[], pattern: interfaces.traderPatternModel.Record, - dailyTickers: interfaces.dailyTickersModel.TickerInfos, + tickerInfos: interfaces.dailyTickersModel.TickerInfos, ): TickerWithEvaluation[] => { - const emptyEvaluations: TickerWithEvaluation[] = [] + // Get sell evaluation of each ticker that should be sold const tickerEvaluations = tickerIds.reduce((evaluations, tickerId) => { const evaluation = getTickerWithSellEvaluation( - tickerId, pattern, dailyTickers[tickerId], + tickerId, pattern, tickerInfos[tickerId], ) if (evaluation) evaluations.push(evaluation) return evaluations - }, emptyEvaluations) + }, [] as TickerWithEvaluation[]) + // Order tickerEvalutions by weight and prefer value const orderedEvaluations = getOrderedTickerEvaluations(tickerEvaluations, pattern.sellPreference) return orderedEvaluations } diff --git a/server/logics/transaction.ts b/server/logics/transaction.ts index 6e423e39..bc8805b1 100644 --- a/server/logics/transaction.ts +++ b/server/logics/transaction.ts @@ -19,7 +19,7 @@ export const refreshHoldingItemValue = ( tickerInfo.splitMultiplier, ) : item.value - const splitMultiplier = tickerInfo?.splitMultiplier ?? item.splitMultiplier + const splitMultiplier = tickerInfo ? tickerInfo.splitMultiplier : item.splitMultiplier const updatedHolding = { tickerId: item.tickerId, shares: item.shares, @@ -29,7 +29,7 @@ export const refreshHoldingItemValue = ( return updatedHolding } -export const detailFromCashAndItems = ( +export const generateHoldingDetail = ( totalCash: number, items: interfaces.traderHoldingModel.Item[], tickerInfos: interfaces.dailyTickersModel.TickerInfos, @@ -40,14 +40,16 @@ export const detailFromCashAndItems = ( totalValue: totalCash, totalCash, items: [], date: '', } return items.reduce((details, item) => { - const matchedRecord = delistedLastPrices[item.tickerId] - const isDelisted = matchedRecord && matchedRecord.date <= tradeDate + const delistedRecord = delistedLastPrices[item.tickerId] + const isDelisted = delistedRecord && delistedRecord.date <= tradeDate + + // If one holding item is delisted, then treat as cash out based on price from last record if (isDelisted) { - const holdingValue = matchedRecord + const holdingValue = delistedRecord ? getItemHoldingValue( item.shares, - matchedRecord.closePrice, - matchedRecord.splitMultiplier, + delistedRecord.closePrice, + delistedRecord.splitMultiplier, ) : item.value details.totalValue += holdingValue @@ -56,6 +58,7 @@ export const detailFromCashAndItems = ( } const matchedInfo = tickerInfos[item.tickerId] + // Recalculate one holding item by provided market data const refreshedHolding = refreshHoldingItemValue(item, matchedInfo) details.totalValue += refreshedHolding.value details.items.push(refreshedHolding) @@ -63,7 +66,7 @@ export const detailFromCashAndItems = ( }, emptyDetail) } -export const sellForLessThanTickerMinPercent = ( +export const sellItemIfLessThanTickerMinPercent = ( holdingDetail: interfaces.traderHoldingModel.Detail, itemForSell: interfaces.traderHoldingModel.Item, holdingPercent: number, @@ -74,8 +77,11 @@ export const sellForLessThanTickerMinPercent = ( if (!isLessThanMinPercent) return null const cashAfterSell = holdingDetail.totalCash + itemForSell.value + + // Only sell if cash after sell is less than allowed max cash const couldSell = cashAfterSell <= maxCashValue if (!couldSell) return null + return { date: '', totalValue: holdingDetail.totalValue, @@ -84,35 +90,34 @@ export const sellForLessThanTickerMinPercent = ( } } -export const sellForMoreThanTickerMaxPercernt = ( +export const sellItemIfMoreThanTickerMaxPercernt = ( holdingDetail: interfaces.traderHoldingModel.Detail, itemForSell: interfaces.traderHoldingModel.Item, holdingPercent: number, tickerMaxPercent: number, maxCashValue: number, - tickerDaily: interfaces.tickerDailyModel.Record | null, + tickerInfo: interfaces.dailyTickersModel.TickerInfo, ): interfaces.traderHoldingModel.Detail | null => { - if (!tickerDaily) return null - const isMoreThanMaxPercent = holdingPercent > tickerMaxPercent if (!isMoreThanMaxPercent) return null const sellTargetPercent = holdingPercent - tickerMaxPercent - const sellTargetValue = Math.ceil(holdingDetail.totalValue * sellTargetPercent / 100) - const sellTargetShares = Math.ceil(sellTargetValue / tickerDaily.closePrice) - const sharesSold = sellTargetShares / tickerDaily.splitMultiplier + const sellTargetValue = Math.ceil(holdingDetail.totalValue * sellTargetPercent) + const sellTargetShares = Math.ceil(sellTargetValue / tickerInfo.closePrice) + const sharesSold = sellTargetShares / tickerInfo.splitMultiplier const sharesLeft = itemForSell.shares - sharesSold - const dailyFinalPrice = tickerDaily.closePrice * tickerDaily.splitMultiplier - const cashAfterSell = holdingDetail.totalCash + sharesSold * dailyFinalPrice - const couldSell = sharesLeft && cashAfterSell < maxCashValue + const sellValue = sellTargetShares * tickerInfo.closePrice + const cashAfterSell = holdingDetail.totalCash + sellValue + // Only sell if cash after sell is less than allowed max cash and there are still shares left + const couldSell = sharesLeft && (cashAfterSell <= maxCashValue) if (!couldSell) return null const holdingAfterSell = { tickerId: itemForSell.tickerId, shares: sharesLeft, - value: sharesLeft * dailyFinalPrice, - splitMultiplier: tickerDaily.splitMultiplier, + value: itemForSell.value - sellValue, + splitMultiplier: tickerInfo.splitMultiplier, } return { totalValue: holdingDetail.totalValue, @@ -134,13 +139,13 @@ export interface DelistedLastPrices { [tickerId: number]: interfaces.tickerDailyModel.Record; } -export const detailAfterRebalance = ( +export const rebalanceHoldingDetail = ( shouldRebalance: boolean, currentDetail: interfaces.traderHoldingModel.Detail, - dailyTickers: interfaces.dailyTickersModel.TickerInfos, + tickerInfos: interfaces.dailyTickersModel.TickerInfos, tickerMinPercent: number, tickerMaxPercent: number, - maxCashValue: number, + cashMaxPercent: number, ): DetailAndTransaction => { if (!shouldRebalance) { return { @@ -149,33 +154,39 @@ export const detailAfterRebalance = ( } } - // let hasTransaction = false + let hasTransaction = false + const maxCashValue = currentDetail.totalValue * cashMaxPercent + const details = currentDetail.items.reduce((details, item) => { - const matched = dailyTickers[item.tickerId] - if (!matched) return details - return details - // const matchedDaily = matched.daily - // const holdingPercent = (item.value / details.totalValue) * 100 - // const refreshedForMinPercernt = sellForLessThanTickerMinPercent( - // details, item, holdingPercent, tickerMinPercent, maxCashValue, - // ) - // if (refreshedForMinPercernt) { - // hasTransaction = true - // return refreshedForMinPercernt - // } + // If holding item has no market data, skip rebalance + const matchedInfo = tickerInfos[item.tickerId] + if (!matchedInfo) return details - // const refreshedForMaxPercent = sellForMoreThanTickerMaxPercernt( - // details, item, holdingPercent, tickerMaxPercent, maxCashValue, matchedDaily, - // ) - // if (refreshedForMaxPercent) { - // hasTransaction = true - // return refreshedForMaxPercent - // } + const holdingPercent = item.value / details.totalValue + + // If hold less than required min percent, then sell all shares + const refreshedForMinPercernt = sellItemIfLessThanTickerMinPercent( + details, item, holdingPercent, tickerMinPercent, maxCashValue, + ) + if (refreshedForMinPercernt) { + hasTransaction = true + return refreshedForMinPercernt + } - // return details + // If hold more than allowed max percent, then sell exceed shares + const refreshedForMaxPercent = sellItemIfMoreThanTickerMaxPercernt( + details, item, holdingPercent, tickerMaxPercent, maxCashValue, matchedInfo, + ) + if (refreshedForMaxPercent) { + hasTransaction = true + return refreshedForMaxPercent + } + + return details }, currentDetail) + return { - hasTransaction: false, + hasTransaction, holdingDetail: details, } } diff --git a/server/migrations/20231009130000_rename_yearly_ratio_columns.js b/server/migrations/20231009130000_rename_yearly_ratio_columns.js new file mode 100644 index 00000000..fc84a750 --- /dev/null +++ b/server/migrations/20231009130000_rename_yearly_ratio_columns.js @@ -0,0 +1,17 @@ +exports.up = (knex) => { + return knex.schema + .table('ticker_yearly', (table) => { + table.renameColumn('peRatio', 'annualPeRatio') + table.renameColumn('pbRatio', 'annualPbRatio') + table.renameColumn('psRatio', 'annualPsRatio') + }) +} + +exports.down = (knex) => { + return knex.schema + .table('ticker_yearly', (table) => { + table.renameColumn('annualPeRatio', 'peRatio') + table.renameColumn('annualPbRatio', 'pbRatio') + table.renameColumn('annualPsRatio', 'psRatio') + }) +} diff --git a/server/services/calcTraders.ts b/server/services/calcTraders.ts index 93b5310a..569ddb8d 100644 --- a/server/services/calcTraders.ts +++ b/server/services/calcTraders.ts @@ -2,6 +2,7 @@ import * as cacheAdapter from 'adapters/cache' import * as cacheTool from 'tools/cache' import * as constants from '@shared/constants' import * as dailyTickersModel from 'models/dailyTickers' +import * as dailyIndicatorsModel from 'models/dailyIndicators' import * as databaseAdapter from 'adapters/database' import * as dateTool from 'tools/date' import * as errorEnums from 'enums/error' @@ -62,7 +63,9 @@ const isActiveTraderEnv = async (env: interfaces.traderEnvModel.Record) => { return userPaymentModel.hasActiveUser(userIds) } -const getCachedDailyTickers = async (entityId: number, date: string) => { +const getCachedDailyTickers = async ( + entityId: number, date: string, +): Promise => { return cacheAdapter.returnBuild({ cacheAge: '1d', cacheKey: cacheTool.generateDailyTickersKey(entityId, date), @@ -74,6 +77,20 @@ const getCachedDailyTickers = async (entityId: number, date: string) => { }) } +const getCachedDailyIndicators = async ( + date: string, +): Promise => { + return cacheAdapter.returnBuild({ + cacheAge: '1d', + cacheKey: cacheTool.generateDailyIndicatorsKey(date), + buildFunction: async () => { + const dailyIndicators = await dailyIndicatorsModel.getByUK(date) + return dailyIndicators + }, + preferLocal: true, + }) +} + const calcTraderPerformance = async ( targetTrader: interfaces.traderModel.Record, env: interfaces.traderEnvModel.Record, @@ -81,49 +98,61 @@ const calcTraderPerformance = async ( latestDate: string, delistedLastPrices: transactionLogic.DelistedLastPrices, ) => { + // Delete all trader records in related tables and reset trader record if forceRecheck = true const trader = forceRecheck ? await cleanupTrader(targetTrader.id) : targetTrader - console.info(`Checking Trader:${trader.id}`) + // If trader has been estimated already, then skip if (trader.estimatedAt && trader.estimatedAt >= latestDate) return const pattern = await traderPatternModel.getByPK(targetTrader.traderPatternId) if (!pattern) throw errorEnums.Custom.RecordNotFound - const tickerMinPercent = pattern.tickerMinPercent - const tickerMaxPercent = pattern.tickerMaxPercent - const holdingBuyPercent = pattern.holdingBuyPercent - const holdingSellPercent = pattern.holdingSellPercent - const cashMaxPercent = pattern.cashMaxPercent - let holding = await traderHoldingModel.getLatest(trader.id) + + // Get next trade date, or use env start date for empty trader record let tradeDate = holding ? dateTool.getNextDate(holding.date, pattern.tradeFrequency) : env.startDate + + // If target trade date is larger than latest date with market data, then skip + if (tradeDate > latestDate) return + let rebalancedAt = trader.rebalancedAt || tradeDate let startedAt = trader.startedAt let hasRebalanced = false - let hasCreatedAnyRecord = false + let shouldCommitTransaction = false + + const cashMaxPercent = pattern.cashMaxPercent / 100 + const tickerMinPercent = pattern.tickerMinPercent / 100 + const tickerMaxPercent = pattern.tickerMaxPercent / 100 + const holdingBuyPercent = pattern.holdingBuyPercent + const holdingSellPercent = pattern.holdingSellPercent + console.info(`Checking Trader:${trader.id}`) const transaction = await databaseAdapter.createTransaction() try { + // Keep calculate until target trade date is larger than lastes date with market data while (tradeDate <= latestDate) { const nextDate = dateTool.getNextDate(tradeDate, pattern.tradeFrequency) - const dailyTickersRecord: interfaces.dailyTickersModel.Record | null = await getCachedDailyTickers( + const dailyTickersRecord = await getCachedDailyTickers( env.entityId, tradeDate, ) + // If target trade date has no market date, then try next date if (!dailyTickersRecord?.tickerInfos) { tradeDate = nextDate continue } - const tickerInfos = dailyTickersRecord.tickerInfos || {} - const indicatorInfo = {} + const tickerInfos = dailyTickersRecord.tickerInfos + const dailyIndicators = await getCachedDailyIndicators(tradeDate) + const indicatorInfo = dailyIndicators?.indicatorInfo || {} + // Only keep market data related to env defined tickers const availableTickerInfos = env.tickerIds.reduce((tickers, tickerId) => { tickers[tickerId] = tickerInfos[tickerId] return tickers @@ -132,36 +161,41 @@ const calcTraderPerformance = async ( const totalCash = holding ? holding.totalCash : constants.Trader.Initial.Cash const items = holding ? holding.items : [] - const detailsAfterUpdate = transactionLogic.detailFromCashAndItems( + // Regenerate holding detail by cash, holding items, and target market info + const regeneratedDetail = transactionLogic.generateHoldingDetail( totalCash, items, availableTickerInfos, tradeDate, delistedLastPrices, ) + // If next expected rebalance date is earlier than target trade date, then rebalance const shouldRebalance = !!pattern.rebalanceFrequency && dateTool.getNextDate(rebalancedAt, pattern.rebalanceFrequency) <= tradeDate - const maxCashValue = detailsAfterUpdate.totalValue * cashMaxPercent / 100 + // Rebalance holding based on tickerMinPercent, tickerMaxPercent and cashMaxPercent const { - holdingDetail: detailAfterRebalance, + holdingDetail: rebalancedDetail, hasTransaction: hasRebalanceTransaction, - } = transactionLogic.detailAfterRebalance( + } = transactionLogic.rebalanceHoldingDetail( shouldRebalance, - detailsAfterUpdate, - availableTargets, + regeneratedDetail, + availableTickerInfos, tickerMinPercent, tickerMaxPercent, - maxCashValue, + cashMaxPercent, ) - const holdingTickerIds = detailAfterRebalance.items.map((item) => item.tickerId) + // Check if indicatorInfo matches sell criterion + const shouldSellBasedOnIndicator = !!indicatorInfo && + evaluationLogic.shouldSellBasedOnIndicator(pattern, indicatorInfo) - const isSellIndicatorMatches = !!indicatorInfo && evaluationLogic.getIndicatorSellMatches(pattern, indicatorInfo) - const sellTickerEvaluations = isSellIndicatorMatches - ? evaluationLogic.getTickerSellEaluations( - holdingTickerIds, pattern, availableTargets, + // Get a list of ordered tickerIds that should be sold + const holdingTickerIds = rebalancedDetail.items.map((item) => item.tickerId) + const tickerSellEvaluations = shouldSellBasedOnIndicator + ? evaluationLogic.getTickerSellEvalutions( + holdingTickerIds, pattern, tickerInfos, ) : [] - const sellTickerIds = sellTickerEvaluations.map((sellTickerEvaluation) => sellTickerEvaluation.tickerId) + const sellTickerIds = tickerSellEvaluations.map((tickerSellEvaluation) => tickerSellEvaluation.tickerId) const { holdingDetail: detailAfterSell, diff --git a/server/tools/cache.ts b/server/tools/cache.ts index a6c9095c..a796fd12 100644 --- a/server/tools/cache.ts +++ b/server/tools/cache.ts @@ -4,6 +4,10 @@ export const generateDailyTickersKey = (entityId: number, date: string): string return `${adapterEnum.CacheKey.DailyTickers}-[${entityId}+${date}]` } +export const generateDailyIndicatorsKey = (date: string): string => { + return `${adapterEnum.CacheKey.DailyIndicators}-${date}]` +} + export const generateTickerPricesKey = (entityId: number, date: string): string => { return `${adapterEnum.CacheKey.TickerPrices}-[${entityId}+${date}]` } From fb7e607a95239784b0a6d438f5bcbfcdcdd51a45 Mon Sep 17 00:00:00 2001 From: Baozier Date: Mon, 23 Oct 2023 10:07:56 -0400 Subject: [PATCH 3/5] save --- package.json | 5 +- server/logics/evaluation.ts | 80 ++++++---------- server/logics/transaction.ts | 170 ++++++++++++++++++--------------- server/services/calcTraders.ts | 73 ++++++++------ 4 files changed, 172 insertions(+), 156 deletions(-) diff --git a/package.json b/package.json index a00f5c29..3456191f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "melody-app", + "name": "melody-invest", "private": true, "workspaces": [ "server", @@ -11,7 +11,6 @@ "version": "0.0.2", "description": "", "author": "Baozier", - "license": "UNLICENSED", "engines": { "node": "^18.12.1", "npm": "^9.1.2", @@ -20,7 +19,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/ValueMelody/melody-app" + "url": "git+https://github.com/ValueMelody/melody-invest" }, "scripts": { "shared": "npm run build --workspace=constants && npm run build --workspace=helpers", diff --git a/server/logics/evaluation.ts b/server/logics/evaluation.ts index 1054750b..269834c3 100644 --- a/server/logics/evaluation.ts +++ b/server/logics/evaluation.ts @@ -307,6 +307,8 @@ export const isIndicatorFitPatternBehaviors = ( movementBehaviors: interfaces.traderPatternModel.IndicatorMovementBehavior[], compareBehaviors: interfaces.traderPatternModel.indicatorCompareBehavior[], ): boolean => { + if (!indicatorInfo) return false + // Make sure every movement behaviors in one trader pattern fit current indicator const movementFit = movementBehaviors.every((behavior) => { const indicatorKey = IndicatorMovementTriggers[behavior] @@ -446,27 +448,32 @@ export const getTickerWithSellEvaluation = ( export const getTickerWithBuyEvaluation = ( tickerId: number, pattern: interfaces.traderPatternModel.Record, - dailyTicker: interfaces.dailyTickersModel.TickerInfo | null, + tickerInfo: interfaces.dailyTickersModel.TickerInfo | null, ): TickerWithEvaluation | null => { - if (!dailyTicker) return null - return null + // When no available info for this tickerId, then do not buy + if (!tickerInfo) return null - // const preferValue = getTickerPreferValue( - // pattern.buyPreference, dailyTicker.daily, dailyTicker.quarterly, dailyTicker.yearly, - // ) - // if (!preferValue && preferValue !== 0) return null + // Check current patterns buy preference if tickers are equal weight + const preferValue = getTickerEqualWeightPreferValue( + pattern.buyPreference, tickerInfo, + ) + if (preferValue === null) return null - // const weight = getTickerMovementWeights( - // pattern, - // dailyTicker.info, - // constants.Behavior.TickerMovementBuyBehaviors, - // ) + // Get current tickers buy weight + const weight = getTickerWeights( + pattern, + tickerInfo, + constants.Behavior.TickerMovementBuyBehaviors, + constants.Behavior.TickerCompareBuyBehaviors, + ) - // if (!weight) return null + if (!weight) return null - // return { - // tickerId, preferValue, weight, - // } + return { + tickerId, + preferValue, + weight, + } } export const getOrderedTickerEvaluations = ( @@ -485,51 +492,24 @@ export const getOrderedTickerEvaluations = ( }) } -export const getTickerBuyEaluations = ( +export const getTickerBuyEvaluations = ( tickerIds: number[], pattern: interfaces.traderPatternModel.Record, - dailyTickers: interfaces.dailyTickersModel.TickerInfos, + tickerInfos: interfaces.dailyTickersModel.TickerInfos, ) => { - const emptyEvaluations: TickerWithEvaluation[] = [] const tickerEvaluations = tickerIds.reduce((evaluations, tickerId) => { const evaluation = getTickerWithBuyEvaluation( - tickerId, pattern, dailyTickers[tickerId], + tickerId, pattern, tickerInfos[tickerId], ) if (evaluation) evaluations.push(evaluation) return evaluations - }, emptyEvaluations) + }, [] as TickerWithEvaluation[]) + // Order tickerEvaluations by weight and prefer value const orderedEvaluations = getOrderedTickerEvaluations(tickerEvaluations, pattern.buyPreference) return orderedEvaluations } -export const getIndicatorBuyMatches = ( - pattern: interfaces.traderPatternModel.Record, - indicatorInfo: interfaces.dailyIndicatorsModel.IndicatorInfo, -): boolean => { - const shouldBuy = getIndicatorMovementAndCompareMatches( - pattern, - indicatorInfo, - constants.Behavior.IndicatorMovementBuyBehaviors, - constants.Behavior.IndicatorCompareBuyBehaviors, - ) - return shouldBuy -} - -export const shouldSellBasedOnIndicator = ( - pattern: interfaces.traderPatternModel.Record, - indicatorInfo: interfaces.dailyIndicatorsModel.IndicatorInfo, -): boolean => { - // Should sell if indicator matches all pattern behaviors - const shouldSell = isIndicatorFitPatternBehaviors( - pattern, - indicatorInfo, - constants.Behavior.IndicatorMovementSellBehaviors, - constants.Behavior.IndicatorCompareSellBehaviors, - ) - return shouldSell -} - -export const getTickerSellEvalutions = ( +export const getTickerSellEvaluations = ( tickerIds: number[], pattern: interfaces.traderPatternModel.Record, tickerInfos: interfaces.dailyTickersModel.TickerInfos, @@ -542,7 +522,7 @@ export const getTickerSellEvalutions = ( if (evaluation) evaluations.push(evaluation) return evaluations }, [] as TickerWithEvaluation[]) - // Order tickerEvalutions by weight and prefer value + // Order tickerEvaluations by weight and prefer value const orderedEvaluations = getOrderedTickerEvaluations(tickerEvaluations, pattern.sellPreference) return orderedEvaluations } diff --git a/server/logics/transaction.ts b/server/logics/transaction.ts index bc8805b1..4a77a7c1 100644 --- a/server/logics/transaction.ts +++ b/server/logics/transaction.ts @@ -191,37 +191,46 @@ export const rebalanceHoldingDetail = ( } } -export const sellForHoldingPercent = ( +export const sellItemFromHolding = ( holdingDetail: interfaces.traderHoldingModel.Detail, itemForSell: interfaces.traderHoldingModel.Item, - tickerDaily: interfaces.tickerDailyModel.Record, + tickerInfo: interfaces.dailyTickersModel.TickerInfo, holdingSellPercent: number, tickerMinPercent: number, - maxCashValue: number, + cashMaxPercent: number, ): interfaces.traderHoldingModel.Detail | null => { - const sharesSold = Math.floor(itemForSell.shares * tickerDaily.splitMultiplier * holdingSellPercent / 100) - const baseSharesShold = holdingSellPercent === 100 ? itemForSell.shares : sharesSold / tickerDaily.splitMultiplier + // Get how many shares and base shares should be sold + const sharesSold = Math.floor(itemForSell.shares * tickerInfo.splitMultiplier * holdingSellPercent) + const baseSharesShold = holdingSellPercent === 1 ? itemForSell.shares : sharesSold / tickerInfo.splitMultiplier if (itemForSell.shares < baseSharesShold) return null - const valueSold = getItemHoldingValue(baseSharesShold, 0, tickerDaily) + const valueSold = sharesSold * tickerInfo.closePrice + + // If hold percentage is less than min required hold percentage, then do not sell const percentAfterSell = (itemForSell.value - valueSold) / holdingDetail.totalValue - if (percentAfterSell * 100 < tickerMinPercent) return null + if (percentAfterSell < tickerMinPercent) return null + // If case after sell is larger than maxmium allow cash, then do not sell const cashAfterSell = holdingDetail.totalCash + valueSold - if (cashAfterSell > maxCashValue) return null + const cashMaxValue = holdingDetail.totalValue * cashMaxPercent + if (cashAfterSell > cashMaxValue) return null const sharesAfterSell = itemForSell.shares - baseSharesShold - const valueAfterSell = sharesAfterSell * tickerDaily.closePrice * tickerDaily.splitMultiplier - const itemDetail = { - tickerId: itemForSell.tickerId, - shares: sharesAfterSell, - value: valueAfterSell, - splitMultiplier: tickerDaily.splitMultiplier, - } - const items = sharesAfterSell - ? holdingDetail.items.map((item) => item.tickerId === itemForSell.tickerId ? itemDetail : item) - : holdingDetail.items.filter((item) => item.tickerId !== itemForSell.tickerId) + let items + // If no shares left, then remove from holdings + if (sharesAfterSell) { + const valueAfterSell = sharesAfterSell * tickerInfo.closePrice * tickerInfo.splitMultiplier + const itemDetail = { + tickerId: itemForSell.tickerId, + shares: sharesAfterSell, + value: valueAfterSell, + splitMultiplier: tickerInfo.splitMultiplier, + } + items = holdingDetail.items.map((item) => item.tickerId === itemForSell.tickerId ? itemDetail : item) + } else { + items = holdingDetail.items.filter((item) => item.tickerId !== itemForSell.tickerId) + } return { date: '', @@ -231,63 +240,66 @@ export const sellForHoldingPercent = ( } } -export const detailAfterSell = ( +export const getHoldingDetailAfterSell = ( currentDetail: interfaces.traderHoldingModel.Detail, sellTickerIds: number[], - dailyTickers: interfaces.dailyTickersModel.TickerInfos, + tickerInfos: interfaces.dailyTickersModel.TickerInfos, holdingSellPercent: number, tickerMinPercent: number, - maxCashValue: number, + cashMaxPercent: number, ): DetailAndTransaction => { - // let hasTransaction = false + let hasTransaction = false const holdingDetail = sellTickerIds.reduce((details, tickerId) => { - return details + const tickerInfo = tickerInfos[tickerId] + const item = details.items.find((item) => item.tickerId === tickerId) + // Make sure holding and ticker info exists + if (!tickerInfo || !item) return details + + const refreshedDetails = sellItemFromHolding( + details, + item, + tickerInfo, + holdingSellPercent, + tickerMinPercent, + cashMaxPercent, + ) + // If nothing sold, keep details the same + if (!refreshedDetails) return details - // const matchedDaily = dailyTickers[tickerId]?.daily - // const item = details.items.find((item) => item.tickerId === tickerId) - // if (!matchedDaily || !item) return details - - // const refreshed = sellForHoldingPercent( - // details, - // item, - // matchedDaily, - // holdingSellPercent, - // tickerMinPercent, - // maxCashValue, - // ) - // if (!refreshed) return details - - // hasTransaction = true - // return refreshed + hasTransaction = true + return refreshedDetails }, currentDetail) return { - hasTransaction: false, + hasTransaction, holdingDetail, } } -export const buyForHoldingPercent = ( +export const buyItemToHolding = ( holdingDetail: interfaces.traderHoldingModel.Detail, itemForBuy: interfaces.traderHoldingModel.Item, - tickerDaily: interfaces.tickerDailyModel.Record, - maxBuyAmount: number, + tickerInfo: interfaces.dailyTickersModel.TickerInfo, + holdingBuyPercent: number, tickerMaxPercent: number, ): interfaces.traderHoldingModel.Detail | null => { - const isNewHolding = !itemForBuy.shares + const maxBuyAmount = holdingDetail.totalValue & holdingBuyPercent + + // Use maximum allowed cash or use total cash left if less than maximun allowed const maxCashToUse = holdingDetail.totalCash < maxBuyAmount ? holdingDetail.totalCash : maxBuyAmount - const sharesBought = Math.floor(maxCashToUse / tickerDaily.closePrice) - if (!sharesBought) return null - const baseSharesBought = sharesBought / tickerDaily.splitMultiplier - const adjustedPrice = tickerDaily.closePrice * tickerDaily.splitMultiplier - const valueBought = baseSharesBought * adjustedPrice + const sharesBought = Math.floor(maxCashToUse / tickerInfo.closePrice) + if (!sharesBought) return null + const baseSharesBought = sharesBought / tickerInfo.splitMultiplier + const valueBought = sharesBought * tickerInfo.closePrice const sharesAfterBuy = baseSharesBought + itemForBuy.shares - const valueAfterBuy = sharesAfterBuy * adjustedPrice - const percentAfterBuy = (valueAfterBuy / holdingDetail.totalValue) * 100 + const valueAfterBuy = sharesAfterBuy * tickerInfo.closePrice * tickerInfo.splitMultiplier + + // If hold more than maxmum allowed percentage after buy, then do not buy + const percentAfterBuy = valueAfterBuy / holdingDetail.totalValue const isLessThanMaxPercent = percentAfterBuy <= tickerMaxPercent if (!isLessThanMaxPercent) return null @@ -295,12 +307,14 @@ export const buyForHoldingPercent = ( tickerId: itemForBuy.tickerId, shares: sharesAfterBuy, value: valueAfterBuy, - splitMultiplier: tickerDaily.splitMultiplier, + splitMultiplier: tickerInfo.splitMultiplier, } + const isNewHolding = !itemForBuy.shares const items = isNewHolding ? [...holdingDetail.items, itemDetail] : holdingDetail.items.map((item) => item.tickerId === itemForBuy.tickerId ? itemDetail : item) + return { totalValue: holdingDetail.totalValue, totalCash: holdingDetail.totalCash - valueBought, @@ -309,37 +323,41 @@ export const buyForHoldingPercent = ( } } -export const detailAfterBuy = ( +export const getHoldingDetailAfterBuy = ( currentDetail: interfaces.traderHoldingModel.Detail, buyTickerIds: number[], - dailyTickers: interfaces.dailyTickersModel.TickerInfos, - maxBuyAmount: number, + tickerInfos: interfaces.dailyTickersModel.TickerInfos, + holdingBuyPercent: number, tickerMaxPercent: number, ): DetailAndTransaction => { - // let hasTransaction = false - const holdingDetail = buyTickerIds.reduce((detail, tickerId) => { - return detail - // const matchedDaily = dailyTickers[tickerId]?.daily - // if (!matchedDaily) return detail - - // const item = currentDetail.items.find((item) => item.tickerId === tickerId) || - // { tickerId, shares: 0, splitMultiplier: 0, value: 0 } - - // const refreshed = buyForHoldingPercent( - // detail, - // item, - // matchedDaily, - // maxBuyAmount, - // tickerMaxPercent, - // ) - // if (!refreshed) return detail - - // hasTransaction = true - // return refreshed + let hasTransaction = false + const holdingDetail = buyTickerIds.reduce((details, tickerId) => { + const tickerInfo = tickerInfos[tickerId] + // Make sure holding and ticker info exists + if (!tickerInfo) return details + + // Find existing holding item or initial an empty one + const item = details.items.find((item) => item.tickerId === tickerId) || { + tickerId, shares: 0, splitMultiplier: 0, value: 0 + } + + const refreshedDetails = buyItemToHolding( + details, + item, + tickerInfo, + holdingBuyPercent, + tickerMaxPercent, + ) + + // If nothing bought, keep details the same + if (!refreshedDetails) return details + + hasTransaction = true + return refreshedDetails }, currentDetail) return { holdingDetail, - hasTransaction: false, + hasTransaction, } } diff --git a/server/services/calcTraders.ts b/server/services/calcTraders.ts index 569ddb8d..cbdca69a 100644 --- a/server/services/calcTraders.ts +++ b/server/services/calcTraders.ts @@ -127,8 +127,8 @@ const calcTraderPerformance = async ( const cashMaxPercent = pattern.cashMaxPercent / 100 const tickerMinPercent = pattern.tickerMinPercent / 100 const tickerMaxPercent = pattern.tickerMaxPercent / 100 - const holdingBuyPercent = pattern.holdingBuyPercent - const holdingSellPercent = pattern.holdingSellPercent + const holdingBuyPercent = pattern.holdingBuyPercent / 100 + const holdingSellPercent = pattern.holdingSellPercent / 100 console.info(`Checking Trader:${trader.id}`) const transaction = await databaseAdapter.createTransaction() @@ -185,60 +185,72 @@ const calcTraderPerformance = async ( ) // Check if indicatorInfo matches sell criterion - const shouldSellBasedOnIndicator = !!indicatorInfo && - evaluationLogic.shouldSellBasedOnIndicator(pattern, indicatorInfo) + const shouldSellBasedOnIndicator = evaluationLogic.isIndicatorFitPatternBehaviors( + pattern, + indicatorInfo, + constants.Behavior.IndicatorMovementSellBehaviors, + constants.Behavior.IndicatorCompareSellBehaviors, + ) // Get a list of ordered tickerIds that should be sold const holdingTickerIds = rebalancedDetail.items.map((item) => item.tickerId) + + // Get evaluations of each ticker, order by which one should be sold first const tickerSellEvaluations = shouldSellBasedOnIndicator - ? evaluationLogic.getTickerSellEvalutions( + ? evaluationLogic.getTickerSellEvaluations( holdingTickerIds, pattern, tickerInfos, ) : [] const sellTickerIds = tickerSellEvaluations.map((tickerSellEvaluation) => tickerSellEvaluation.tickerId) + // Update holding after sell target tickers const { holdingDetail: detailAfterSell, hasTransaction: hasSellTransaction, - } = transactionLogic.detailAfterSell( - detailAfterRebalance, + } = transactionLogic.getHoldingDetailAfterSell( + rebalancedDetail, sellTickerIds, - availableTargets, + availableTickerInfos, holdingSellPercent, tickerMinPercent, - maxCashValue, + cashMaxPercent, ) - const isBuyIndicatorMatches = !!indicatorInfo && evaluationLogic.getIndicatorBuyMatches(pattern, indicatorInfo) - const buyTickerEvaluations = isBuyIndicatorMatches - ? evaluationLogic.getTickerBuyEaluations( - Object.keys(availableTargets).map((id) => parseInt(id)), - pattern, - availableTargets, + // Check if indicatorInfo matches buy criterion + const shouldBuyBasedOnIndicator = evaluationLogic.isIndicatorFitPatternBehaviors( + pattern, + indicatorInfo, + constants.Behavior.IndicatorMovementBuyBehaviors, + constants.Behavior.IndicatorCompareBuyBehaviors, + ) + + // Get a list of tickerIds that could be trade + const availableTickerIds = Object.keys(availableTickerInfos).map((id) => parseInt(id)) + + // Get evaluations of each ticker, order by which one should be bought first + const tickerBuyEvaluations = shouldBuyBasedOnIndicator + ? evaluationLogic.getTickerBuyEvaluations( + availableTickerIds, pattern, tickerInfos, ) : [] - const buyTickerIds = buyTickerEvaluations.map((evaluation) => evaluation.tickerId) + const buyTickerIds = tickerBuyEvaluations.map((evaluation) => evaluation.tickerId) - const maxBuyAmount = detailAfterSell.totalValue * holdingBuyPercent / 100 + // Update holding after buy target tickers const { holdingDetail: detailAfterBuy, hasTransaction: hasBuyTransaction, - } = transactionLogic.detailAfterBuy( + } = transactionLogic.getHoldingDetailAfterBuy( detailAfterSell, buyTickerIds, - availableTargets, - maxBuyAmount, + tickerInfos, + holdingBuyPercent, tickerMaxPercent, ) - if (shouldRebalance && hasRebalanceTransaction) { - rebalancedAt = tradeDate - hasRebalanced = true - } - const hasTransaction = hasRebalanceTransaction || hasSellTransaction || hasBuyTransaction + if (hasTransaction) { - hasCreatedAnyRecord = true + shouldCommitTransaction = true if (!startedAt) startedAt = tradeDate holding = await traderHoldingModel.create({ traderId: trader.id, @@ -249,10 +261,15 @@ const calcTraderPerformance = async ( }, transaction) } + if (shouldRebalance && hasRebalanceTransaction) { + rebalancedAt = tradeDate + hasRebalanced = true + } + tradeDate = nextDate } - if (hasCreatedAnyRecord) { + if (shouldCommitTransaction) { await transaction.commit() } else { await transaction.rollback() @@ -264,12 +281,14 @@ const calcTraderPerformance = async ( const traderTransaction = await databaseAdapter.createTransaction() try { + // If not holding created or there is no start date, then save estimate date only if (!holding || !startedAt) { await traderModel.update(trader.id, { estimatedAt: latestDate }, traderTransaction) await traderTransaction.commit() return } + // Regenerate tickerHolder records for current trader await tickerHolderModel.destroyTraderTickers(trader.id, traderTransaction) await runTool.asyncForEach(holding.items, async ( item: interfaces.traderHoldingModel.Item, From 2a1d016fcd2e2a2899bb07a15d6f6dc07299d5c1 Mon Sep 17 00:00:00 2001 From: Baozier Date: Mon, 23 Oct 2023 10:23:28 -0400 Subject: [PATCH 4/5] save --- .eslintrc.json | 2 +- client/src/actions/general.test.ts | 2 +- client/src/actions/user.ts | 2 +- client/src/adapters/request.test.ts | 2 +- client/src/containers/Client.test.tsx | 2 +- client/src/containers/Client.tsx | 2 +- .../containers/accounts/Activation.test.tsx | 2 +- .../src/containers/accounts/Forgot.test.tsx | 2 +- client/src/containers/accounts/Forgot.tsx | 2 +- client/src/containers/accounts/Reset.test.tsx | 2 +- client/src/containers/accounts/Reset.tsx | 2 +- .../src/containers/accounts/Setting.test.tsx | 2 +- .../src/containers/accounts/SignIn.test.tsx | 2 +- client/src/containers/accounts/SignIn.tsx | 2 +- .../src/containers/accounts/SignUp.test.tsx | 2 +- client/src/containers/accounts/SignUp.tsx | 2 +- .../accounts/blocks/PaymentModal.tsx | 2 +- client/src/containers/general/Home.test.tsx | 2 +- .../src/containers/general/Privacy.test.tsx | 2 +- client/src/containers/general/Terms.test.tsx | 2 +- .../containers/layouts/blocks/Footer.test.tsx | 2 +- .../containers/layouts/blocks/Header.test.tsx | 2 +- .../layouts/elements/HeaderLink.test.tsx | 2 +- .../traders/behaviors/BehaviorDetail.test.tsx | 2 +- .../traders/behaviors/BehaviorList.test.tsx | 2 +- .../traders/blocks/ComboProfiles.test.tsx | 2 +- .../traders/blocks/ComboProfiles.tsx | 2 +- .../containers/traders/blocks/EachTops.tsx | 2 +- .../traders/blocks/TraderComboCard.test.tsx | 2 +- .../traders/blocks/TraderComboCard.tsx | 2 +- .../traders/blocks/TraderEnvCard.test.tsx | 131 -------------- .../traders/blocks/TraderEnvCard.tsx | 2 +- .../traders/blocks/TraderProfileCard.test.tsx | 2 +- .../traders/blocks/UnwatchEnvButton.test.tsx | 2 +- .../traders/blocks/UnwatchEnvButton.tsx | 2 +- .../traders/combos/ComboBuilder.test.tsx | 2 +- .../traders/combos/ComboDetail.test.tsx | 2 +- .../traders/elements/BehaviorEditor.tsx | 2 +- .../traders/elements/HoldingShare.test.tsx | 2 +- .../elements/PatternBehaviors.test.tsx | 2 +- .../traders/elements/PatternBehaviors.tsx | 2 +- .../traders/elements/ProfileLabel.test.tsx | 2 +- .../traders/elements/ProfileValue.test.tsx | 2 +- .../traders/elements/ProfileValue.tsx | 2 +- .../traders/elements/TickerLabel.test.tsx | 2 +- .../traders/elements/ValueChangePanel.tsx | 2 +- .../traders/envs/EnvBuilder.test.tsx | 2 +- .../traders/envs/EnvDetail.test.tsx | 2 +- .../profiles/ProfileDashboard.test.tsx | 2 +- .../traders/profiles/ProfileDetail.test.tsx | 2 +- .../profile-builder/ProfileBuilder.test.tsx | 2 +- .../profile-builder/ProfileBuilder.tsx | 4 +- client/src/hooks/usePrivateGuard.test.ts | 2 +- client/src/hooks/usePublicGuard.test.ts | 2 +- client/src/index.tsx | 2 +- client/src/selectors/general.test.ts | 2 +- client/src/selectors/tickerIdentity.test.ts | 2 +- client/src/selectors/traderBehavior.test.ts | 2 +- client/src/selectors/traderCombo.test.ts | 2 +- client/src/selectors/traderEnv.test.ts | 2 +- client/src/selectors/traderProfile.test.ts | 2 +- client/src/selectors/user.test.ts | 2 +- client/src/stores/tickerIdentity.test.ts | 2 +- client/src/stores/traderBehavior.test.ts | 2 +- client/src/stores/traderCombo.test.ts | 2 +- client/src/stores/traderEnv.test.ts | 2 +- client/src/stores/traderProfile.test.ts | 2 +- client/src/stores/user.test.ts | 2 +- client/src/tools/parse.test.ts | 160 ------------------ constants/src/behaviorValue.test.ts | 20 --- package-lock.json | 5 +- server/adapters/database.test.ts | 153 ----------------- server/cron.test.ts | 2 +- server/index.ts | 8 +- server/logics/email.test.ts | 2 +- server/logics/holding.test.ts | 2 +- server/logics/price.test.ts | 2 +- server/logics/trader.test.ts | 2 +- server/logics/transaction.ts | 2 +- server/middlewares/access.test.ts | 2 +- server/middlewares/auth.test.ts | 2 +- server/models/dailyTickers.test.ts | 84 --------- server/models/traderEnv.test.ts | 143 ---------------- server/routers/system.test.ts | 2 +- server/routers/traders.test.ts | 2 +- server/routers/users.test.ts | 2 +- server/services/calcTickers.ts | 7 +- server/services/crudSystems.test.ts | 2 +- server/services/crudTraders.ts | 4 +- server/services/crudUsers.test.ts | 2 +- server/services/processEmails.test.ts | 2 +- .../services/shared/buildHoldingValueStats.ts | 2 +- server/tasks/cache.test.ts | 2 +- server/tasks/calc.test.ts | 2 +- server/tasks/email.test.ts | 2 +- server/tasks/sync.test.ts | 2 +- server/tools/date.test.ts | 2 +- server/tools/generate.test.ts | 2 +- server/tools/generate.ts | 4 +- server/tools/locale.test.ts | 2 +- 100 files changed, 106 insertions(+), 793 deletions(-) delete mode 100644 client/src/containers/traders/blocks/TraderEnvCard.test.tsx delete mode 100644 client/src/tools/parse.test.ts delete mode 100644 constants/src/behaviorValue.test.ts delete mode 100644 server/adapters/database.test.ts delete mode 100644 server/models/dailyTickers.test.ts delete mode 100644 server/models/traderEnv.test.ts diff --git a/.eslintrc.json b/.eslintrc.json index a042dfd9..079b5581 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -25,7 +25,7 @@ "message": "Don't declare enums" } ], - "sort-imports": "error", + "import/order": "error", "react/jsx-closing-bracket-location": "error", "react/jsx-max-props-per-line": "error", "react/jsx-indent-props": ["error", 2], diff --git a/client/src/actions/general.test.ts b/client/src/actions/general.test.ts index 725c8761..634a4392 100644 --- a/client/src/actions/general.test.ts +++ b/client/src/actions/general.test.ts @@ -1,8 +1,8 @@ -import * as general from './general' import * as requestAdapter from 'adapters/request' import { act, waitFor } from 'test.utils' import { globalSlice } from 'stores/global' import { store } from 'stores' +import * as general from './general' jest.mock('adapters/request', () => { const actual = jest.requireActual('adapters/request') diff --git a/client/src/actions/user.ts b/client/src/actions/user.ts index 73f893ea..3a75efbe 100644 --- a/client/src/actions/user.ts +++ b/client/src/actions/user.ts @@ -2,8 +2,8 @@ import * as interfaces from '@shared/interfaces' import * as localeTool from 'tools/locale' import * as requestAdapter from 'adapters/request' import * as routerEnum from 'enums/router' -import { logout, refreshAccessToken } from './general' import { createAsyncThunk } from '@reduxjs/toolkit' +import { logout, refreshAccessToken } from './general' export const fetchUserEntity = createAsyncThunk( 'user/fetchUserEntity', diff --git a/client/src/adapters/request.test.ts b/client/src/adapters/request.test.ts index 29d6e1c1..6514c8ae 100644 --- a/client/src/adapters/request.test.ts +++ b/client/src/adapters/request.test.ts @@ -1,6 +1,6 @@ import * as localeTool from 'tools/locale' -import * as request from './request' import axios from 'axios' +import * as request from './request' describe('#setAuthToken', () => { test('could set auth', () => { diff --git a/client/src/containers/Client.test.tsx b/client/src/containers/Client.test.tsx index 7ae0a404..e9a9a503 100644 --- a/client/src/containers/Client.test.tsx +++ b/client/src/containers/Client.test.tsx @@ -1,9 +1,9 @@ import * as routerEnum from 'enums/router' import { act, render, screen } from 'test.utils' -import Client from './Client' import { createMemoryHistory } from 'history' import { globalSlice } from 'stores/global' import { store } from 'stores' +import Client from './Client' jest.mock('react-select', () => '') diff --git a/client/src/containers/Client.tsx b/client/src/containers/Client.tsx index 155e7ba7..b841272b 100644 --- a/client/src/containers/Client.tsx +++ b/client/src/containers/Client.tsx @@ -1,7 +1,7 @@ import * as selectors from 'selectors' -import Router from './Router' import { Spinner } from 'flowbite-react' import { useSelector } from 'react-redux' +import Router from './Router' const Client = () => { const { isLoading } = useSelector(selectors.selectGlobal()) diff --git a/client/src/containers/accounts/Activation.test.tsx b/client/src/containers/accounts/Activation.test.tsx index 47482488..14a43046 100644 --- a/client/src/containers/accounts/Activation.test.tsx +++ b/client/src/containers/accounts/Activation.test.tsx @@ -3,10 +3,10 @@ import * as routerTool from 'tools/router' import * as usePublicGuard from 'hooks/usePublicGuard' import * as userAction from 'actions/user' import { Route, Routes } from 'react-router-dom' -import Activation from './Activation' import { createAsyncThunk } from '@reduxjs/toolkit' import { createMemoryHistory } from 'history' import { render } from 'test.utils' +import Activation from './Activation' jest.mock('hooks/usePublicGuard', () => { const actual = jest.requireActual('hooks/usePublicGuard') diff --git a/client/src/containers/accounts/Forgot.test.tsx b/client/src/containers/accounts/Forgot.test.tsx index f9948798..263849b3 100644 --- a/client/src/containers/accounts/Forgot.test.tsx +++ b/client/src/containers/accounts/Forgot.test.tsx @@ -3,9 +3,9 @@ import * as routerTool from 'tools/router' import * as usePublicGuard from 'hooks/usePublicGuard' import * as userAction from 'actions/user' import { fireEvent, render, screen } from 'test.utils' -import Forgot from './Forgot' import { createAsyncThunk } from '@reduxjs/toolkit' import { createMemoryHistory } from 'history' +import Forgot from './Forgot' jest.mock('hooks/usePublicGuard', () => { const actual = jest.requireActual('hooks/usePublicGuard') diff --git a/client/src/containers/accounts/Forgot.tsx b/client/src/containers/accounts/Forgot.tsx index 215e380b..48fc840f 100644 --- a/client/src/containers/accounts/Forgot.tsx +++ b/client/src/containers/accounts/Forgot.tsx @@ -3,11 +3,11 @@ import * as localeTool from 'tools/locale' import * as routerTool from 'tools/router' import { Button, TextInput } from 'flowbite-react' import { ChangeEvent, FormEvent, useState } from 'react' -import GoToButton from './elements/GoToButton' import RequiredLabel from 'containers/elements/RequiredLabel' import { useDispatch } from 'react-redux' import { useNavigate } from 'react-router-dom' import usePublicGuard from 'hooks/usePublicGuard' +import GoToButton from './elements/GoToButton' const Forgot = () => { usePublicGuard() diff --git a/client/src/containers/accounts/Reset.test.tsx b/client/src/containers/accounts/Reset.test.tsx index c915e3bc..6fae4933 100644 --- a/client/src/containers/accounts/Reset.test.tsx +++ b/client/src/containers/accounts/Reset.test.tsx @@ -5,10 +5,10 @@ import * as usePublicGuard from 'hooks/usePublicGuard' import * as userAction from 'actions/user' import { Route, Routes } from 'react-router-dom' import { fireEvent, render, screen } from 'test.utils' -import Reset from './Reset' import { createAsyncThunk } from '@reduxjs/toolkit' import { createMemoryHistory } from 'history' import { store } from 'stores' +import Reset from './Reset' jest.mock('hooks/usePublicGuard', () => { const actual = jest.requireActual('hooks/usePublicGuard') diff --git a/client/src/containers/accounts/Reset.tsx b/client/src/containers/accounts/Reset.tsx index 2676cf1e..03861870 100644 --- a/client/src/containers/accounts/Reset.tsx +++ b/client/src/containers/accounts/Reset.tsx @@ -4,12 +4,12 @@ import * as routerTool from 'tools/router' import { Button, TextInput } from 'flowbite-react' import { ChangeEvent, FormEvent, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' -import GoToButton from './elements/GoToButton' import RequiredLabel from 'containers/elements/RequiredLabel' import { globalSlice } from 'stores/global' import { useDispatch } from 'react-redux' import usePasswordValidator from 'hooks/usePasswordValidator' import usePublicGuard from 'hooks/usePublicGuard' +import GoToButton from './elements/GoToButton' const Reset = () => { usePublicGuard() diff --git a/client/src/containers/accounts/Setting.test.tsx b/client/src/containers/accounts/Setting.test.tsx index b6b3d67a..43af014b 100644 --- a/client/src/containers/accounts/Setting.test.tsx +++ b/client/src/containers/accounts/Setting.test.tsx @@ -3,9 +3,9 @@ import * as generalAction from 'actions/general' import * as selectors from 'selectors' import { act, fireEvent, render, screen, waitFor } from 'test.utils' import { instance, mock } from 'ts-mockito' -import Setting from './Setting' import { UserState } from 'stores/user' import axios from 'axios' +import Setting from './Setting' jest.mock('actions/general', () => { const actual = jest.requireActual('actions/general') diff --git a/client/src/containers/accounts/SignIn.test.tsx b/client/src/containers/accounts/SignIn.test.tsx index 9eb1bd6d..081b1a64 100644 --- a/client/src/containers/accounts/SignIn.test.tsx +++ b/client/src/containers/accounts/SignIn.test.tsx @@ -3,10 +3,10 @@ import * as routerTool from 'tools/router' import * as usePublicGuard from 'hooks/usePublicGuard' import * as userAction from 'actions/user' import { fireEvent, render, screen } from 'test.utils' -import SignIn from './SignIn' import { createAsyncThunk } from '@reduxjs/toolkit' import { createMemoryHistory } from 'history' import { store } from 'stores' +import SignIn from './SignIn' jest.mock('hooks/usePublicGuard', () => { const actual = jest.requireActual('hooks/usePublicGuard') diff --git a/client/src/containers/accounts/SignIn.tsx b/client/src/containers/accounts/SignIn.tsx index 5c3227fc..ec16c3cb 100644 --- a/client/src/containers/accounts/SignIn.tsx +++ b/client/src/containers/accounts/SignIn.tsx @@ -3,13 +3,13 @@ import * as localeTool from 'tools/locale' import * as routerTool from 'tools/router' import { Button, Checkbox, Label, TextInput } from 'flowbite-react' import { ChangeEvent, FormEvent, useState } from 'react' -import GoToButton from './elements/GoToButton' import RequiredLabel from 'containers/elements/RequiredLabel' import { globalSlice } from 'stores/global' import { useDispatch } from 'react-redux' import { useNavigate } from 'react-router-dom' import usePasswordValidator from 'hooks/usePasswordValidator' import usePublicGuard from 'hooks/usePublicGuard' +import GoToButton from './elements/GoToButton' const SignIn = () => { usePublicGuard() diff --git a/client/src/containers/accounts/SignUp.test.tsx b/client/src/containers/accounts/SignUp.test.tsx index c6c9e78f..c78c0031 100644 --- a/client/src/containers/accounts/SignUp.test.tsx +++ b/client/src/containers/accounts/SignUp.test.tsx @@ -4,13 +4,13 @@ import * as routerTool from 'tools/router' import * as usePublicGuard from 'hooks/usePublicGuard' import * as userAction from 'actions/user' import { act, fireEvent, render, screen } from 'test.utils' -import SignUp from './SignUp' import axios from 'axios' import { contentSlice } from 'stores/content' import { createAsyncThunk } from '@reduxjs/toolkit' import { createMemoryHistory } from 'history' import { globalSlice } from 'stores/global' import { store } from 'stores' +import SignUp from './SignUp' jest.mock('hooks/usePublicGuard', () => { const actual = jest.requireActual('hooks/usePublicGuard') diff --git a/client/src/containers/accounts/SignUp.tsx b/client/src/containers/accounts/SignUp.tsx index f5ea3563..95e85404 100644 --- a/client/src/containers/accounts/SignUp.tsx +++ b/client/src/containers/accounts/SignUp.tsx @@ -6,12 +6,12 @@ import * as selectors from 'selectors' import { Button, Checkbox, Label, TextInput, Textarea } from 'flowbite-react' import { ChangeEvent, FormEvent, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import GoToButton from './elements/GoToButton' import RequiredLabel from 'containers/elements/RequiredLabel' import { globalSlice } from 'stores/global' import { useNavigate } from 'react-router-dom' import usePasswordValidator from 'hooks/usePasswordValidator' import usePublicGuard from 'hooks/usePublicGuard' +import GoToButton from './elements/GoToButton' const SignUp = () => { usePublicGuard() diff --git a/client/src/containers/accounts/blocks/PaymentModal.tsx b/client/src/containers/accounts/blocks/PaymentModal.tsx index ddbdcca1..2be30ab3 100644 --- a/client/src/containers/accounts/blocks/PaymentModal.tsx +++ b/client/src/containers/accounts/blocks/PaymentModal.tsx @@ -4,8 +4,8 @@ import * as helpers from '@shared/helpers' import * as localeTool from 'tools/locale' import { Button, Card, Label, Modal, Radio, Select } from 'flowbite-react' import { ChangeEvent, useEffect, useMemo, useState } from 'react' -import SubscribeButton from './SubscribeButton' import classNames from 'classnames' +import SubscribeButton from './SubscribeButton' const PaymentModal = ({ userType, diff --git a/client/src/containers/general/Home.test.tsx b/client/src/containers/general/Home.test.tsx index 7b5adcea..52b361d3 100644 --- a/client/src/containers/general/Home.test.tsx +++ b/client/src/containers/general/Home.test.tsx @@ -2,8 +2,8 @@ import * as routerEnum from 'enums/router' import * as routerTool from 'tools/router' import { Route, Routes } from 'react-router-dom' import { fireEvent, render, screen } from 'test.utils' -import Home from './Home' import { createMemoryHistory } from 'history' +import Home from './Home' describe('#Home', () => { test('could redirect correctly', () => { diff --git a/client/src/containers/general/Privacy.test.tsx b/client/src/containers/general/Privacy.test.tsx index 94810ad7..9d670a07 100644 --- a/client/src/containers/general/Privacy.test.tsx +++ b/client/src/containers/general/Privacy.test.tsx @@ -1,9 +1,9 @@ import * as constants from '@shared/constants' import * as requestAdapter from 'adapters/request' import { render, screen, waitFor } from 'test.utils' -import Privacy from './Privacy' import { contentSlice } from 'stores/content' import { store } from 'stores' +import Privacy from './Privacy' jest.mock('adapters/request', () => { const actual = jest.requireActual('adapters/request') diff --git a/client/src/containers/general/Terms.test.tsx b/client/src/containers/general/Terms.test.tsx index 0fd8b623..be709c08 100644 --- a/client/src/containers/general/Terms.test.tsx +++ b/client/src/containers/general/Terms.test.tsx @@ -1,9 +1,9 @@ import * as constants from '@shared/constants' import * as requestAdapter from 'adapters/request' import { render, screen, waitFor } from 'test.utils' -import Terms from './Terms' import { contentSlice } from 'stores/content' import { store } from 'stores' +import Terms from './Terms' jest.mock('adapters/request', () => { const actual = jest.requireActual('adapters/request') diff --git a/client/src/containers/layouts/blocks/Footer.test.tsx b/client/src/containers/layouts/blocks/Footer.test.tsx index d1c7621d..da7bb541 100644 --- a/client/src/containers/layouts/blocks/Footer.test.tsx +++ b/client/src/containers/layouts/blocks/Footer.test.tsx @@ -1,8 +1,8 @@ import * as localeTool from 'tools/locale' import * as routerTool from 'tools/router' import { fireEvent, render, screen } from 'test.utils' -import Footer from './Footer' import { createMemoryHistory } from 'history' +import Footer from './Footer' describe('#Footer', () => { test('could render Footer', () => { diff --git a/client/src/containers/layouts/blocks/Header.test.tsx b/client/src/containers/layouts/blocks/Header.test.tsx index 5a2706ef..a2756a02 100644 --- a/client/src/containers/layouts/blocks/Header.test.tsx +++ b/client/src/containers/layouts/blocks/Header.test.tsx @@ -1,10 +1,10 @@ import * as localeTool from 'tools/locale' import * as routerTool from 'tools/router' import { fireEvent, render, screen } from 'test.utils' -import Header from './Header' import { createMemoryHistory } from 'history' import { globalSlice } from 'stores/global' import { store } from 'stores' +import Header from './Header' afterEach(() => { jest.clearAllMocks() diff --git a/client/src/containers/layouts/elements/HeaderLink.test.tsx b/client/src/containers/layouts/elements/HeaderLink.test.tsx index 489b08a0..428f4107 100644 --- a/client/src/containers/layouts/elements/HeaderLink.test.tsx +++ b/client/src/containers/layouts/elements/HeaderLink.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, render, screen } from 'test.utils' -import HeaderLink from './HeaderLink' import { StarIcon } from '@heroicons/react/24/solid' import { createMemoryHistory } from 'history' +import HeaderLink from './HeaderLink' describe('#HeaderLink', () => { test('could render Footer', () => { diff --git a/client/src/containers/traders/behaviors/BehaviorDetail.test.tsx b/client/src/containers/traders/behaviors/BehaviorDetail.test.tsx index c45422b7..ad346b9d 100644 --- a/client/src/containers/traders/behaviors/BehaviorDetail.test.tsx +++ b/client/src/containers/traders/behaviors/BehaviorDetail.test.tsx @@ -5,9 +5,9 @@ import * as selectors from 'selectors' import { UserAccess, UserState } from 'stores/user' import { act, fireEvent, render, screen } from 'test.utils' import { instance, mock } from 'ts-mockito' -import BehaviorDetail from './BehaviorDetail' import { GlobalState } from 'stores/global' import axios from 'axios' +import BehaviorDetail from './BehaviorDetail' afterEach(() => { jest.clearAllMocks() diff --git a/client/src/containers/traders/behaviors/BehaviorList.test.tsx b/client/src/containers/traders/behaviors/BehaviorList.test.tsx index cb08700c..64094231 100644 --- a/client/src/containers/traders/behaviors/BehaviorList.test.tsx +++ b/client/src/containers/traders/behaviors/BehaviorList.test.tsx @@ -3,8 +3,8 @@ import * as parseTool from 'tools/parse' import * as selectors from 'selectors' import { fireEvent, render, screen } from 'test.utils' import { instance, mock } from 'ts-mockito' -import BehaviorList from './BehaviorList' import { GlobalState } from 'stores/global' +import BehaviorList from './BehaviorList' const navigate = jest.fn() jest.mock('react-router-dom', () => { diff --git a/client/src/containers/traders/blocks/ComboProfiles.test.tsx b/client/src/containers/traders/blocks/ComboProfiles.test.tsx index 3d7d87da..663894d2 100644 --- a/client/src/containers/traders/blocks/ComboProfiles.test.tsx +++ b/client/src/containers/traders/blocks/ComboProfiles.test.tsx @@ -2,8 +2,8 @@ import * as interfaces from '@shared/interfaces' import * as localeTool from 'tools/locale' import * as selectors from 'selectors' import { fireEvent, render, screen } from 'test.utils' -import ComboProfiles from './ComboProfiles' import { mock } from 'ts-mockito' +import ComboProfiles from './ComboProfiles' jest.mock('selectors', () => { const actual = jest.requireActual('selectors') diff --git a/client/src/containers/traders/blocks/ComboProfiles.tsx b/client/src/containers/traders/blocks/ComboProfiles.tsx index bbb94150..28f2deb3 100644 --- a/client/src/containers/traders/blocks/ComboProfiles.tsx +++ b/client/src/containers/traders/blocks/ComboProfiles.tsx @@ -2,8 +2,8 @@ import * as interfaces from '@shared/interfaces' import * as localeTool from 'tools/locale' import * as parseTool from 'tools/parse' import TraderProfileCard from 'containers/traders/blocks/TraderProfileCard' -import WeightChart from '../elements/WeightChart' import { useState } from 'react' +import WeightChart from '../elements/WeightChart' interface ProfileWithEnv { profile?: interfaces.response.TraderProfile; diff --git a/client/src/containers/traders/blocks/EachTops.tsx b/client/src/containers/traders/blocks/EachTops.tsx index e8ca6d94..e56708ec 100644 --- a/client/src/containers/traders/blocks/EachTops.tsx +++ b/client/src/containers/traders/blocks/EachTops.tsx @@ -3,9 +3,9 @@ import * as localeTool from 'tools/locale' import * as routerTool from 'tools/router' import * as selectors from 'selectors' import { Alert } from 'flowbite-react' -import TraderProfileCard from './TraderProfileCard' import { useNavigate } from 'react-router-dom' import { useSelector } from 'react-redux' +import TraderProfileCard from './TraderProfileCard' const sectionClass = 'w-full m-4 max-sm:w-80' const titleClass = 'font-semibold mb-4' diff --git a/client/src/containers/traders/blocks/TraderComboCard.test.tsx b/client/src/containers/traders/blocks/TraderComboCard.test.tsx index bd030edf..1a76f880 100644 --- a/client/src/containers/traders/blocks/TraderComboCard.test.tsx +++ b/client/src/containers/traders/blocks/TraderComboCard.test.tsx @@ -5,10 +5,10 @@ import * as selectors from 'selectors' import { UserState, userSlice } from 'stores/user' import { fireEvent, render, screen, waitFor } from 'test.utils' import { instance, mock } from 'ts-mockito' -import TraderComboCard from './TraderComboCard' import axios from 'axios' import { store } from 'stores' import { traderComboSlice } from 'stores/traderCombo' +import TraderComboCard from './TraderComboCard' jest.mock('selectors', () => { const actual = jest.requireActual('selectors') diff --git a/client/src/containers/traders/blocks/TraderComboCard.tsx b/client/src/containers/traders/blocks/TraderComboCard.tsx index d35826cf..2a3d3a29 100644 --- a/client/src/containers/traders/blocks/TraderComboCard.tsx +++ b/client/src/containers/traders/blocks/TraderComboCard.tsx @@ -6,9 +6,9 @@ import * as routerTool from 'tools/router' import * as selectors from 'selectors' import { useDispatch, useSelector } from 'react-redux' import { Card } from 'flowbite-react' -import WatchButton from '../elements/WatchButton' import classNames from 'classnames' import { useNavigate } from 'react-router-dom' +import WatchButton from '../elements/WatchButton' const TraderComboCard = ({ traderCombo, diff --git a/client/src/containers/traders/blocks/TraderEnvCard.test.tsx b/client/src/containers/traders/blocks/TraderEnvCard.test.tsx deleted file mode 100644 index 5ee85269..00000000 --- a/client/src/containers/traders/blocks/TraderEnvCard.test.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import * as constants from '@shared/constants' -import * as localeTool from 'tools/locale' -import * as parseTool from 'tools/parse' -import { fireEvent, render, screen } from 'test.utils' -import TraderEnvCard from './TraderEnvCard' -import { store } from 'stores' -import { traderEnvSlice } from 'stores/traderEnv' -import { userSlice } from 'stores/user' - -const traderEnv = { - id: 123, - entityId: 1, - name: 'test env', - startDate: '2001-01-02', - tickerIds: [1, 2, 3, 4, 5], - activeTotal: 100, -} - -const setupStore = () => { - store.dispatch(userSlice.actions._updateForTest({ - userType: constants.User.Type.Pro, - })) - store.dispatch(traderEnvSlice.actions._updateForTest({ - base: { 123: traderEnv }, - })) -} - -afterEach(() => { - jest.clearAllMocks() -}) - -describe('#traderComboCard', () => { - test('do not render if there is no env', () => { - render( - , - ) - const container = screen.queryByTestId('traderEnvCard') - expect(container).toBeFalsy() - }) - - test('could render', () => { - setupStore() - render( - , - ) - const container = screen.getByTestId('traderEnvCard') - expect(container).toBeTruthy() - - expect(screen.getByText(`Env: ${traderEnv.name}`)).toBeTruthy() - expect(screen.getByText(parseTool.traderEnvStartDate(traderEnv))).toBeTruthy() - expect(screen.queryByText('System')).toBeFalsy() - expect(screen.queryByText('Trade based on selected 5 stocks')).toBeTruthy() - expect(screen.queryByText(localeTool.t('traderEnv.allTickers') as string)).toBeFalsy() - expect(container.className).not.toContain('card-active') - - const watchButton = screen.queryByTestId('watchButton') - expect(watchButton).toBeFalsy() - - fireEvent.click(container) - }) - - test('could render all tickers', () => { - render( - , - ) - const container = screen.getByTestId('traderEnvCard') - expect(container).toBeTruthy() - expect(screen.getByText(localeTool.t('traderEnv.allTickers') as string)).toBeTruthy() - }) - - test('could render as active', () => { - render( - , - ) - const container = screen.getByTestId('traderEnvCard') - expect(container?.className).toContain('card-active') - }) - - test('could render as clickable', () => { - setupStore() - const onClick = jest.fn() - render( - , - ) - const container = screen.getByTestId('traderEnvCard') - - fireEvent.click(container) - expect(onClick).toBeCalledTimes(1) - expect(onClick).toBeCalledWith(123) - }) - - test('could render as disabled', () => { - const onClick = jest.fn() - render( - , - ) - const container = screen.getByTestId('traderEnvCard') - expect(container?.className).toContain('card-disabled') - const limitText = localeTool.t('permission.limited') - expect(screen.getByText(limitText)).toBeTruthy() - - const watchButton = screen.getByTestId('watchButton') - expect(watchButton).toBeTruthy() - - fireEvent.click(container) - expect(onClick).toBeCalledTimes(0) - }) -}) diff --git a/client/src/containers/traders/blocks/TraderEnvCard.tsx b/client/src/containers/traders/blocks/TraderEnvCard.tsx index 4c8e45b7..b5267841 100644 --- a/client/src/containers/traders/blocks/TraderEnvCard.tsx +++ b/client/src/containers/traders/blocks/TraderEnvCard.tsx @@ -3,9 +3,9 @@ import * as localeTool from 'tools/locale' import * as parseTool from 'tools/parse' import * as selectors from 'selectors' import { Card } from 'flowbite-react' -import UnwatchEnvButton from './UnwatchEnvButton' import classNames from 'classnames' import { useSelector } from 'react-redux' +import UnwatchEnvButton from './UnwatchEnvButton' const TraderEnvCard = ({ traderEnv, diff --git a/client/src/containers/traders/blocks/TraderProfileCard.test.tsx b/client/src/containers/traders/blocks/TraderProfileCard.test.tsx index df10dd4c..3e02e391 100644 --- a/client/src/containers/traders/blocks/TraderProfileCard.test.tsx +++ b/client/src/containers/traders/blocks/TraderProfileCard.test.tsx @@ -3,11 +3,11 @@ import * as localeTool from 'tools/locale' import * as selectors from 'selectors' import { act, fireEvent, render, screen } from 'test.utils' import { instance, mock } from 'ts-mockito' -import TraderProfileCard from './TraderProfileCard' import { UserState } from 'stores/user' import axios from 'axios' import { contentSlice } from 'stores/content' import { store } from 'stores' +import TraderProfileCard from './TraderProfileCard' afterEach(() => { jest.clearAllMocks() diff --git a/client/src/containers/traders/blocks/UnwatchEnvButton.test.tsx b/client/src/containers/traders/blocks/UnwatchEnvButton.test.tsx index fd491e24..3f89dbc6 100644 --- a/client/src/containers/traders/blocks/UnwatchEnvButton.test.tsx +++ b/client/src/containers/traders/blocks/UnwatchEnvButton.test.tsx @@ -2,8 +2,8 @@ import * as interfaces from '@shared/interfaces' import * as traderAction from 'actions/trader' import { act, fireEvent, render, screen } from 'test.utils' import { instance, mock } from 'ts-mockito' -import UnwatchEnvButton from './UnwatchEnvButton' import { createAsyncThunk } from '@reduxjs/toolkit' +import UnwatchEnvButton from './UnwatchEnvButton' const envType = mock({}) diff --git a/client/src/containers/traders/blocks/UnwatchEnvButton.tsx b/client/src/containers/traders/blocks/UnwatchEnvButton.tsx index 15cffae7..44392133 100644 --- a/client/src/containers/traders/blocks/UnwatchEnvButton.tsx +++ b/client/src/containers/traders/blocks/UnwatchEnvButton.tsx @@ -4,10 +4,10 @@ import * as localeTool from 'tools/locale' import * as routerTool from 'tools/router' import { Button } from 'flowbite-react' import ConfirmModal from 'containers/elements/ConfirmModal' -import WatchButton from '../elements/WatchButton' import { useDispatch } from 'react-redux' import { useNavigate } from 'react-router-dom' import { useState } from 'react' +import WatchButton from '../elements/WatchButton' const UnwatchEnvButton = ({ traderEnv, diff --git a/client/src/containers/traders/combos/ComboBuilder.test.tsx b/client/src/containers/traders/combos/ComboBuilder.test.tsx index d895a66d..72367716 100644 --- a/client/src/containers/traders/combos/ComboBuilder.test.tsx +++ b/client/src/containers/traders/combos/ComboBuilder.test.tsx @@ -2,10 +2,10 @@ import * as interfaces from '@shared/interfaces' import * as selectors from 'selectors' import { act, fireEvent, render, screen, waitFor } from 'test.utils' import { instance, mock } from 'ts-mockito' -import ComboBuilder from './ComboBuilder' import { GlobalState } from 'stores/global' import { UserState } from 'stores/user' import axios from 'axios' +import ComboBuilder from './ComboBuilder' jest.mock('selectors', () => { const actual = jest.requireActual('selectors') diff --git a/client/src/containers/traders/combos/ComboDetail.test.tsx b/client/src/containers/traders/combos/ComboDetail.test.tsx index e0bc6336..68e0b6a9 100644 --- a/client/src/containers/traders/combos/ComboDetail.test.tsx +++ b/client/src/containers/traders/combos/ComboDetail.test.tsx @@ -3,9 +3,9 @@ import * as router from 'react-router-dom' import * as selectors from 'selectors' import { act, fireEvent, render, screen } from 'test.utils' import { instance, mock } from 'ts-mockito' -import ComboDetail from './ComboDetail' import { GlobalState } from 'stores/global' import axios from 'axios' +import ComboDetail from './ComboDetail' afterEach(() => { jest.clearAllMocks() diff --git a/client/src/containers/traders/elements/BehaviorEditor.tsx b/client/src/containers/traders/elements/BehaviorEditor.tsx index ca1adb27..5586b005 100644 --- a/client/src/containers/traders/elements/BehaviorEditor.tsx +++ b/client/src/containers/traders/elements/BehaviorEditor.tsx @@ -2,9 +2,9 @@ import * as constants from '@shared/constants' import * as interfaces from '@shared/interfaces' import * as localeTool from 'tools/locale' import * as parseTool from 'tools/parse' -import BehaviorLabel from './BehaviorLabel' import { ChangeEvent } from 'react' import { Select } from 'flowbite-react' +import BehaviorLabel from './BehaviorLabel' const BehaviorEditor = ({ behavior, diff --git a/client/src/containers/traders/elements/HoldingShare.test.tsx b/client/src/containers/traders/elements/HoldingShare.test.tsx index 6dbecae5..bdc162d3 100644 --- a/client/src/containers/traders/elements/HoldingShare.test.tsx +++ b/client/src/containers/traders/elements/HoldingShare.test.tsx @@ -1,8 +1,8 @@ import * as interfaces from '@shared/interfaces' import * as localeTool from 'tools/locale' import { render, screen } from 'test.utils' -import HoldingShare from './HoldingShare' import { mock } from 'ts-mockito' +import HoldingShare from './HoldingShare' describe('#HoldingShare', () => { const tickerIdentity = { diff --git a/client/src/containers/traders/elements/PatternBehaviors.test.tsx b/client/src/containers/traders/elements/PatternBehaviors.test.tsx index 1fb30bfb..9e0d1b59 100644 --- a/client/src/containers/traders/elements/PatternBehaviors.test.tsx +++ b/client/src/containers/traders/elements/PatternBehaviors.test.tsx @@ -1,9 +1,9 @@ import * as interfaces from '@shared/interfaces' import * as routerTool from 'tools/router' import { fireEvent, render, screen } from 'test.utils' -import PatternBehaviors from './PatternBehaviors' import { createMemoryHistory } from 'history' import { mock } from 'ts-mockito' +import PatternBehaviors from './PatternBehaviors' describe('#PatternBehaviors', () => { const history = createMemoryHistory({ initialEntries: ['/test'] }) diff --git a/client/src/containers/traders/elements/PatternBehaviors.tsx b/client/src/containers/traders/elements/PatternBehaviors.tsx index 51a65f49..cea14850 100644 --- a/client/src/containers/traders/elements/PatternBehaviors.tsx +++ b/client/src/containers/traders/elements/PatternBehaviors.tsx @@ -1,8 +1,8 @@ import * as constants from '@shared/constants' import * as interfaces from '@shared/interfaces' import * as routerTool from 'tools/router' -import BehaviorLabel from './BehaviorLabel' import { useNavigate } from 'react-router-dom' +import BehaviorLabel from './BehaviorLabel' const labelClass = 'mx-2 my-1 w-auto' diff --git a/client/src/containers/traders/elements/ProfileLabel.test.tsx b/client/src/containers/traders/elements/ProfileLabel.test.tsx index 9b9b2484..89bad480 100644 --- a/client/src/containers/traders/elements/ProfileLabel.test.tsx +++ b/client/src/containers/traders/elements/ProfileLabel.test.tsx @@ -2,8 +2,8 @@ import * as interfaces from '@shared/interfaces' import * as localeTool from 'tools/locale' import * as parseTool from 'tools/parse' import { render, screen } from 'test.utils' -import ProfileLabel from './ProfileLabel' import { mock } from 'ts-mockito' +import ProfileLabel from './ProfileLabel' describe('#ProfileLabel', () => { const traderMock: interfaces.traderModel.Record = mock({}) diff --git a/client/src/containers/traders/elements/ProfileValue.test.tsx b/client/src/containers/traders/elements/ProfileValue.test.tsx index fc4b52ef..6ff3d54e 100644 --- a/client/src/containers/traders/elements/ProfileValue.test.tsx +++ b/client/src/containers/traders/elements/ProfileValue.test.tsx @@ -1,7 +1,7 @@ import * as interfaces from '@shared/interfaces' import { fireEvent, render, screen } from 'test.utils' -import ProfileValue from './ProfileValue' import { mock } from 'ts-mockito' +import ProfileValue from './ProfileValue' describe('#ProfileValue', () => { const traderMock: interfaces.traderModel.Record = mock({}) diff --git a/client/src/containers/traders/elements/ProfileValue.tsx b/client/src/containers/traders/elements/ProfileValue.tsx index 8e1475bc..4713d7b3 100644 --- a/client/src/containers/traders/elements/ProfileValue.tsx +++ b/client/src/containers/traders/elements/ProfileValue.tsx @@ -1,8 +1,8 @@ import * as constants from '@shared/constants' import * as interfaces from '@shared/interfaces' +import classNames from 'classnames' import ProfileLabel from './ProfileLabel' import ValueDiffer from './ValueDiffer' -import classNames from 'classnames' const ProfileValue = ({ trader, diff --git a/client/src/containers/traders/elements/TickerLabel.test.tsx b/client/src/containers/traders/elements/TickerLabel.test.tsx index 242a2898..d330988f 100644 --- a/client/src/containers/traders/elements/TickerLabel.test.tsx +++ b/client/src/containers/traders/elements/TickerLabel.test.tsx @@ -1,7 +1,7 @@ import * as interfaces from '@shared/interfaces' import { fireEvent, render, screen } from 'test.utils' -import TickerLabel from './TickerLabel' import { mock } from 'ts-mockito' +import TickerLabel from './TickerLabel' describe('#tickerLabel', () => { test('do not render if there is no ticker', () => { diff --git a/client/src/containers/traders/elements/ValueChangePanel.tsx b/client/src/containers/traders/elements/ValueChangePanel.tsx index c6123dd7..a3f670cb 100644 --- a/client/src/containers/traders/elements/ValueChangePanel.tsx +++ b/client/src/containers/traders/elements/ValueChangePanel.tsx @@ -1,6 +1,6 @@ +import classNames from 'classnames' import ValueChangeCharts from './ValueChangeCharts' import ValueChangePercents from './ValueChangePercents' -import classNames from 'classnames' const ValueChangePanel = ({ yearlyPercentNumber, diff --git a/client/src/containers/traders/envs/EnvBuilder.test.tsx b/client/src/containers/traders/envs/EnvBuilder.test.tsx index 612edc38..0038cd3d 100644 --- a/client/src/containers/traders/envs/EnvBuilder.test.tsx +++ b/client/src/containers/traders/envs/EnvBuilder.test.tsx @@ -1,8 +1,8 @@ import * as interfaces from '@shared/interfaces' import { act, fireEvent, render, screen, waitFor } from 'test.utils' import { instance, mock } from 'ts-mockito' -import EnvBuilder from './EnvBuilder' import axios from 'axios' +import EnvBuilder from './EnvBuilder' jest.mock('react-select', () => '') diff --git a/client/src/containers/traders/envs/EnvDetail.test.tsx b/client/src/containers/traders/envs/EnvDetail.test.tsx index f1d1187c..71fec39d 100644 --- a/client/src/containers/traders/envs/EnvDetail.test.tsx +++ b/client/src/containers/traders/envs/EnvDetail.test.tsx @@ -3,8 +3,8 @@ import * as router from 'react-router-dom' import * as selectors from 'selectors' import { act, render, screen } from 'test.utils' import { instance, mock } from 'ts-mockito' -import EnvDetail from './EnvDetail' import axios from 'axios' +import EnvDetail from './EnvDetail' afterEach(() => { jest.clearAllMocks() diff --git a/client/src/containers/traders/profiles/ProfileDashboard.test.tsx b/client/src/containers/traders/profiles/ProfileDashboard.test.tsx index 70d98c98..411250bf 100644 --- a/client/src/containers/traders/profiles/ProfileDashboard.test.tsx +++ b/client/src/containers/traders/profiles/ProfileDashboard.test.tsx @@ -3,8 +3,8 @@ import * as selectors from 'selectors' import { act, fireEvent, render, screen } from 'test.utils' import { instance, mock } from 'ts-mockito' import { GlobalState } from 'stores/global' -import ProfileDashboard from './ProfileDashboard' import { UserState } from 'stores/user' +import ProfileDashboard from './ProfileDashboard' afterEach(() => { jest.clearAllMocks() diff --git a/client/src/containers/traders/profiles/ProfileDetail.test.tsx b/client/src/containers/traders/profiles/ProfileDetail.test.tsx index cd97e6d9..ec8e22ef 100644 --- a/client/src/containers/traders/profiles/ProfileDetail.test.tsx +++ b/client/src/containers/traders/profiles/ProfileDetail.test.tsx @@ -5,8 +5,8 @@ import { UserAccess, UserState } from 'stores/user' import { act, fireEvent, render, screen } from 'test.utils' import { instance, mock } from 'ts-mockito' import { GlobalState } from 'stores/global' -import ProfileDetail from './ProfileDetail' import axios from 'axios' +import ProfileDetail from './ProfileDetail' afterEach(() => { jest.clearAllMocks() diff --git a/client/src/containers/traders/profiles/profile-builder/ProfileBuilder.test.tsx b/client/src/containers/traders/profiles/profile-builder/ProfileBuilder.test.tsx index 1e7920c7..80c02eaf 100644 --- a/client/src/containers/traders/profiles/profile-builder/ProfileBuilder.test.tsx +++ b/client/src/containers/traders/profiles/profile-builder/ProfileBuilder.test.tsx @@ -5,10 +5,10 @@ import * as selectors from 'selectors' import { UserAccess, UserState } from 'stores/user' import { act, fireEvent, render, screen } from 'test.utils' import { instance, mock } from 'ts-mockito' -import ProfileBuilder from './ProfileBuilder' import axios from 'axios' import { globalSlice } from 'stores/global' import { store } from 'stores' +import ProfileBuilder from './ProfileBuilder' store.dispatch(globalSlice.actions._updateForTest({ refreshToken: 'aaa', diff --git a/client/src/containers/traders/profiles/profile-builder/ProfileBuilder.tsx b/client/src/containers/traders/profiles/profile-builder/ProfileBuilder.tsx index c79702a5..23601bba 100644 --- a/client/src/containers/traders/profiles/profile-builder/ProfileBuilder.tsx +++ b/client/src/containers/traders/profiles/profile-builder/ProfileBuilder.tsx @@ -10,10 +10,10 @@ import { FormEvent, useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useLocation, useNavigate } from 'react-router-dom' import BehaviorEditor from 'containers/traders/elements/BehaviorEditor' -import ProfileBuilderGroup from './ProfileBuilderGroup' -import ProfileBuilderHeader from './ProfileBuilderHeader' import TraderEnvCard from 'containers/traders/blocks/TraderEnvCard' import usePrivateGuard from 'hooks/usePrivateGuard' +import ProfileBuilderGroup from './ProfileBuilderGroup' +import ProfileBuilderHeader from './ProfileBuilderHeader' type ActiveBehavior = interfaces.traderPatternModel.Behavior | null diff --git a/client/src/hooks/usePrivateGuard.test.ts b/client/src/hooks/usePrivateGuard.test.ts index 941be1fe..a5df5e58 100644 --- a/client/src/hooks/usePrivateGuard.test.ts +++ b/client/src/hooks/usePrivateGuard.test.ts @@ -3,8 +3,8 @@ import { createMemoryHistory } from 'history' import { globalSlice } from 'stores/global' import { renderHook } from 'test.utils' import { store } from 'stores' -import usePrivateGuard from './usePrivateGuard' import { userSlice } from 'stores/user' +import usePrivateGuard from './usePrivateGuard' afterEach(() => { jest.clearAllMocks() diff --git a/client/src/hooks/usePublicGuard.test.ts b/client/src/hooks/usePublicGuard.test.ts index 5e8ed3e3..ef32e96a 100644 --- a/client/src/hooks/usePublicGuard.test.ts +++ b/client/src/hooks/usePublicGuard.test.ts @@ -3,8 +3,8 @@ import { createMemoryHistory } from 'history' import { globalSlice } from 'stores/global' import { renderHook } from 'test.utils' import { store } from 'stores' -import usePublicGuard from './usePublicGuard' import { userSlice } from 'stores/user' +import usePublicGuard from './usePublicGuard' afterEach(() => { jest.clearAllMocks() diff --git a/client/src/index.tsx b/client/src/index.tsx index e25ebf11..cb57d5ef 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -1,11 +1,11 @@ import './index.css' import 'react-datepicker/dist/react-datepicker.css' import { BrowserRouter } from 'react-router-dom' -import Client from './containers/Client' import { Provider } from 'react-redux' import ReactDOM from 'react-dom/client' import { store } from 'stores' +import Client from './containers/Client' const root = ReactDOM.createRoot(document.getElementById('root')!) root.render( diff --git a/client/src/selectors/general.test.ts b/client/src/selectors/general.test.ts index 3bf62f99..52b8c867 100644 --- a/client/src/selectors/general.test.ts +++ b/client/src/selectors/general.test.ts @@ -1,7 +1,7 @@ -import * as general from './general' import * as interfaces from '@shared/interfaces' import { instance, mock, when } from 'ts-mockito' import { GlobalState } from 'stores/global' +import * as general from './general' const type = mock({}) const policyType = mock({}) diff --git a/client/src/selectors/tickerIdentity.test.ts b/client/src/selectors/tickerIdentity.test.ts index ab071f00..5bae4be1 100644 --- a/client/src/selectors/tickerIdentity.test.ts +++ b/client/src/selectors/tickerIdentity.test.ts @@ -1,6 +1,6 @@ import * as interfaces from '@shared/interfaces' -import * as tickerIdentity from './tickerIdentity' import { instance, mock, when } from 'ts-mockito' +import * as tickerIdentity from './tickerIdentity' const type = mock({}) diff --git a/client/src/selectors/traderBehavior.test.ts b/client/src/selectors/traderBehavior.test.ts index 7cd2bfc7..8675cbab 100644 --- a/client/src/selectors/traderBehavior.test.ts +++ b/client/src/selectors/traderBehavior.test.ts @@ -1,5 +1,5 @@ -import * as traderBehavior from './traderBehavior' import { instance, mock, when } from 'ts-mockito' +import * as traderBehavior from './traderBehavior' const type = mock({}) diff --git a/client/src/selectors/traderCombo.test.ts b/client/src/selectors/traderCombo.test.ts index fe6eaa30..6b00a0be 100644 --- a/client/src/selectors/traderCombo.test.ts +++ b/client/src/selectors/traderCombo.test.ts @@ -1,6 +1,6 @@ import * as interfaces from '@shared/interfaces' -import * as traderCombo from './traderCombo' import { instance, mock, when } from 'ts-mockito' +import * as traderCombo from './traderCombo' const type = mock({}) diff --git a/client/src/selectors/traderEnv.test.ts b/client/src/selectors/traderEnv.test.ts index 4533056f..b4066d16 100644 --- a/client/src/selectors/traderEnv.test.ts +++ b/client/src/selectors/traderEnv.test.ts @@ -1,6 +1,6 @@ import * as interfaces from '@shared/interfaces' -import * as traderEnv from './traderEnv' import { instance, mock, when } from 'ts-mockito' +import * as traderEnv from './traderEnv' const type = mock({}) diff --git a/client/src/selectors/traderProfile.test.ts b/client/src/selectors/traderProfile.test.ts index 8becbec0..848d139e 100644 --- a/client/src/selectors/traderProfile.test.ts +++ b/client/src/selectors/traderProfile.test.ts @@ -1,6 +1,6 @@ import * as interfaces from '@shared/interfaces' -import * as traderProfile from './traderProfile' import { instance, mock, when } from 'ts-mockito' +import * as traderProfile from './traderProfile' const type = mock({}) diff --git a/client/src/selectors/user.test.ts b/client/src/selectors/user.test.ts index 38b5ae83..0d77f4c1 100644 --- a/client/src/selectors/user.test.ts +++ b/client/src/selectors/user.test.ts @@ -1,7 +1,7 @@ import * as interfaces from '@shared/interfaces' -import * as userSelector from './user' import { instance, mock, when } from 'ts-mockito' import { UserState } from 'stores/user' +import * as userSelector from './user' const type = mock({}) const userType = mock({}) diff --git a/client/src/stores/tickerIdentity.test.ts b/client/src/stores/tickerIdentity.test.ts index eba225f9..5fb5efc2 100644 --- a/client/src/stores/tickerIdentity.test.ts +++ b/client/src/stores/tickerIdentity.test.ts @@ -1,5 +1,5 @@ -import { store } from './index' import { tickerIdentitySlice } from './tickerIdentity' +import { store } from './index' afterEach(() => { store.dispatch(tickerIdentitySlice.actions._resetForTest()) diff --git a/client/src/stores/traderBehavior.test.ts b/client/src/stores/traderBehavior.test.ts index 353246e5..fcab5c35 100644 --- a/client/src/stores/traderBehavior.test.ts +++ b/client/src/stores/traderBehavior.test.ts @@ -1,7 +1,7 @@ import * as actions from 'actions' import axios from 'axios' -import { store } from './index' import { traderBehaviorSlice } from './traderBehavior' +import { store } from './index' afterEach(() => { store.dispatch(traderBehaviorSlice.actions._resetForTest()) diff --git a/client/src/stores/traderCombo.test.ts b/client/src/stores/traderCombo.test.ts index 257424ae..cfc39a81 100644 --- a/client/src/stores/traderCombo.test.ts +++ b/client/src/stores/traderCombo.test.ts @@ -2,8 +2,8 @@ import * as actions from 'actions' import * as interfaces from '@shared/interfaces' import { instance, mock } from 'ts-mockito' import axios from 'axios' -import { store } from './index' import { traderComboSlice } from './traderCombo' +import { store } from './index' afterEach(() => { store.dispatch(traderComboSlice.actions._resetForTest()) diff --git a/client/src/stores/traderEnv.test.ts b/client/src/stores/traderEnv.test.ts index 8f9ac4e4..aaa430f5 100644 --- a/client/src/stores/traderEnv.test.ts +++ b/client/src/stores/traderEnv.test.ts @@ -2,8 +2,8 @@ import * as actions from 'actions' import * as interfaces from '@shared/interfaces' import { instance, mock } from 'ts-mockito' import axios from 'axios' -import { store } from './index' import { traderEnvSlice } from './traderEnv' +import { store } from './index' afterEach(() => { store.dispatch(traderEnvSlice.actions._resetForTest()) diff --git a/client/src/stores/traderProfile.test.ts b/client/src/stores/traderProfile.test.ts index 78d590eb..09be22e3 100644 --- a/client/src/stores/traderProfile.test.ts +++ b/client/src/stores/traderProfile.test.ts @@ -2,8 +2,8 @@ import * as actions from 'actions' import * as interfaces from '@shared/interfaces' import { instance, mock } from 'ts-mockito' import axios from 'axios' -import { store } from './index' import { traderProfileSlice } from './traderProfile' +import { store } from './index' afterEach(() => { store.dispatch(traderProfileSlice.actions._resetForTest()) diff --git a/client/src/stores/user.test.ts b/client/src/stores/user.test.ts index f5218fd7..43d82322 100644 --- a/client/src/stores/user.test.ts +++ b/client/src/stores/user.test.ts @@ -3,8 +3,8 @@ import * as constants from '@shared/constants' import * as interfaces from '@shared/interfaces' import { instance, mock } from 'ts-mockito' import axios from 'axios' -import { store } from './index' import { userSlice } from './user' +import { store } from './index' afterEach(() => { store.dispatch(userSlice.actions._resetForTest()) diff --git a/client/src/tools/parse.test.ts b/client/src/tools/parse.test.ts deleted file mode 100644 index 83b7e882..00000000 --- a/client/src/tools/parse.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import * as localeTool from './locale' -import * as parseTool from './parse' - -describe('#dbPercentNumber', () => { - test('could parse db percent number to percent', () => { - expect(parseTool.dbPercentNumber(null)).toBe('') - expect(parseTool.dbPercentNumber(10000)).toBe('100%') - expect(parseTool.dbPercentNumber(5000)).toBe('50%') - expect(parseTool.dbPercentNumber(0)).toBe('0%') - }) -}) - -describe('#dbPercent', () => { - test('could parse db percent to percent', () => { - expect(parseTool.dbPercent(null)).toBe('') - expect(parseTool.dbPercent(100)).toBe('100%') - expect(parseTool.dbPercent(50)).toBe('50%') - expect(parseTool.dbPercent(0)).toBe('0%') - }) -}) - -describe('#behaviorValue', () => { - test('could parse behavior value', () => { - expect(parseTool.behaviorValue('priceDailyIncreaseBuy', null)).toBe('') - expect(parseTool.behaviorValue('tickerMinPercent', 0)).toBe('0%') - expect(parseTool.behaviorValue('tickerMaxPercent', 10)).toBe('10%') - expect(parseTool.behaviorValue('tickerMaxPercent', 50)).toBe('50%') - expect(parseTool.behaviorValue('tradeFrequency', null)).toBe('') - expect(parseTool.behaviorValue('tradeFrequency', 0)).toBe(localeTool.t('behavior.frequency.never')) - expect(parseTool.behaviorValue('rebalanceFrequency', 10)).toBe(localeTool.t('behavior.frequency.type', { num: 10 })) - expect(parseTool.behaviorValue('rebalanceFrequency', 50)).toBe(localeTool.t('behavior.frequency.type', { num: 50 })) - expect(parseTool.behaviorValue('buyPreference', null)).toBe('') - expect(parseTool.behaviorValue('buyPreference', 0)).toBe('') - expect(parseTool.behaviorValue('buyPreference', 1)).toBe(localeTool.t('behavior.preference.type.1')) - expect(parseTool.behaviorValue('sellPreference', 22)).toBe(localeTool.t('behavior.preference.type.22')) - expect(parseTool.behaviorValue('sellPreference', 23)).toBe('') - expect(parseTool.behaviorValue('priceYearlyDecreaseSell', null)).toBe('') - expect(parseTool.behaviorValue('priceYearlyDecreaseSell', 1)).toBe(1) - expect(parseTool.behaviorValue('priceYearlyDecreaseSell', 10)).toBe(10) - }) -}) - -describe('#behaviorTitle', () => { - test('could parse behavior title', () => { - expect(parseTool.behaviorTitle('tickerMaxPercent')).toBe(localeTool.t('behaviorTitle.tickerMaxPercent')) - expect(parseTool.behaviorTitle('tradeFrequency')).toBe(localeTool.t('behaviorTitle.tradeFrequency')) - expect(parseTool.behaviorTitle('buyPreference')).toBe(localeTool.t('behaviorTitle.buyPreference')) - expect(parseTool.behaviorTitle('priceYearlyDecreaseSell')) - .toBe(localeTool.t('behaviorTitle.priceYearlyDecreaseSell')) - }) -}) - -describe('#behaviorDesc', () => { - test('could parse behavior desc', () => { - expect(parseTool.behaviorDesc('tickerMaxPercent')).toBe(localeTool.t('behaviorDesc.tickerMaxPercent')) - expect(parseTool.behaviorDesc('tradeFrequency')).toBe(localeTool.t('behaviorDesc.tradeFrequency')) - expect(parseTool.behaviorDesc('buyPreference')).toBe(localeTool.t('behaviorDesc.buyPreference')) - expect(parseTool.behaviorDesc('priceYearlyDecreaseSell')).toBe(localeTool.t('behaviorDesc.priceYearlyDecreaseSell')) - }) -}) - -describe('#traderEnvStartDate', () => { - test('could parse trader env start date', () => { - expect(parseTool.traderEnvStartDate({ - id: 1, - entityId: 1, - activeTotal: 100, - startDate: '2000-01-01', - tickerIds: null, - })).toBe(localeTool.t('traderEnv.startAt', { date: '2000-01-01' })) - expect(parseTool.traderEnvStartDate({ - id: 1, - entityId: 1, - activeTotal: 100, - startDate: '2002-02-02', - tickerIds: null, - })).toBe(localeTool.t('traderEnv.startAt', { date: '2002-02-02' })) - }) -}) - -describe('#traderEnvTickers', () => { - test('could parse trader env start date', () => { - expect(parseTool.traderEnvTickers({ - id: 1, - entityId: 1, - activeTotal: 100, - startDate: '2000-01-01', - tickerIds: null, - })).toBe(localeTool.t('traderEnv.allTickers')) - expect(parseTool.traderEnvTickers({ - id: 1, - entityId: 1, - activeTotal: 100, - startDate: '2002-02-02', - tickerIds: [111], - })).toBe(localeTool.t('traderEnv.selectedTickers', { num: 1 })) - expect(parseTool.traderEnvTickers({ - id: 1, - entityId: 1, - activeTotal: 100, - startDate: '2002-02-02', - tickerIds: [1, 2, 3, 4, 5], - })).toBe(localeTool.t('traderEnv.selectedTickers', { num: 5 })) - }) -}) - -describe('#profileName', () => { - test('could parse profile name', () => { - expect(parseTool.profileName(1)).toBe(`${localeTool.t('entity.profile')} #1`) - expect(parseTool.profileName(2)).toBe(`${localeTool.t('entity.profile')} #2`) - }) -}) - -describe('#traderComboTraders', () => { - test('could parse trader combo selected traders', () => { - expect(parseTool.traderComboTraders({ - id: 1, entityId: 1, name: 'systemCombo.-1', traderIds: [], - })).toBe(localeTool.t('traderCombo.selectedTraders', { num: 0 })) - expect(parseTool.traderComboTraders({ - id: 1, entityId: 1, name: 'systemCombo.-1', traderIds: [1, 2], - })).toBe(localeTool.t('traderCombo.selectedTraders', { num: 2 })) - }) -}) - -describe('#holdingValue', () => { - test('could parse holding value', () => { - expect(parseTool.holdingValue(null)).toBe(null) - expect(parseTool.holdingValue(100)).toBe('$1.00') - expect(parseTool.holdingValue(1010)).toBe('$10.10') - expect(parseTool.holdingValue(1111)).toBe('$11.11') - expect(parseTool.holdingValue(1000000)).toBe('$10,000.00') - expect(parseTool.holdingValue(100000000)).toBe('$1,000,000.00') - // @ts-ignore - Intl = null // eslint-disable-line no-global-assign - expect(parseTool.holdingValue(100000000)).toBe('1000000.00') - }) -}) - -describe('#floatToPercent', () => { - test('could parse float to percent', () => { - expect(parseTool.floatToPercent(0.11)).toBe('11.00%') - expect(parseTool.floatToPercent(0.1122)).toBe('11.22%') - expect(parseTool.floatToPercent(0.112233)).toBe('11.22%') - expect(parseTool.floatToPercent(0.112253)).toBe('11.23%') - }) -}) - -describe('#chartTrends', () => { - test('could parse chart trends', () => { - expect(parseTool.chartTrends([1, 2, 3], null)).toStrictEqual([]) - expect(parseTool.chartTrends(null, 100)).toStrictEqual([]) - expect(parseTool.chartTrends([], 100)).toStrictEqual([]) - expect(parseTool.chartTrends([1, 2, 3], 4)).toStrictEqual([ - { label: '1', value: 1 }, { label: '2', value: 2 }, { label: '3', value: 3 }, { label: '4', value: 4 }, - ]) - expect(parseTool.chartTrends([3, 2, 1], 4)).toStrictEqual([ - { label: '1', value: 3 }, { label: '2', value: 2 }, { label: '3', value: 1 }, { label: '4', value: 4 }, - ]) - }) -}) diff --git a/constants/src/behaviorValue.test.ts b/constants/src/behaviorValue.test.ts deleted file mode 100644 index 2307c713..00000000 --- a/constants/src/behaviorValue.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as behavior from './behavior' -import * as behaviorValue from './behaviorValue' - -describe('#Preference', () => { - test('defined preference correctly', () => { - expect(Object.values(behaviorValue.Preference).length).toBe(22) - Object.keys(behaviorValue.Preference).forEach((key) => { - expect(key.includes('Higher') || key.includes('Lower')).toBeTruthy() - }) - }) -}) - -describe('#Options', () => { - test('defined options correctly', () => { - expect(Object.values(behaviorValue.Options).length).toBe(behavior.Behaviors.length) - Object.values(behaviorValue.Options).forEach((value) => { - expect(Array.isArray(value)).toBeTruthy() - }) - }) -}) diff --git a/package-lock.json b/package-lock.json index 40b2cb75..9917b4c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,12 @@ { - "name": "melody-app", + "name": "melody-invest", "version": "0.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "melody-app", + "name": "melody-invest", "version": "0.0.2", - "license": "UNLICENSED", "workspaces": [ "server", "client", diff --git a/server/adapters/database.test.ts b/server/adapters/database.test.ts deleted file mode 100644 index 60f0a09b..00000000 --- a/server/adapters/database.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import * as adapterEnum from 'enums/adapter' -import * as database from './database' -import * as errorEnum from 'enums/error' -import * as ticker from 'models/ticker' -import * as traderEnv from 'models/traderEnv' - -beforeEach(async () => { - database.initConnection() - const connection = database.getConnection() - await connection.migrate.up({ - directory: './server/migrations/test-tables', - name: 'entity.js', - }) - await connection.seed.run({ - directory: './server/migrations/test-seeds', - specific: 'entity.js', - }) - await connection.migrate.up({ - directory: './server/migrations/test-tables', - name: 'trader_env.js', - }) - await connection.seed.run({ - directory: './server/migrations/test-seeds', - specific: 'trader_env.js', - }) - await connection.migrate.up({ - directory: './server/migrations/test-tables', - name: 'ticker.js', - }) -}) - -afterEach(async () => { - const connection = database.getConnection() - await connection.destroy() -}) - -describe('#findAll', () => { - test('could return empty array', async () => { - const tickers = await ticker.getAll() - expect(tickers).toStrictEqual([]) - }) -}) - -describe('#findOne', () => { - test('could return null', async () => { - const result = await ticker.getByUK(1, 'US', 'AAPL') - expect(result).toBeNull() - }) -}) - -describe('#update', () => { - test('could return empty array', async () => { - const transaction = await database.createTransaction() - const tickers = await ticker.update(1, { firstPriceDate: '2000-01-01' }, transaction) - await transaction.commit() - expect(tickers).toBeUndefined() - }) -}) - -describe('#create', () => { - test('could throw error', async () => { - await expect(async () => { - const transaction = await database.createTransaction() - await database.create({ - tableName: adapterEnum.DatabaseTable.TraderEnv, - values: { - id: 1, - activeTotal: 0, - startDate: '2000-01-01', - }, - transaction, - }) - await transaction.commit() - }) - .rejects - .toStrictEqual(errorEnum.Custom.CreationFailed) - }) -}) - -describe('#update', () => { - test('do not throw error if nothing updated', async () => { - await expect(async () => { - const transaction = await database.createTransaction() - await database.update({ - tableName: adapterEnum.DatabaseTable.TraderEnv, - values: { activeTotal: 10000 }, - conditions: [ - { key: 'id', value: 111 }, - ], - transaction, - }) - await transaction.commit() - }) - .not - .toThrowError() - }) - - test('could throw error', async () => { - await expect(async () => { - const transaction = await database.createTransaction() - await database.update({ - tableName: adapterEnum.DatabaseTable.TraderEnv, - values: { activeTotal: 111 }, - conditions: [ - { key: 'id', value: 'abc' }, - ], - transaction, - }) - await transaction.commit() - }) - .rejects - .toStrictEqual(errorEnum.Custom.UpdationFailed) - }) -}) - -describe('#runWithTransaction', () => { - test('could run with transaction', async () => { - const records = await database.runWithTransaction(async (transaction) => { - return database.update({ - tableName: adapterEnum.DatabaseTable.TraderEnv, - values: { activeTotal: 100 }, - conditions: [ - { key: 'id', value: 1 }, - ], - transaction, - }) - }) - - const updatedEnvs = await traderEnv.getAll() - expect(updatedEnvs[0].activeTotal).toBe(100) - expect(records[0].activeTotal).toBe(100) - }) - - test('could trigger rollback', async () => { - await expect(async () => { - await database.runWithTransaction(async (transaction) => { - return database.update({ - tableName: adapterEnum.DatabaseTable.TraderEnv, - values: { activeTotal: 'wrong format that causing error' }, - conditions: [ - { key: 'id', value: 1 }, - ], - transaction, - }) - }) - }) - .rejects - .toStrictEqual(errorEnum.Custom.UpdationFailed) - - const updatedEnvs = await traderEnv.getAll() - expect(updatedEnvs[0].activeTotal).toBe(10000) - }) -}) diff --git a/server/cron.test.ts b/server/cron.test.ts index 9313e49f..e9340fbb 100644 --- a/server/cron.test.ts +++ b/server/cron.test.ts @@ -1,6 +1,6 @@ import * as cacheTask from 'tasks/cache' -import * as cron from './cron' import * as emailTask from 'tasks/email' +import * as cron from './cron' jest.mock('node-cron', () => ({ schedule: jest.fn(), diff --git a/server/index.ts b/server/index.ts index 763a384f..e0d3df39 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,18 +1,18 @@ import 'express-async-errors' +import http, { Server as HttpServer } from 'http' +import https, { Server as HttpsServer } from 'https' +import fs from 'fs' +import path from 'path' import * as adapterEnum from 'enums/adapter' import * as errorEnum from 'enums/error' import express, { NextFunction, Request, Response, Router } from 'express' -import http, { Server as HttpServer } from 'http' -import https, { Server as HttpsServer } from 'https' import { attachRoutes as attachSystemRoutes } from 'routers/system' import { attachRoutes as attachTradersRoutes } from 'routers/traders' import { attachRoutes as attachUsersRoutes } from 'routers/users' import compression from 'compression' import cors from 'cors' -import fs from 'fs' import { initConnection as initCache } from 'adapters/cache' import { initConnection as initDatabase } from 'adapters/database' -import path from 'path' const app = express() export default app diff --git a/server/logics/email.test.ts b/server/logics/email.test.ts index 3a049ea5..9542e555 100644 --- a/server/logics/email.test.ts +++ b/server/logics/email.test.ts @@ -1,8 +1,8 @@ -import * as email from './email' import * as interfaces from '@shared/interfaces' import * as localeTool from 'tools/locale' import { mock } from 'ts-mockito' +import * as email from './email' const userMock: interfaces.userModel.Record = mock({}) const user = { diff --git a/server/logics/holding.test.ts b/server/logics/holding.test.ts index 7b5755d0..d61aee7e 100644 --- a/server/logics/holding.test.ts +++ b/server/logics/holding.test.ts @@ -1,7 +1,7 @@ -import * as holding from './holding' import * as interfaces from '@shared/interfaces' import { instance, mock } from 'ts-mockito' +import * as holding from './holding' describe('#groupHoldingsByTraders', () => { const holdingMock: interfaces.traderHoldingModel.Record = mock({}) diff --git a/server/logics/price.test.ts b/server/logics/price.test.ts index 63e64266..e3544d87 100644 --- a/server/logics/price.test.ts +++ b/server/logics/price.test.ts @@ -1,6 +1,6 @@ import * as interfaces from '@shared/interfaces' -import * as price from './price' import { mock } from 'ts-mockito' +import * as price from './price' describe('#getSplitMultiplier', () => { test('could get correct value', () => { diff --git a/server/logics/trader.test.ts b/server/logics/trader.test.ts index 5c320d1e..8e27bd4a 100644 --- a/server/logics/trader.test.ts +++ b/server/logics/trader.test.ts @@ -1,6 +1,6 @@ import * as interfaces from '@shared/interfaces' -import * as trader from './trader' import { instance, mock } from 'ts-mockito' +import * as trader from './trader' const traderMock: interfaces.traderModel.Record = mock({}) const traderInstance = instance(traderMock) diff --git a/server/logics/transaction.ts b/server/logics/transaction.ts index 4a77a7c1..9101259b 100644 --- a/server/logics/transaction.ts +++ b/server/logics/transaction.ts @@ -338,7 +338,7 @@ export const getHoldingDetailAfterBuy = ( // Find existing holding item or initial an empty one const item = details.items.find((item) => item.tickerId === tickerId) || { - tickerId, shares: 0, splitMultiplier: 0, value: 0 + tickerId, shares: 0, splitMultiplier: 0, value: 0, } const refreshedDetails = buyItemToHolding( diff --git a/server/middlewares/access.test.ts b/server/middlewares/access.test.ts index bcc9c645..6f7c1c72 100644 --- a/server/middlewares/access.test.ts +++ b/server/middlewares/access.test.ts @@ -1,4 +1,3 @@ -import * as access from './access' import * as constants from '@shared/constants' import * as errorEnum from 'enums/error' import * as interfaces from '@shared/interfaces' @@ -8,6 +7,7 @@ import * as traderEnvModel from 'models/traderEnv' import * as traderFollowerModel from 'models/traderFollower' import { Request, Response } from 'express' import { instance, mock, when } from 'ts-mockito' +import * as access from './access' jest.mock('models/traderEnv', () => { const actual = jest.requireActual('models/traderEnv') diff --git a/server/middlewares/auth.test.ts b/server/middlewares/auth.test.ts index c215a151..5d7bbb9c 100644 --- a/server/middlewares/auth.test.ts +++ b/server/middlewares/auth.test.ts @@ -1,7 +1,7 @@ -import * as auth from './auth' import * as generateTool from 'tools/generate' import { Request, Response } from 'express' import { instance, mock, when } from 'ts-mockito' +import * as auth from './auth' const resMock: Response = mock({}) const res = instance(resMock) diff --git a/server/models/dailyTickers.test.ts b/server/models/dailyTickers.test.ts deleted file mode 100644 index 1700ea2d..00000000 --- a/server/models/dailyTickers.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import * as dailyTickers from './dailyTickers' -import * as databaseAdapter from 'adapters/database' - -beforeAll(async () => { - databaseAdapter.initConnection() - const connection = databaseAdapter.getConnection() - await connection.migrate.up({ - directory: './server/migrations/test-tables', - name: 'entity.js', - }) - await connection.seed.run({ - directory: './server/migrations/test-seeds', - specific: 'entity.js', - }) - await connection.migrate.up({ - directory: './server/migrations/test-tables', - name: 'daily_tickers.js', - }) - await connection.seed.run({ - directory: './server/migrations/test-seeds', - specific: 'daily_tickers.js', - }) -}) - -afterAll(async () => { - const connection = databaseAdapter.getConnection() - await connection.destroy() -}) - -describe('#getLatestDate', () => { - test('could get latest date', async () => { - const date = await dailyTickers.getLatestDate() - expect(date).toEqual('2022-01-01') - }) -}) - -describe('#getByUK', () => { - test('could get by UK', async () => { - const record1 = await dailyTickers.getByUK(1, '2021-12-31') - expect(record1?.id).toBe(undefined) - const record2 = await dailyTickers.getByUK(1, '2021-12-30') - expect(record2).toBeNull() - - const record3 = await dailyTickers.getByUK(1, '2022-01-01') - expect(record3?.id).toBe(1) - expect(record3?.date).toBe('2022-01-01') - - const record4 = await dailyTickers.getByUK(1, '2022-01-02') - expect(record4).toBeNull() - }) -}) - -describe('#create', () => { - test('could create', async () => { - const transaction = await databaseAdapter.createTransaction() - const created = await dailyTickers.create({ - entityId: 2, - date: '2022-01-02', - tickerInfos: {}, - priceInfo: {}, - }, transaction) - await transaction.commit() - expect(created.id).toBe(2) - expect(created.entityId).toBe(2) - expect(created.date).toBe('2022-01-02') - - const record = await dailyTickers.getByUK(2, '2022-01-02') - expect(record?.id).toBe(2) - expect(record?.entityId).toBe(2) - expect(record?.date).toBe('2022-01-02') - }) -}) - -describe('#destroyAll', () => { - test('could destroy all', async () => { - const transaction = await databaseAdapter.createTransaction() - await dailyTickers.destroyAll(transaction) - await transaction.commit() - const record = await dailyTickers.getByUK(2, '2021-12-31') - expect(record).toBeNull() - const date = await dailyTickers.getLatestDate() - expect(date).toBe('2001-01-01') - }) -}) diff --git a/server/models/traderEnv.test.ts b/server/models/traderEnv.test.ts deleted file mode 100644 index d1997a18..00000000 --- a/server/models/traderEnv.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as databaseAdapter from 'adapters/database' -import * as traderEnv from './traderEnv' - -beforeAll(async () => { - databaseAdapter.initConnection() - const connection = databaseAdapter.getConnection() - await connection.migrate.up({ - directory: './server/migrations/test-tables', - name: 'entity.js', - }) - await connection.seed.run({ - directory: './server/migrations/test-seeds', - specific: 'entity.js', - }) - await connection.migrate.up({ - directory: './server/migrations/test-tables', - name: 'trader_env.js', - }) - await connection.seed.run({ - directory: './server/migrations/test-seeds', - specific: 'trader_env.js', - }) -}) - -afterAll(async () => { - const connection = databaseAdapter.getConnection() - await connection.destroy() -}) - -describe('#getAll', () => { - test('could get all', async () => { - const envs = await traderEnv.getAll() - expect(envs.length).toBe(3) - }) -}) - -describe('#getByPK', () => { - test('could get by PK', async () => { - const env1 = await traderEnv.getByPK(1) - expect(env1?.tickerIds).toBe(null) - expect(env1?.startDate).toBe('2001-01-01') - - const env2 = await traderEnv.getByPK(2) - expect(env2?.tickerIds).toBe(null) - - const env3 = await traderEnv.getByPK(3) - expect(env3?.tickerIds).toStrictEqual([1, 2]) - - const env4 = await traderEnv.getByPK(4) - expect(env4).toBe(null) - }) -}) - -describe('#getByUK', () => { - test('could get by UK', async () => { - const env1 = await traderEnv.getByUK(1, '2001-01-01', null) - expect(env1?.id).toBe(1) - - const env2 = await traderEnv.getByUK(1, '2015-06-01', '1,2') - expect(env2?.id).toBe(3) - - const env3 = await traderEnv.getByUK(1, '2001-01-01', '1,2') - expect(env3).toBe(null) - }) -}) - -describe('#getInPKs', () => { - test('could get by PKs', async () => { - const envs = await traderEnv.getInPKs([2, 3]) - expect(envs.length).toBe(2) - expect(envs[0].id).toBe(2) - expect(envs[1].id).toBe(3) - }) -}) - -describe('#create', () => { - test('could create', async () => { - const transaction = await databaseAdapter.createTransaction() - const created = await traderEnv.create({ - entityId: 3, - activeTotal: 100, - startDate: '2022-01-01', - tickerIds: '1, 2, 3', - }, transaction) - await transaction.commit() - const expected = { - id: 4, - entityId: 3, - activeTotal: 100, - startDate: '2022-01-01', - tickerIds: [1, 2, 3], - } - expect(created).toStrictEqual(expected) - const record = await traderEnv.getByPK(4) - expect(record).toStrictEqual(expected) - }) -}) - -describe('#createIfEmpty', () => { - test('could return existing one', async () => { - const transaction = await databaseAdapter.createTransaction() - const created = await traderEnv.createIfEmpty({ - entityId: 3, - activeTotal: 100, - startDate: '2022-01-01', - tickerIds: '1, 2, 3', - }, transaction) - await transaction.rollback() - const expected = { - id: 4, - entityId: 3, - activeTotal: 100, - startDate: '2022-01-01', - tickerIds: [1, 2, 3], - } - expect(created).toStrictEqual({ - record: expected, - isNew: false, - }) - }) - - test('could create new', async () => { - const transaction = await databaseAdapter.createTransaction() - const created = await traderEnv.createIfEmpty({ - entityId: 2, - activeTotal: 100, - startDate: '2023-01-01', - tickerIds: '1, 2', - }, transaction) - await transaction.commit() - const expected = { - id: 5, - entityId: 2, - activeTotal: 100, - startDate: '2023-01-01', - tickerIds: [1, 2], - } - expect(created).toStrictEqual({ - record: expected, - isNew: true, - }) - }) -}) diff --git a/server/routers/system.test.ts b/server/routers/system.test.ts index 83896c37..830cc577 100644 --- a/server/routers/system.test.ts +++ b/server/routers/system.test.ts @@ -2,9 +2,9 @@ import * as constants from '@shared/constants' import * as crudSystems from 'services/crudSystems' import * as errorEnum from 'enums/error' import * as interfaces from '@shared/interfaces' -import * as system from './system' import { Request, Response, Router } from 'express' import { instance, mock } from 'ts-mockito' +import * as system from './system' jest.mock('services/crudSystems', () => ({ ...jest.requireActual('services/crudSystems'), diff --git a/server/routers/traders.test.ts b/server/routers/traders.test.ts index c22eb506..00b6de88 100644 --- a/server/routers/traders.test.ts +++ b/server/routers/traders.test.ts @@ -3,9 +3,9 @@ import * as authMiddleware from 'middlewares/auth' import * as crudTraders from 'services/crudTraders' import * as errorEnum from 'enums/error' import * as interfaces from '@shared/interfaces' -import * as traders from './traders' import { Request, Response, Router } from 'express' import { instance, mock } from 'ts-mockito' +import * as traders from './traders' jest.mock('services/crudTraders', () => ({ ...jest.requireActual('services/crudTraders'), diff --git a/server/routers/users.test.ts b/server/routers/users.test.ts index 77a89d30..d196decc 100644 --- a/server/routers/users.test.ts +++ b/server/routers/users.test.ts @@ -2,9 +2,9 @@ import * as authMiddleware from 'middlewares/auth' import * as crudUsers from 'services/crudUsers' import * as errorEnum from 'enums/error' import * as interfaces from '@shared/interfaces' -import * as users from './users' import { Request, Response, Router } from 'express' import { instance, mock } from 'ts-mockito' +import * as users from './users' jest.mock('services/crudUsers', () => ({ ...jest.requireActual('services/crudUsers'), diff --git a/server/services/calcTickers.ts b/server/services/calcTickers.ts index 03774139..219cece9 100644 --- a/server/services/calcTickers.ts +++ b/server/services/calcTickers.ts @@ -691,7 +691,7 @@ export const groupTickerInfo = ( tickerQuarterly: interfaces.tickerQuarterlyModel.Record | null, tickerYearly: interfaces.tickerYearlyModel.Record | null, ): interfaces.dailyTickersModel.TickerInfo => { - const info: interfaces.dailyTickersModel.TickerInfo = {} + const info = {} as interfaces.dailyTickersModel.TickerInfo constants.Ticker.DailyMovementKeys.forEach((key) => { if (tickerDaily[key] !== undefined && tickerDaily[key] !== null) { @@ -712,7 +712,12 @@ export const groupTickerInfo = ( }) constants.Ticker.YearlyCompareKeys.forEach((key) => { + if (key === 'annualPeRatio') info[key] = tickerYearly?.peRatio + if (key === 'annualPbRatio') info[key] = tickerYearly?.pbRatio + if (key === 'annualPsRatio') info[key] = tickerYearly?.psRatio + // @ts-ignore if (tickerYearly?.[key] !== undefined && tickerYearly?.[key] !== null) { + // @ts-ignore info[key] = tickerYearly[key] } }) diff --git a/server/services/crudSystems.test.ts b/server/services/crudSystems.test.ts index 4f5cec84..3261ad21 100644 --- a/server/services/crudSystems.test.ts +++ b/server/services/crudSystems.test.ts @@ -1,6 +1,6 @@ import * as constants from '@shared/constants' -import * as crudSystems from './crudSystems' import * as databaseAdapter from 'adapters/database' +import * as crudSystems from './crudSystems' beforeAll(async () => { databaseAdapter.initConnection() diff --git a/server/services/crudTraders.ts b/server/services/crudTraders.ts index 34ef6cbb..3c24ddeb 100644 --- a/server/services/crudTraders.ts +++ b/server/services/crudTraders.ts @@ -185,7 +185,7 @@ export const getComboDetail = async ( const traders = await traderModel.getInPKs(combo.traderIds) const { traderProfiles, holdings } = await buildComboEntities(traders) - const latestDate = await dailyTickersModel.getLatestDate() + const latestDate = await dailyTickersModel.getLatestDate(1) const startDate = holdings.length ? holdings[holdings.length - 1].date : latestDate const stats = await buildHoldingValueStats( combo.entityId, @@ -244,7 +244,7 @@ export const createTraderEnv = async ( ): Promise => { const tickerIdsAsString = tickerIds ? generateTool.sortNumsToString(tickerIds) - : null + : '' const transaction = await databaseAdapter.createTransaction() try { const envResult = await traderEnvModel.createIfEmpty({ diff --git a/server/services/crudUsers.test.ts b/server/services/crudUsers.test.ts index 4ae8ae5c..20c7cb05 100644 --- a/server/services/crudUsers.test.ts +++ b/server/services/crudUsers.test.ts @@ -1,5 +1,4 @@ import * as constants from '@shared/constants' -import * as crudUsers from './crudUsers' import * as databaseAdapter from 'adapters/database' import * as dateTool from 'tools/date' import * as emailAdapter from 'adapters/email' @@ -11,6 +10,7 @@ import * as localeTool from 'tools/locale' import * as userModel from 'models/user' import { SendMailOptions, Transporter } from 'nodemailer' import { instance, mock, when } from 'ts-mockito' +import * as crudUsers from './crudUsers' jest.mock('adapters/email', () => { const actual = jest.requireActual('adapters/email') diff --git a/server/services/processEmails.test.ts b/server/services/processEmails.test.ts index 7f668ed0..db0f9846 100644 --- a/server/services/processEmails.test.ts +++ b/server/services/processEmails.test.ts @@ -2,10 +2,10 @@ import * as constants from '@shared/constants' import * as databaseAdapter from 'adapters/database' import * as emailAdapter from 'adapters/email' import * as emailModel from 'models/email' -import * as processEmail from './processEmails' import * as runTool from 'tools/run' import { SendMailOptions, Transporter } from 'nodemailer' import { instance, mock, when } from 'ts-mockito' +import * as processEmail from './processEmails' jest.mock('adapters/email', () => { const actual = jest.requireActual('adapters/email') diff --git a/server/services/shared/buildHoldingValueStats.ts b/server/services/shared/buildHoldingValueStats.ts index 64299906..69d8e305 100644 --- a/server/services/shared/buildHoldingValueStats.ts +++ b/server/services/shared/buildHoldingValueStats.ts @@ -16,7 +16,7 @@ export const calHoldingValueByDate = async ( cacheAge: '1d', cacheKey: cacheTool.generateTickerPricesKey(entityId, date), buildFunction: async () => { - const dailyTickers = await dailyTickersModel.getByUK(entityId, date, ['nearestPrices']) + const dailyTickers = await dailyTickersModel.getByUK(entityId, date) return dailyTickers?.priceInfo || {} }, preferLocal: true, diff --git a/server/tasks/cache.test.ts b/server/tasks/cache.test.ts index e9d5be7f..b1d1ce03 100644 --- a/server/tasks/cache.test.ts +++ b/server/tasks/cache.test.ts @@ -1,6 +1,6 @@ -import * as cache from './cache' import * as constants from '@shared/constants' import * as crudSystems from 'services/crudSystems' +import * as cache from './cache' jest.mock('services/crudSystems', () => { const actual = jest.requireActual('services/crudSystems') diff --git a/server/tasks/calc.test.ts b/server/tasks/calc.test.ts index 910c1667..552109db 100644 --- a/server/tasks/calc.test.ts +++ b/server/tasks/calc.test.ts @@ -1,7 +1,7 @@ -import * as calc from './calc' import * as calcIndicators from 'services/calcIndicators' import * as calcTickers from 'services/calcTickers' import * as calcTraders from 'services/calcTraders' +import * as calc from './calc' jest.mock('services/calcIndicators', () => { const actual = jest.requireActual('services/calcIndicators') diff --git a/server/tasks/email.test.ts b/server/tasks/email.test.ts index bc89869b..4e6e955c 100644 --- a/server/tasks/email.test.ts +++ b/server/tasks/email.test.ts @@ -1,5 +1,5 @@ -import * as email from './email' import * as processEmails from 'services/processEmails' +import * as email from './email' jest.mock('services/processEmails', () => { const actual = jest.requireActual('services/processEmails') diff --git a/server/tasks/sync.test.ts b/server/tasks/sync.test.ts index 2261d577..2449e7d0 100644 --- a/server/tasks/sync.test.ts +++ b/server/tasks/sync.test.ts @@ -1,6 +1,6 @@ import * as dateTool from 'tools/date' -import * as sync from './sync' import * as syncTickers from 'services/syncTickers' +import * as sync from './sync' jest.mock('services/syncTickers', () => { const actual = jest.requireActual('services/syncTickers') diff --git a/server/tools/date.test.ts b/server/tools/date.test.ts index 35e93b35..3d9551ca 100644 --- a/server/tools/date.test.ts +++ b/server/tools/date.test.ts @@ -1,6 +1,6 @@ import 'moment-timezone' -import * as date from './date' import moment from 'moment' +import * as date from './date' describe('#getInitialDate', () => { test('could get initial date', () => { diff --git a/server/tools/generate.test.ts b/server/tools/generate.test.ts index 552f71a4..14ac7310 100644 --- a/server/tools/generate.test.ts +++ b/server/tools/generate.test.ts @@ -1,7 +1,7 @@ import * as constants from '@shared/constants' -import * as generate from './generate' import { instance, mock } from 'ts-mockito' import SMTPTransport from 'nodemailer/lib/smtp-transport' +import * as generate from './generate' describe('buildAccessHash', () => { test('could generate accessHash', () => { diff --git a/server/tools/generate.ts b/server/tools/generate.ts index 4e884b93..a5d61a4e 100644 --- a/server/tools/generate.ts +++ b/server/tools/generate.ts @@ -1,12 +1,12 @@ +import fs from 'fs' +import path from 'path' import * as adapterEnum from 'enums/adapter' import * as constants from '@shared/constants' import * as emailEnum from 'enums/email' import * as helpers from '@shared/helpers' import * as interfaces from '@shared/interfaces' import SMTPTransport from 'nodemailer/lib/smtp-transport' -import fs from 'fs' import jwt from 'jsonwebtoken' -import path from 'path' export const buildAccessHash = (digits: number): string => { const code = helpers.toMD5(Math.random().toString()) diff --git a/server/tools/locale.test.ts b/server/tools/locale.test.ts index f6c9c43b..169106ac 100644 --- a/server/tools/locale.test.ts +++ b/server/tools/locale.test.ts @@ -1,5 +1,5 @@ -import * as locale from './locale' import enLocale from 'locales/en.json' +import * as locale from './locale' describe('#getTranslation', () => { test('could get translation', () => { From 687a1858e855f9298dd0d26a3eba854b57c0df71 Mon Sep 17 00:00:00 2001 From: Baozier Date: Mon, 23 Oct 2023 10:26:43 -0400 Subject: [PATCH 5/5] fix --- .github/workflows/pullRequest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pullRequest.yml b/.github/workflows/pullRequest.yml index 896dac15..2111a35d 100644 --- a/.github/workflows/pullRequest.yml +++ b/.github/workflows/pullRequest.yml @@ -19,6 +19,6 @@ jobs: cache: 'npm' - run: npm ci - run: npm run lint - - run: npm run basic + - run: npm run shared - run: npm run tsc - run: npm run test