Skip to content

Commit

Permalink
Milestone in saving to DB:
Browse files Browse the repository at this point in the history
Had to turn off SWC compilation in next.js. Should be fixed in
vercel/next.js#32914

Added some babel plugins needed for decoratorMetadata emit

Workaround for DB connection failure when dealing with HMR in dev mode

Workaround for loading Entity classes (has to have ormconfig.js config
overridden in app to get around dynamic import which was causing all
sorts of issues with JS module loading)
  • Loading branch information
chrisbenincasa committed Jan 30, 2022
1 parent 49158b2 commit 220ec22
Show file tree
Hide file tree
Showing 19 changed files with 751 additions and 127 deletions.
17 changes: 17 additions & 0 deletions .babelrc
@@ -0,0 +1,17 @@
{
"presets": [
"next/babel"
],
"plugins": [
"babel-plugin-transform-typescript-metadata",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
[
"@babel/plugin-proposal-class-properties",
],
],
}
73 changes: 71 additions & 2 deletions app/db.ts
@@ -1,5 +1,74 @@
import { Connection, createConnection } from 'typeorm';
import 'reflect-metadata';
import {
Connection,
ConnectionManager,
createConnection,
EntityManager,
getConnectionManager,
getConnectionOptions,
} from 'typeorm';
import { Execution } from '../model/db/entity/Execution';
import { Trade } from '../model/db/entity/Trade';

export default async function initDb(): Promise<Connection> {
export async function initDb(): Promise<Connection> {
const connectionOptions = await getConnectionOptions();
Object.assign(connectionOptions, {
entities: [Execution, Trade],
});
return await createConnection();
}

// const getConnectionOptions = () => {};

/**
* Database manager class
*/
class Database {
private connectionManager: ConnectionManager;

private hasCreatedConnection = false;

constructor() {
this.connectionManager = getConnectionManager();
}

private async getConnection(): Promise<Connection> {
const DEFAULT_CONNECTION_NAME = 'default';
const currentConnection = this.connectionManager.has(
DEFAULT_CONNECTION_NAME
)
? this.connectionManager.get(DEFAULT_CONNECTION_NAME)
: undefined;
if (currentConnection && !this.hasCreatedConnection) {
console.debug('recreating connection due to hot reloading');
if (currentConnection.isConnected) {
await currentConnection.close();
}
console.debug('done closing, making new connection..');
return this.createConnectionWithName(DEFAULT_CONNECTION_NAME);
}
if (currentConnection) {
if (!currentConnection.isConnected) {
return currentConnection.connect();
} else return currentConnection;
} else {
return this.createConnectionWithName(DEFAULT_CONNECTION_NAME);
}
}

private async createConnectionWithName(name: string): Promise<Connection> {
this.hasCreatedConnection = true;
const connectionOptions = await getConnectionOptions();
Object.assign(connectionOptions, {
entities: [Execution, Trade],
});
return createConnection(connectionOptions);
}

public getManager(): Promise<EntityManager> {
return this.getConnection().then((conn) => conn.manager);
}
}

const db = new Database();
export default db;
3 changes: 2 additions & 1 deletion app/store.ts
@@ -1,10 +1,11 @@
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';

import counterReducer from '../features/counterSlice';
import executionsReducer from '../features/executionsSlice';

export function makeStore() {
return configureStore({
reducer: { counter: counterReducer },
reducer: { counter: counterReducer, exeuctions: executionsReducer },
});
}

Expand Down
60 changes: 31 additions & 29 deletions components/tradesTable.tsx
Expand Up @@ -160,41 +160,43 @@ export default function TradesTable(props: Props) {
})}
</tbody>
</table>
<AreaChart
width={730}
height={250}
data={chartData}
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8884d8" stopOpacity={0.8} />
<stop offset="95%" stopColor="#8884d8" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorPv" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#82ca9d" stopOpacity={0.8} />
<stop offset="95%" stopColor="#82ca9d" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis dataKey="name" />
<YAxis />
<CartesianGrid strokeDasharray="3 3" />
<Tooltip />
<Area
type="monotone"
dataKey="y"
stroke="#8884d8"
fillOpacity={1}
fill="url(#colorUv)"
/>
{/* <Area
{chartData.length > 0 ? (
<AreaChart
width={730}
height={250}
data={chartData}
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8884d8" stopOpacity={0.8} />
<stop offset="95%" stopColor="#8884d8" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorPv" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#82ca9d" stopOpacity={0.8} />
<stop offset="95%" stopColor="#82ca9d" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis dataKey="name" />
<YAxis />
<CartesianGrid strokeDasharray="3 3" />
<Tooltip />
<Area
type="monotone"
dataKey="y"
stroke="#8884d8"
fillOpacity={1}
fill="url(#colorUv)"
/>
{/* <Area
type="monotone"
dataKey="pv"
stroke="#82ca9d"
fillOpacity={1}
fill="url(#colorPv)"
/> */}
</AreaChart>
</AreaChart>
) : null}
</div>
);
}
43 changes: 43 additions & 0 deletions features/executionsSlice.ts
@@ -0,0 +1,43 @@
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Execution, ExecutionJson, toExecutionJson } from '../model';
import TraderPerfApiClient from '../util/apiClient';

