Skip to content

Commit eb24ab0

Browse files
feat : Update ENV, Seed to Sync Data from Local PostgreSQL to NeonDB
1 parent d8a7475 commit eb24ab0

File tree

9 files changed

+207
-55
lines changed

9 files changed

+207
-55
lines changed

client/components/MainChart.vue

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,28 @@ onUnmounted(() => {
6464
})
6565
6666
// Get models with current values
67+
// accountValues contains total account value = current_balance + unrealized PnL
68+
// This includes both realized PnL (in current_balance via total_pnl) and unrealized PnL from open positions
6769
const modelsWithValues = computed(() => {
6870
if (props.accountValues.length === 0) return props.models.map(m => ({ ...m, currentValue: 10000 }))
6971
const latest = props.accountValues[props.accountValues.length - 1]
7072
return props.models.map(model => ({
7173
...model,
74+
// latest.models[model.id] is the total account value (account_value from database)
7275
currentValue: latest.models[model.id] || 10000
7376
}))
7477
})
7578
7679
// Prepare chart series
80+
// This plots TOTAL ACCOUNT VALUE over time:
81+
// - account_value = current_balance (includes realized PnL) + unrealized PnL from open positions
82+
// - Includes both realized PnL from closed positions (via total_pnl) and unrealized PnL from open positions
7783
const chartSeries = computed(() => {
7884
return props.models.map(model => {
7985
const data = props.accountValues.map(av => ({
8086
x: av.timestamp.getTime(),
87+
// y is the total account value at this timestamp
88+
// This includes: initial_balance + realized PnL (from total_pnl) + unrealized PnL (from open positions)
8189
y: av.models[model.id] || 10000
8290
}))
8391
@@ -169,9 +177,9 @@ const chartOptions = computed(() => {
169177
// min: yAxisMin,
170178
// max: yAxisMax,
171179
// tickAmount: Math.ceil((yAxisMax - yAxisMin) / 5000),
172-
min: 7000,
173-
max: 13000,
174-
tickAmount: 6,
180+
min: 6000,
181+
max: 16000,
182+
tickAmount: 5,
175183
labels: {
176184
style: {
177185
colors: '#000000',

client/nuxt.config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,18 @@ export default defineNuxtConfig({
5050
},
5151
},
5252
},
53+
runtimeConfig: {
54+
postgresUser: process.env.NUXT_POSTGRES_USER || '',
55+
postgresPassword: process.env.NUXT_POSTGRES_PASSWORD || '',
56+
postgresHost: process.env.NUXT_POSTGRES_HOST || '',
57+
postgresPort: process.env.NUXT_POSTGRES_PORT || '',
58+
postgresDb: process.env.NUXT_POSTGRES_DB || '',
59+
dbConnectionString: process.env.NUXT_DB_CONNECTION_STRING || '',
60+
nodeEnv: process.env.NUXT_NODE_ENV || '',
61+
62+
public: {
63+
// apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
64+
},
65+
},
5366
})
5467

client/server/api/account-values.get.ts

Lines changed: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getDb, accounts, accountSnapshots } from '~/server/utils/db'
1+
import { getDb, accounts, accountSnapshots, orders, positions } from '~/server/utils/db'
22
import { eq, desc, asc } from 'drizzle-orm'
33

44
export default defineEventHandler(async (event) => {
@@ -33,61 +33,80 @@ export default defineEventHandler(async (event) => {
3333
snapshotsByAccount.get(snapshot.account_id)!.push(snapshot)
3434
}
3535

36-
// If we have snapshots, use them
36+
// If we have snapshots, use them for accurate historical data
3737
if (allSnapshots.length > 0) {
38-
// Get unique timestamps from all snapshots
39-
const uniqueTimestamps = Array.from(new Set(allSnapshots.map(s => s.snapshot_at.getTime())))
40-
.sort()
41-
.map(ts => new Date(ts))
38+
// Group snapshots by timestamp to ensure we get all accounts at each point
39+
const snapshotsByTimestamp = new Map<number, typeof allSnapshots>()
4240

43-
// Build account values array
44-
for (const timestamp of uniqueTimestamps) {
41+
for (const snapshot of allSnapshots) {
42+
const timestamp = new Date(snapshot.snapshot_at).getTime()
43+
if (!snapshotsByTimestamp.has(timestamp)) {
44+
snapshotsByTimestamp.set(timestamp, [])
45+
}
46+
snapshotsByTimestamp.get(timestamp)!.push(snapshot)
47+
}
48+
49+
// Get unique timestamps and sort them
50+
const uniqueTimestamps = Array.from(snapshotsByTimestamp.keys()).sort()
51+
52+
// Build account values array from snapshots
53+
for (const timestampMs of uniqueTimestamps) {
54+
const timestamp = new Date(timestampMs)
55+
const snapshotsAtTime = snapshotsByTimestamp.get(timestampMs) || []
56+
4557
const entry: { timestamp: Date; models: Record<string, number> } = {
4658
timestamp,
4759
models: {},
4860
}
4961

50-
// For each account, find the closest snapshot at or before this timestamp
62+
// For each account, find its snapshot at this exact timestamp
5163
for (const account of allAccounts) {
52-
const accountSnapshots = snapshotsByAccount.get(account.id) || []
53-
// Find the latest snapshot at or before this timestamp
54-
let closestSnapshot = accountSnapshots
55-
.filter(s => new Date(s.snapshot_at).getTime() <= timestamp.getTime())
56-
.sort((a, b) => new Date(b.snapshot_at).getTime() - new Date(a.snapshot_at).getTime())[0]
64+
const snapshot = snapshotsAtTime.find(s => s.account_id === account.id)
5765

58-
if (closestSnapshot && closestSnapshot.account_value) {
59-
entry.models[account.id] = parseFloat(closestSnapshot.account_value)
60-
} else if (account.account_value) {
61-
// Fallback to current account_value if no snapshot
62-
entry.models[account.id] = parseFloat(account.account_value)
66+
if (snapshot && snapshot.account_value) {
67+
entry.models[account.id] = parseFloat(snapshot.account_value)
68+
} else {
69+
// Find the latest snapshot before this timestamp
70+
const accountSnapshots = snapshotsByAccount.get(account.id) || []
71+
const closestSnapshot = accountSnapshots
72+
.filter(s => new Date(s.snapshot_at).getTime() <= timestampMs)
73+
.sort((a, b) => new Date(b.snapshot_at).getTime() - new Date(a.snapshot_at).getTime())[0]
74+
75+
if (closestSnapshot && closestSnapshot.account_value) {
76+
entry.models[account.id] = parseFloat(closestSnapshot.account_value)
77+
} else if (account.account_value) {
78+
// Fallback to current account_value if no snapshot found
79+
entry.models[account.id] = parseFloat(account.account_value)
80+
}
6381
}
6482
}
6583

6684
if (Object.keys(entry.models).length > 0) {
6785
accountValues.push(entry)
6886
}
6987
}
70-
}
71-
72-
// If no snapshots, add current account values as a single point
73-
if (accountValues.length === 0) {
74-
const currentEntry: { timestamp: Date; models: Record<string, number> } = {
75-
timestamp: new Date(),
76-
models: {},
77-
}
7888

79-
for (const account of allAccounts) {
80-
// Use stored account_value, or calculate from current_balance if not stored
81-
if (account.account_value) {
82-
currentEntry.models[account.id] = parseFloat(account.account_value)
83-
} else {
84-
// Fallback: use current_balance (shouldn't happen if metrics are updated)
85-
currentEntry.models[account.id] = parseFloat(account.current_balance)
89+
// Add current value as the latest point if not already included
90+
const latestTimestamp = uniqueTimestamps.length > 0 ? uniqueTimestamps[uniqueTimestamps.length - 1] : null
91+
const currentTime = Date.now()
92+
if (!latestTimestamp || (currentTime - latestTimestamp) > 60000) { // More than 1 minute difference
93+
const currentEntry: { timestamp: Date; models: Record<string, number> } = {
94+
timestamp: new Date(),
95+
models: {},
96+
}
97+
98+
for (const account of allAccounts) {
99+
if (account.account_value) {
100+
currentEntry.models[account.id] = parseFloat(account.account_value)
101+
} else {
102+
// Fallback: use current_balance
103+
currentEntry.models[account.id] = parseFloat(account.current_balance)
104+
}
105+
}
106+
107+
if (Object.keys(currentEntry.models).length > 0) {
108+
accountValues.push(currentEntry)
86109
}
87-
}
88-
89-
if (Object.keys(currentEntry.models).length > 0) {
90-
accountValues.push(currentEntry)
91110
}
92111
}
93112

client/server/api/completed-trades.get.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,17 @@ export default defineEventHandler(async (event) => {
6767

6868
const modelName = getModelName(account.id)
6969

70+
// Use stored trade_value if available, otherwise calculate
7071
// Calculate entry price from position or use order price
7172
const entryPrice = position ? parseFloat(position.entry_price) : parseFloat(order.price || '0')
7273
const exitPrice = parseFloat(order.filled_price)
7374
const quantity = parseFloat(order.quantity)
75+
76+
// Use stored trade_value if available, otherwise calculate notional
77+
const tradeValue = order.trade_value ? parseFloat(order.trade_value) : null
7478
const notionalEntry = entryPrice * quantity
7579
const notionalExit = exitPrice * quantity
80+
7681
const realizedPnl = parseFloat(order.realized_pnl)
7782

7883
// Determine trade type and holding time
@@ -104,8 +109,11 @@ export default defineEventHandler(async (event) => {
104109
quantity: quantity,
105110
notional_entry: notionalEntry,
106111
notional_exit: notionalExit,
112+
trade_value: tradeValue, // Include stored trade_value
107113
holding_time: holdingTimeFormatted || '0m',
108114
net_pnl: realizedPnl,
115+
// Calculate return percentage for this trade
116+
return_percent: notionalEntry > 0 ? (realizedPnl / notionalEntry) * 100 : 0,
109117
})
110118
}
111119

client/server/api/positions.get.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ export default defineEventHandler(async (event) => {
4040
total_pnl: parseFloat(account.total_pnl),
4141
account_value: account.account_value ? parseFloat(account.account_value) : parseFloat(account.current_balance),
4242
crypto_value: cryptoValue,
43+
// Include performance metrics
44+
total_return_percent: account.total_return_percent ? parseFloat(account.total_return_percent) : null,
45+
sharpe_ratio: account.sharpe_ratio ? parseFloat(account.sharpe_ratio) : null,
4346
},
4447
positions: [],
4548
total_unrealized_pnl: cryptoValue,

client/server/utils/db.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,22 @@ import { drizzle } from 'drizzle-orm/postgres-js'
22
import postgres from 'postgres'
33
import * as schema from './schema'
44

5-
function getConnectionString(): string {
6-
// For Nuxt server, use environment variables or defaults for local dev
7-
const POSTGRES_USER = process.env.POSTGRES_USER || 'postgres'
8-
const POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD || 'postgres'
9-
const POSTGRES_HOST = process.env.POSTGRES_HOST || 'localhost'
10-
const POSTGRES_PORT = process.env.POSTGRES_PORT || '5432'
11-
const POSTGRES_DB = process.env.POSTGRES_DB || 'trading_bot'
5+
export function connectionString(env?: string): string {
6+
const config = useRuntimeConfig()
7+
const environment = env || config.nodeEnv
128

13-
return `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}`
9+
if (environment === 'production' && config.dbConnectionString) {
10+
return config.dbConnectionString
11+
}
12+
return `postgresql://${config.postgresUser}:${config.postgresPassword}@${config.postgresHost}:${config.postgresPort}/${config.postgresDb}`
1413
}
1514

16-
// Create a singleton connection
1715
let client: postgres.Sql | null = null
1816
let dbInstance: ReturnType<typeof drizzle> | null = null
1917

2018
export function getDb() {
2119
if (!client || !dbInstance) {
22-
const connectionString = getConnectionString()
23-
client = postgres(connectionString)
20+
client = postgres(connectionString())
2421
dbInstance = drizzle(client, { schema })
2522
}
2623
return dbInstance

server/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"db:generate": "drizzle-kit generate",
1414
"db:migrate": "drizzle-kit migrate",
1515
"db:push": "drizzle-kit push",
16-
"db:studio": "drizzle-kit studio"
16+
"db:studio": "drizzle-kit studio",
17+
"db:sync": "bun run src/scripts/sync-to-neon.ts"
1718
},
1819
"dependencies": {
1920
"@noble/ed25519": "^2.3.0",

server/src/config/database/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import postgres from 'postgres';
33
import * as schema from './schema';
44

55

6-
export function connectionString(): string {
7-
const { POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB } = process.env;
6+
export function connectionString(env?: string): string {
7+
const { POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, DB_CONNECTION_STRING, NODE_ENV } = process.env;
8+
const environment = env || NODE_ENV;
9+
10+
if (environment === 'production' && DB_CONNECTION_STRING) {
11+
return DB_CONNECTION_STRING;
12+
}
813
return `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}`;
914
}
1015

server/src/scripts/sync-to-neon.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { drizzle } from 'drizzle-orm/postgres-js';
2+
import postgres from 'postgres';
3+
import { eq, inArray } from 'drizzle-orm';
4+
import { PgTable } from 'drizzle-orm/pg-core';
5+
import * as schema from '../config/database/schema';
6+
import { connectionString } from '../config/database/index';
7+
8+
async function syncTable(
9+
localDb: ReturnType<typeof drizzle>,
10+
neonDb: ReturnType<typeof drizzle>,
11+
table: PgTable<any>,
12+
tableName: string
13+
): Promise<number> {
14+
console.log(`Syncing ${tableName}...`);
15+
16+
const localData = await localDb.select().from(table);
17+
console.log(`Found ${localData.length} records in local database`);
18+
19+
if (localData.length === 0) {
20+
console.log(`No data to sync for ${tableName}`);
21+
return 0;
22+
}
23+
24+
let syncedCount = 0;
25+
const batchSize = 100;
26+
27+
for (let i = 0; i < localData.length; i += batchSize) {
28+
const batch = localData.slice(i, i + batchSize);
29+
30+
try {
31+
const batchIds = batch.map((b) => b.id);
32+
const existingIds = await neonDb
33+
.select({ id: table.id })
34+
.from(table)
35+
.where(inArray(table.id, batchIds));
36+
37+
const existingIdSet = new Set(existingIds.map((r) => r.id));
38+
const toInsert = batch.filter((r) => !existingIdSet.has(r.id));
39+
const toUpdate = batch.filter((r) => existingIdSet.has(r.id));
40+
41+
if (toInsert.length > 0) {
42+
await neonDb.insert(table).values(toInsert as any);
43+
syncedCount += toInsert.length;
44+
}
45+
46+
for (const record of toUpdate) {
47+
const { id, created_at, ...updateData } = record as any;
48+
await neonDb
49+
.update(table)
50+
.set(updateData)
51+
.where(eq(table.id, id));
52+
syncedCount += 1;
53+
}
54+
55+
console.log(`Synced ${syncedCount}/${localData.length} records...`);
56+
} catch (error) {
57+
console.error(`Error syncing batch ${Math.floor(i / batchSize) + 1}:`, error);
58+
throw error;
59+
}
60+
}
61+
62+
console.log(`Successfully synced ${syncedCount} records to Neon\n`);
63+
return syncedCount;
64+
}
65+
66+
async function main() {
67+
console.log('Starting database sync from Local DB to Neon DB...\n');
68+
console.log('Connecting to databases...');
69+
70+
const localClient = postgres(connectionString('development'));
71+
const neonClient = postgres(connectionString('production'));
72+
73+
const localDb = drizzle(localClient, { schema });
74+
const neonDb = drizzle(neonClient, { schema });
75+
76+
try {
77+
await localClient`SELECT 1`;
78+
await neonClient`SELECT 1`;
79+
console.log('Connected to both databases\n');
80+
81+
await syncTable(localDb, neonDb, schema.accounts, 'accounts');
82+
await syncTable(localDb, neonDb, schema.agentInvocations, 'agent_invocations');
83+
await syncTable(localDb, neonDb, schema.positions, 'positions');
84+
await syncTable(localDb, neonDb, schema.orders, 'orders');
85+
await syncTable(localDb, neonDb, schema.accountSnapshots, 'account_snapshots');
86+
87+
console.log('Database sync completed successfully!');
88+
} catch (error) {
89+
console.error('\nError during sync:', error);
90+
process.exit(1);
91+
} finally {
92+
await localClient.end();
93+
await neonClient.end();
94+
console.log('Database connections closed');
95+
}
96+
}
97+
98+
main();

0 commit comments

Comments
 (0)