diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index a7ec32a..aa285fa 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -12,3 +12,14 @@ export const bybit_logs = pgTable('bybit_logs', (t) => ({ response: t.json('response').notNull(), createdAt: t.bigint('created_at', { mode: 'number' }).notNull(), })) + +export const stacking_config = pgTable('stacking_config', (t) => ({ + id: t.text('id').primaryKey(), + threshhold: t.bigint('threshold', { mode: 'number' }).notNull(), + stackRate: t.integer('stack_rate').notNull(), +})) + +export const current_stack = pgTable('current_stack', (t) => ({ + id: t.text('id').primaryKey(), + stack: t.integer('stack').notNull(), +})) diff --git a/src/lib/nanoid.ts b/src/lib/nanoid.ts new file mode 100644 index 0000000..63c4615 --- /dev/null +++ b/src/lib/nanoid.ts @@ -0,0 +1,14 @@ +// nanoid code +const urlAlphabet = + 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict' +export const nanoid = (size = 32) => { + let id = '' + let bytes = crypto.getRandomValues(new Uint8Array((size |= 0))) + while (size--) { + // Using the bitwise AND operator to "cap" the value of + // the random byte from 255 to 63, in that way we can make sure + // that the value will be a valid index for the "chars" string. + id += urlAlphabet[bytes[size] & 63] + } + return id +} diff --git a/src/routes/api/bybit/stacking/+server.ts b/src/routes/api/bybit/stacking/+server.ts new file mode 100644 index 0000000..de41cd6 --- /dev/null +++ b/src/routes/api/bybit/stacking/+server.ts @@ -0,0 +1,40 @@ +import { db } from '$lib/db/index.js' +import { stacking_config } from '$lib/db/schema.js' +import { nanoid } from '$lib/nanoid.js' +import { eq } from 'drizzle-orm' +import { z } from 'zod' + +export const POST = async ({ request }) => { + const body = await request.json() + + const data = z + .object({ + threshhold: z.number().min(0), + stackRate: z.number().min(0), + }) + .safeParse(body) + + if (!data.success) { + return new Response() + } + + const existing = await db.query.stacking_config.findFirst() + + if (!existing) { + await db.insert(stacking_config).values({ + id: nanoid(), + stackRate: data.data.stackRate, + threshhold: data.data.threshhold, + }) + } else { + await db + .update(stacking_config) + .set({ + stackRate: data.data.stackRate, + threshhold: data.data.threshhold, + }) + .where(eq(stacking_config.id, existing.id)) + } + + return new Response() +} diff --git a/src/routes/dashboard/(components)/app-sidebar.svelte b/src/routes/dashboard/(components)/app-sidebar.svelte index e5399de..d4bb572 100644 --- a/src/routes/dashboard/(components)/app-sidebar.svelte +++ b/src/routes/dashboard/(components)/app-sidebar.svelte @@ -24,6 +24,12 @@ icon: HomeIcon, isActive: page.url.pathname === `/dashboard/crypto-data`, }, + { + title: 'Stack', + url: `/dashboard/stack`, + icon: CoinsIcon, + isActive: page.url.pathname === `/dashboard/stack`, + }, { title: 'Download', url: `/dashboard/download`, diff --git a/src/routes/dashboard/stack/+page.server.ts b/src/routes/dashboard/stack/+page.server.ts new file mode 100644 index 0000000..5b96d07 --- /dev/null +++ b/src/routes/dashboard/stack/+page.server.ts @@ -0,0 +1,8 @@ +import { db } from '$lib/db' + +export const load = async () => { + const config = await db.query.stacking_config.findFirst() + return { + config, + } +} diff --git a/src/routes/dashboard/stack/+page.svelte b/src/routes/dashboard/stack/+page.svelte new file mode 100644 index 0000000..c5db5a5 --- /dev/null +++ b/src/routes/dashboard/stack/+page.svelte @@ -0,0 +1,38 @@ + + +
+
+
+ Threshold + +
+
+ Stack Rate (+%): + +
+ +
+
diff --git a/src/routes/webhook/tradingview/+server.ts b/src/routes/webhook/tradingview/+server.ts index 4244745..7cb79c2 100644 --- a/src/routes/webhook/tradingview/+server.ts +++ b/src/routes/webhook/tradingview/+server.ts @@ -1,7 +1,13 @@ import { db } from '$lib/db/index.js' -import { bybit_logs } from '$lib/db/schema.js' +import { + bybit_logs, + current_stack, + stacking_config, +} from '$lib/db/schema.js' +import { nanoid } from '$lib/nanoid.js' import { json } from '@sveltejs/kit' import { RestClientV5 } from 'bybit-api' +import { eq } from 'drizzle-orm' import { z } from 'zod' export const POST = async ({ locals, request }) => { @@ -11,6 +17,8 @@ export const POST = async ({ locals, request }) => { return new Response() } + const requestID = nanoid(6) + const body = await request.json() console.log('Body:', body) @@ -27,6 +35,18 @@ export const POST = async ({ locals, request }) => { stopLoss: z.string().optional(), hedge: z.enum(['true', 'false']).optional(), }) + const compoundingSchema = z.object({ + type: z.literal('Compound'), + key: z.string(), + secret: z.string(), + symbol: z.string(), + side: z.enum(['Buy', 'Sell']), + qty: z.string(), + leverage: z.string().optional(), + takeProfit: z.string().optional(), + stopLoss: z.string().optional(), + hedge: z.enum(['true', 'false']).optional(), + }) const percentSchema = z.object({ type: z.literal('Percent'), key: z.string(), @@ -51,7 +71,12 @@ export const POST = async ({ locals, request }) => { side: z.enum(['Buy', 'Sell']).optional(), }) const form = z - .union([flatSchema, percentSchema, closePositionSchema]) + .union([ + flatSchema, + percentSchema, + closePositionSchema, + compoundingSchema, + ]) .safeParse(body) if (!form.success) { @@ -149,7 +174,7 @@ export const POST = async ({ locals, request }) => { ).toFixed(decimalLength) } positionIdx = form.data.side === 'Buy' ? 1 : 2 - } else { + } else if (form.data.type === 'Close Position') { const position = await client.getPositionInfo({ category: 'linear', symbol: symbol, @@ -192,6 +217,21 @@ export const POST = async ({ locals, request }) => { : ('Buy' as const) qty = position.result.list[0].size } + } else { + const stacking_config = + await db.query.stacking_config.findFirst() + + if (!stacking_config) { + return new Response() + } + + qty = ( + Number(form.data.qty) * Number(form.data.leverage || '1') + ).toString() + takeProfit = form.data.takeProfit + stopLoss = form.data.stopLoss + side = form.data.side + positionIdx = form.data.side === 'Buy' ? 1 : 2 } console.log({ qty, takeProfit, stopLoss }) @@ -209,6 +249,64 @@ export const POST = async ({ locals, request }) => { console.log('Order:', order) + if (form.data.type === 'Close Position') { + console.log('Close Position, checking for compound...') + + const wallet = await client.getTransferableAmount({ + coinName: 'USDT', + }) + + const balance = wallet.result.availableWithdrawal + + console.log(`[REQUEST_${requestID}][COMPOUND]`) + console.log( + `[REQUEST_${requestID}][COMPOUND][AVAILABLE_BALANCE]`, + balance, + ) + + const config = await db.query.stacking_config.findFirst() + + if (config) { + console.log( + `[REQUEST_${requestID}][COMPOUND][THRESHOLD]`, + config.threshhold, + ) + + if (parseFloat(balance) >= config.threshhold * 2) { + const withdrawAmount = + parseFloat(balance) - config.threshhold + + console.log( + `[REQUEST_${requestID}][COMPOUNT][WITHDRAW_AMOUNT]`, + withdrawAmount, + ) + + const response = await client.createInternalTransfer( + crypto.randomUUID(), + 'USDT', + withdrawAmount.toString(), + 'UNIFIED', + 'FUND', + ) + + console.log( + `[REQUEST_${requestID}][COMPOUNT][WITHDRAW_RESPONSE]`, + response, + ) + + const stack = await db.query.current_stack.findFirst() + if (stack) { + await db + .update(current_stack) + .set({ + stack: stack.stack + config.stackRate, + }) + .where(eq(current_stack.id, stack.id)) + } + } + } + } + await db.insert(bybit_logs).values({ status: order.retCode === 0 ? 'success' : 'failed', request: form.data,