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/.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
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/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/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/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/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/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/package.json b/package.json
index 765e8601..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,13 +19,13 @@
},
"repository": {
"type": "git",
- "url": "git+https://github.com/ValueMelody/melody-app"
+ "url": "git+https://github.com/ValueMelody/melody-invest"
},
"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/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/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/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/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/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/evaluation.ts b/server/logics/evaluation.ts
index 7012262d..269834c3 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,146 @@ 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 => {
+ if (!indicatorInfo) return false
- return false
+ // 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
+
+ // 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)
-
- return movementWeights
-}
+ // Multple weight for each behaviors as the final weight
+ weights = weights * movementWeight
-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,52 +417,63 @@ 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
-
- return null
- // const preferValue = getTickerPreferValue(
- // pattern.sellPreference, dailyTicker.daily, dailyTicker.quarterly, dailyTicker.yearly,
- // )
- // if (!preferValue && preferValue !== 0) return null
-
- // const weight = getTickerMovementWeights(
- // pattern,
- // dailyTicker.info,
- // constants.Behavior.TickerMovementSellBehaviors,
- // )
- // if (!weight) return null
-
- // return {
- // tickerId, preferValue, weight,
- // }
+ // When no available info for this tickerId, then do not sell
+ if (!tickerInfo) return null
+
+ // Check current patterns sell preference if tickers are equal weight
+ const preferValue = getTickerEqualWeightPreferValue(
+ pattern.sellPreference, tickerInfo,
+ )
+ if (preferValue === null) return null
+
+ // Get current tickers sell weight
+ const weight = getTickerWeights(
+ pattern,
+ tickerInfo,
+ constants.Behavior.TickerMovementSellBehaviors,
+ constants.Behavior.TickerCompareSellBehaviors,
+ )
+
+ if (!weight) return null
+
+ return {
+ tickerId,
+ preferValue,
+ weight,
+ }
}
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 = (
@@ -453,6 +482,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
@@ -462,62 +492,37 @@ 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 getIndicatorSellMatches = (
- pattern: interfaces.traderPatternModel.Record,
- indicatorInfo: interfaces.dailyIndicatorsModel.IndicatorInfo,
-): boolean => {
- const shouldSell = getIndicatorMovementAndCompareMatches(
- pattern,
- indicatorInfo,
- constants.Behavior.IndicatorMovementSellBehaviors,
- constants.Behavior.IndicatorCompareSellBehaviors,
- )
- return shouldSell
-}
-
-export const getTickerSellEaluations = (
+export const getTickerSellEvaluations = (
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 tickerEvaluations by weight and prefer value
const orderedEvaluations = getOrderedTickerEvaluations(tickerEvaluations, pattern.sellPreference)
return orderedEvaluations
}
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 988afe5c..9101259b 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 ? tickerInfo.splitMultiplier : item.splitMultiplier
+ const updatedHolding = {
+ tickerId: item.tickerId,
+ shares: item.shares,
+ value: holdingValue,
+ splitMultiplier,
+ }
+ return updatedHolding
}
-export const detailFromCashAndItems = (
+export const generateHoldingDetail = (
totalCash: number,
items: interfaces.traderHoldingModel.Item[],
- dailyTickers: interfaces.dailyTickersModel.TickerInfos,
+ tickerInfos: interfaces.dailyTickersModel.TickerInfos,
tradeDate: string,
delistedLastPrices: DelistedLastPrices,
): interfaces.traderHoldingModel.Detail => {
@@ -38,23 +40,33 @@ export const detailFromCashAndItems = (
totalValue: totalCash, totalCash, items: [], date: '',
}
return items.reduce((details, item) => {
- const isDelisted = delistedLastPrices[item.tickerId] && delistedLastPrices[item.tickerId].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 = getItemHoldingValue(item.shares, item.value, delistedLastPrices[item.tickerId])
+ const holdingValue = delistedRecord
+ ? getItemHoldingValue(
+ item.shares,
+ delistedRecord.closePrice,
+ delistedRecord.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]
+ // Recalculate one holding item by provided market data
+ const refreshedHolding = refreshHoldingItemValue(item, matchedInfo)
details.totalValue += refreshedHolding.value
details.items.push(refreshedHolding)
return details
}, emptyDetail)
}
-export const sellForLessThanTickerMinPercent = (
+export const sellItemIfLessThanTickerMinPercent = (
holdingDetail: interfaces.traderHoldingModel.Detail,
itemForSell: interfaces.traderHoldingModel.Item,
holdingPercent: number,
@@ -65,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,
@@ -75,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,
@@ -125,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 {
@@ -140,68 +154,83 @@ 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
+ // If holding item has no market data, skip rebalance
+ const matchedInfo = tickerInfos[item.tickerId]
+ if (!matchedInfo) return details
+
+ 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
+ }
+
+ // 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
- // 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
- // }
-
- // const refreshedForMaxPercent = sellForMoreThanTickerMaxPercernt(
- // details, item, holdingPercent, tickerMaxPercent, maxCashValue, matchedDaily,
- // )
- // if (refreshedForMaxPercent) {
- // hasTransaction = true
- // return refreshedForMaxPercent
- // }
-
- // return details
}, currentDetail)
+
return {
- hasTransaction: false,
+ hasTransaction,
holdingDetail: details,
}
}
-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: '',
@@ -211,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 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
+ 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
+
+ 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
@@ -275,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,
@@ -289,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/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/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/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/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.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/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/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/calcTraders.ts b/server/services/calcTraders.ts
index 8ec80025..cbdca69a 100644
--- a/server/services/calcTraders.ts
+++ b/server/services/calcTraders.ts
@@ -2,9 +2,11 @@ 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'
+import * as entityModel from 'models/entity'
import * as evaluationLogic from 'logics/evaluation'
import * as generateTool from 'tools/generate'
import * as helpers from '@shared/helpers'
@@ -61,18 +63,34 @@ 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),
buildFunction: async () => {
- const dailyTickers = await dailyTickersModel.getByUK(entityId, date, ['tickers', 'indicators'])
+ const dailyTickers = await dailyTickersModel.getByUK(entityId, date)
return dailyTickers
},
preferLocal: true,
})
}
+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,
@@ -80,133 +98,159 @@ 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 / 100
+ const holdingSellPercent = pattern.holdingSellPercent / 100
+ 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 dailyTickers = dailyTickersRecord.tickerInfos || {}
- const indicatorInfo = {}
+ const tickerInfos = dailyTickersRecord.tickerInfos
+ const dailyIndicators = await getCachedDailyIndicators(tradeDate)
+ const indicatorInfo = dailyIndicators?.indicatorInfo || {}
- const emptyDailyTickers: interfaces.dailyTickersModel.TickerInfos = {}
- const availableTargets = env.tickerIds
- ? env.tickerIds.reduce((tickers, tickerId) => {
- tickers[tickerId] = dailyTickers[tickerId]
- return tickers
- }, emptyDailyTickers)
- : dailyTickers
+ // Only keep market data related to env defined tickers
+ 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,
+ // 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,
+ )
+
+ // Check if indicatorInfo matches sell criterion
+ const shouldSellBasedOnIndicator = evaluationLogic.isIndicatorFitPatternBehaviors(
+ pattern,
+ indicatorInfo,
+ constants.Behavior.IndicatorMovementSellBehaviors,
+ constants.Behavior.IndicatorCompareSellBehaviors,
)
- const holdingTickerIds = detailAfterRebalance.items.map((item) => item.tickerId)
+ // Get a list of ordered tickerIds that should be sold
+ const holdingTickerIds = rebalancedDetail.items.map((item) => item.tickerId)
- const isSellIndicatorMatches = !!indicatorInfo && evaluationLogic.getIndicatorSellMatches(pattern, indicatorInfo)
- const sellTickerEvaluations = isSellIndicatorMatches
- ? evaluationLogic.getTickerSellEaluations(
- holdingTickerIds, pattern, availableTargets,
+ // Get evaluations of each ticker, order by which one should be sold first
+ const tickerSellEvaluations = shouldSellBasedOnIndicator
+ ? evaluationLogic.getTickerSellEvaluations(
+ holdingTickerIds, pattern, tickerInfos,
)
: []
- const sellTickerIds = sellTickerEvaluations.map((sellTickerEvaluation) => sellTickerEvaluation.tickerId)
+ 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,
+ )
+
+ // Check if indicatorInfo matches buy criterion
+ const shouldBuyBasedOnIndicator = evaluationLogic.isIndicatorFitPatternBehaviors(
+ pattern,
+ indicatorInfo,
+ constants.Behavior.IndicatorMovementBuyBehaviors,
+ constants.Behavior.IndicatorCompareBuyBehaviors,
)
- const isBuyIndicatorMatches = !!indicatorInfo && evaluationLogic.getIndicatorBuyMatches(pattern, indicatorInfo)
- const buyTickerEvaluations = isBuyIndicatorMatches
- ? evaluationLogic.getTickerBuyEaluations(
- Object.keys(availableTargets).map((id) => parseInt(id)),
- pattern,
- availableTargets,
+ // 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,
@@ -217,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()
@@ -232,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,
@@ -277,45 +328,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,
+ // )
+ // })
+ // }
+ })
})
}
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/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}]`
}
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', () => {