export interface ExeuctionsState {
executions: ExecutionJson[];
}

const initialState: ExeuctionsState = {
executions: [],
};

export const getExecutionsAsync = createAsyncThunk(
'executions/fetch',
async () => {
const executions = await new TraderPerfApiClient().getExecutions();
return executions.data as object;
}
);

export const saveExxecutionsAsync = createAsyncThunk(
'executions/fetch',
async (executions: Execution[]) => {
const response = await new TraderPerfApiClient().saveExecutions(
executions.map(toExecutionJson)
);
return response.data as object;
}
);

export const executionsSlice = createSlice({
name: 'executions',
initialState,
reducers: {
setExecutions: (state, action: PayloadAction<ExecutionJson[]>) => {
state.executions = [...action.payload];
},
},
});

export const { setExecutions } = executionsSlice.actions;

export default executionsSlice.reducer;
26 changes: 21 additions & 5 deletions model/db/entity/Execution.ts
@@ -1,12 +1,28 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { ExecutionType } from '../..';
import {
Column,
Entity,
ManyToOne,
PrimaryColumn,
PrimaryGeneratedColumn,
} from 'typeorm';
import { ExecutionType, Platform } from '../..';
import { Trade } from './Trade';

@Entity()
export class Execution {
@PrimaryGeneratedColumn('uuid') id: string;
@Column() symbol: string;
@Column() executionTimestamp: Date;
@PrimaryColumn() userId?: number;

@PrimaryColumn() symbol: string;

@PrimaryColumn() executionTimestamp: Date;

@PrimaryColumn({
type: 'enum',
enum: Platform,
enumName: 'platform',
})
platform: Platform;

@Column({
type: 'enum',
enum: ExecutionType,
Expand Down
25 changes: 22 additions & 3 deletions model/db/entity/Trade.ts
@@ -1,10 +1,29 @@
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import {
Column,
Entity,
OneToMany,
PrimaryColumn,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Platform } from '../..';
import { Execution } from './Execution';

@Entity()
export class Trade {
@PrimaryGeneratedColumn('uuid') id: string;
@Column() symbol: string;
@PrimaryGeneratedColumn() userId?: number;

@PrimaryColumn() symbol: string;

@PrimaryColumn() opened: Date;

@PrimaryColumn({
type: 'enum',
enum: Platform,
enumName: 'platform',
})
platform: Platform;

@Column() closed?: Date;

@OneToMany(() => Execution, (execution) => execution.trade)
executions: Execution[];
Expand Down
58 changes: 56 additions & 2 deletions model/index.ts
@@ -1,23 +1,63 @@
import { Dinero, DineroSnapshot } from 'dinero.js';
import { Dinero, DineroSnapshot, toSnapshot } from 'dinero.js';
import _ from 'lodash';
import { DateTime } from 'luxon';

export type TraderperfRequest<T> = {
data: T;
};

export type TraderperfResponse<T> = {
data: T;
};

export enum ExecutionType {
BUY_TO_OPEN,
SELL_TO_OPEN,
BUY_TO_CLOSE,
SELL_TO_CLOSE,
}

export enum Platform {
INTERACTIVE_BROKERS = 'interactive_brokers',
THINKORSWIM = 'thinkorswim',
TASTYWORKS = 'tastyworks',
}

export const allPlatforms: Platform[] = (() => {
const values = Object.values(Platform);
const ret: Platform[] = [];
for (let index = 0; index < values.length; index++) {
const element = values[index];
if (_.isString(element)) {
ret.push(element);
}
}
return ret.sort();
})();

export const platformToPrettyString = (p: Platform): string => {
switch (p) {
case Platform.INTERACTIVE_BROKERS:
return 'Interactive Brokers';
case Platform.THINKORSWIM:
return 'ThinkOrSwim';
case Platform.TASTYWORKS:
return 'Tastyworks';
}
};

export interface ExecutionJson {
platform: Platform;
symbol: string;
executionType: ExecutionType;
timestamp: DateTime;
timestamp: string;
quantity: number;
pps: DineroSnapshot<number>;
totalOutflow: DineroSnapshot<number>;
}

export interface TradeJson {
platform: Platform;
symbol: string;
quantity: number;
executions: ExecutionJson[];
Expand All @@ -28,6 +68,7 @@ export interface TradeJson {
}

export interface Execution {
platform: Platform;
symbol: string;
executionType: ExecutionType;
timestamp: DateTime;
Expand All @@ -36,7 +77,20 @@ export interface Execution {
totalOutflow: Dinero<number>;
}

export const toExecutionJson = (execution: Execution): ExecutionJson => {
return {
platform: execution.platform,
symbol: execution.symbol,
executionType: execution.executionType,
timestamp: execution.timestamp,
quantity: execution.quantity,
pps: toSnapshot(execution.pps),
totalOutflow: toSnapshot(execution.totalOutflow),
};
};

export interface Trade {
platform: Platform;
symbol: string;
quantity: number;
executions: Execution[];
Expand Down

0 comments on commit 220ec22

Please sign in to comment.