Filter everything with users (#16)

* Filter everything with users

* Fix backtests and user management

* Add cursor rules

* Fix backtest and bots

* Update configs names

* Sign until unauth

* Setup delegate

* Setup delegate and sign

* refact

* Enhance Privy signature generation with improved cryptographic methods

* Add Fastify backend

* Add Fastify backend routes for privy

* fix privy signing

* fix privy client

* Fix tests

* add gmx core

* fix merging sdk

* Fix tests

* add gmx core

* add gmx core

* add privy to boilerplate

* clean

* fix

* add fastify

* Remove Managing.Fastify submodule

* Add Managing.Fastify as regular directory instead of submodule

* Update .gitignore to exclude Managing.Fastify dist and node_modules directories

* Add token approval functionality to Privy plugin

- Introduced a new endpoint `/approve-token` for approving ERC20 tokens.
- Added `approveToken` method to the Privy plugin for handling token approvals.
- Updated `signPrivyMessage` to differentiate between message signing and token approval requests.
- Enhanced the plugin with additional schemas for input validation.
- Included new utility functions for token data retrieval and message construction.
- Updated tests to verify the new functionality and ensure proper request decoration.

* Add PrivyApproveTokenResponse model for token approval response

- Created a new class `PrivyApproveTokenResponse` to encapsulate the response structure for token approval requests.
- The class includes properties for `Success` status and a transaction `Hash`.

* Refactor trading commands and enhance API routes

- Updated `OpenPositionCommandHandler` to use asynchronous methods for opening trades and canceling orders.
- Introduced new Fastify routes for opening positions and canceling orders with appropriate request validation.
- Modified `EvmManager` to handle both Privy and non-Privy wallet operations, utilizing the Fastify API for Privy wallets.
- Adjusted test configurations to reflect changes in account types and added helper methods for testing Web3 proxy services.

* Enhance GMX trading functionality and update dependencies

- Updated `dev:start` script in `package.json` to include the `-d` flag for Fastify.
- Upgraded `fastify-cli` dependency to version 7.3.0.
- Added `sourceMap` option to `tsconfig.json`.
- Refactored GMX plugin to improve position opening logic, including enhanced error handling and validation.
- Introduced a new method `getMarketInfoFromTicker` for better market data retrieval.
- Updated account type in `PrivateKeys.cs` to use `Privy`.
- Adjusted `EvmManager` to utilize the `direction` enum directly for trade direction handling.

* Refactor GMX plugin for improved trading logic and market data retrieval

- Enhanced the `openGmxPositionImpl` function to utilize the `TradeDirection` enum for trade direction handling.
- Introduced `getTokenDataFromTicker` and `getMarketByIndexToken` functions for better market and token data retrieval.
- Updated collateral calculation and logging for clarity.
- Adjusted `EvmManager` to ensure proper handling of price values in trade requests.

* Refactor GMX plugin and enhance testing for position opening

- Updated `test:single` script in `package.json` to include TypeScript compilation before running tests.
- Removed `this` context from `getClientForAddress` function and replaced logging with `console.error`.
- Improved collateral calculation in `openGmxPositionImpl` for better precision.
- Adjusted type casting for `direction` in the API route to utilize `TradeDirection` enum.
- Added a new test for opening a long position in GMX, ensuring functionality and correctness.

* Update sdk

* Update

* update fastify

* Refactor start script in package.json to simplify command execution

- Removed the build step from the start script, allowing for a more direct launch of the Fastify server.

* Update package.json for Web3Proxy

- Changed the name from "Web3Proxy" to "web3-proxy".
- Updated version from "0.0.0" to "1.0.0".
- Modified the description to "The official Managing Web3 Proxy".

* Update Dockerfile for Web3Proxy

- Upgraded Node.js base image from 18-alpine to 22.14.0-alpine.
- Added NODE_ENV environment variable set to production.

* Refactor Dockerfile and package.json for Web3Proxy

- Removed the build step from the Dockerfile to streamline the image creation process.
- Updated the start script in package.json to include the build step, ensuring the application is built before starting the server.

* Add fastify-tsconfig as a development dependency in Dockerfile-web3proxy

* Remove fastify-tsconfig extension from tsconfig.json for Web3Proxy

* Add PrivyInitAddressResponse model for handling initialization responses

- Introduced a new class `PrivyInitAddressResponse` to encapsulate the response structure for Privy initialization, including properties for success status, USDC hash, order vault hash, and error message.

* Update

* Update

* Remove fastify-tsconfig installation from Dockerfile-web3proxy

* Add build step to Dockerfile-web3proxy

- Included `npm run build` in the Dockerfile to ensure the application is built during the image creation process.

* Update

* approvals

* Open position from front embedded wallet

* Open position from front embedded wallet

* Open position from front embedded wallet

* Fix call contracts

* Fix limit price

* Close position

* Fix close position

* Fix close position

* add pinky

* Refactor position handling logic

* Update Dockerfile-pinky to copy package.json and source code from the correct directory

* Implement password protection modal and enhance UI with new styles; remove unused audio elements and update package dependencies.

* add cancel orders

* Update callContract function to explicitly cast account address as Address type

* Update callContract function to cast transaction parameters as any type for compatibility

* Cast transaction parameters as any type in approveTokenImpl for compatibility

* Cast wallet address and transaction parameters as Address type in approveTokenImpl for type safety

* Add .env configuration file for production setup including database and server settings

* Refactor home route to update welcome message and remove unused SDK configuration code

* add referral code

* fix referral

* Add sltp

* Fix typo

* Fix typo

* setup sltp on backtend

* get orders

* get positions with slp

* fixes

* fixes close position

* fixes

* Remove MongoDB project references from Dockerfiles for managing and worker APIs

* Comment out BotManagerWorker service registration and remove MongoDB project reference from Dockerfile

* fixes
This commit is contained in:
Oda
2025-04-20 22:18:27 +07:00
committed by GitHub
parent 0ae96a3278
commit 528c62a0a1
400 changed files with 94446 additions and 1635 deletions

View File

@@ -18,7 +18,7 @@
"dependencies": {
"@heroicons/react": "^1.0.6",
"@microsoft/signalr": "^6.0.5",
"@privy-io/react-auth": "^2.6.1",
"@privy-io/react-auth": "^2.7.2",
"@privy-io/wagmi": "^1.0.3",
"@tailwindcss/typography": "^0.5.0",
"@tanstack/react-query": "^5.67.1",
@@ -28,9 +28,13 @@
"@walletconnect/universal-provider": "^2.8.6",
"autoprefixer": "^10.4.7",
"axios": "^0.27.2",
"base64-js": "^1.5.1",
"canonicalize": "^2.0.0",
"classnames": "^2.3.1",
"connectkit": "^1.8.2",
"crypto": "^1.0.1",
"date-fns": "^2.30.0",
"elliptic": "^6.6.1",
"jotai": "^1.6.7",
"latest-version": "^9.0.0",
"lightweight-charts": "git+https://github.com/ntf/lightweight-charts.git",
@@ -52,12 +56,14 @@
"react-toastify": "^9.0.1",
"reactflow": "^11.8.3",
"tailwindcss": "^3.0.23",
"viem": "2.x",
"viem": "2.24.2",
"wagmi": "^2.14.12",
"web3": "^4.16.0",
"zustand": "^4.4.1"
"zustand": "^4.4.1",
"@gmx-io/sdk": "0.2.0"
},
"devDependencies": {
"@types/elliptic": "^6.4.18",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4",
"@types/react-grid-layout": "^1.3.2",

View File

@@ -25,7 +25,7 @@ const LogIn = () => {
}
try {
const message = 'wagmi'
const message = 'KaigenTeamXCowchain'
const t = new Toast('Signing message...')
// Use Privy's signMessage function - returns { signature: string }

View File

@@ -0,0 +1,106 @@
import React, { useState, useEffect } from 'react'
import type { Backtest, MoneyManagement } from '../../../generated/ManagingApi'
import type { IModalProps } from '../../../global/type'
import Modal from './Modal'
interface IBotNameModalProps extends IModalProps {
backtest: Backtest
isForWatchOnly: boolean
onSubmitBotName: (botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string) => void
moneyManagements: MoneyManagement[]
selectedMoneyManagement: string
setSelectedMoneyManagement: (name: string) => void
}
const BotNameModal: React.FC<IBotNameModalProps> = ({
showModal,
onClose,
backtest,
isForWatchOnly,
onSubmitBotName,
moneyManagements,
selectedMoneyManagement,
setSelectedMoneyManagement,
}) => {
const [botName, setBotName] = useState<string>('')
// Initialize botName when backtest changes
useEffect(() => {
if (backtest) {
setBotName(`${backtest.ticker}-${backtest.timeframe}`)
}
}, [backtest])
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (botName.trim()) {
onSubmitBotName(botName, backtest, isForWatchOnly, selectedMoneyManagement)
}
}
const handleMoneyManagementChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedMoneyManagement(e.target.value)
}
const titleHeader = isForWatchOnly ? 'Run Bot in Watch Mode' : 'Run Bot'
return (
<Modal
showModal={showModal}
onSubmit={handleSubmit}
onClose={onClose}
titleHeader={titleHeader}
>
<div className="form-control w-full mb-4">
<label className="label">
<span className="label-text">Bot Name</span>
</label>
<input
type="text"
placeholder="Enter bot name"
className="input input-bordered w-full"
value={botName}
onChange={(e) => setBotName(e.target.value)}
required
/>
</div>
<div className="form-control w-full mb-4">
<label className="label">
<span className="label-text">Money Management</span>
</label>
<select
className="select select-bordered w-full"
value={selectedMoneyManagement}
onChange={handleMoneyManagementChange}
required
>
{moneyManagements.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</select>
</div>
<p className="text-xs text-gray-500 mt-2 mb-4">
{isForWatchOnly
? 'The bot will run in watch-only mode and will not execute trades.'
: 'The bot will run and execute trades based on signals.'}
</p>
<div className="modal-action">
<button type="button" className="btn" onClick={onClose as () => void}>
Cancel
</button>
<button type="submit" className="btn btn-primary">
Run Bot
</button>
</div>
</Modal>
)
}
export default BotNameModal

View File

@@ -32,16 +32,33 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
setBacktests,
showLoopSlider = false,
}) => {
const [selectedAccount, setSelectedAccount] = React.useState<string>()
const [selectedTimeframe, setSelectedTimeframe] = React.useState<Timeframe>()
// Get date 15 days ago for start date
const defaultStartDate = new Date();
defaultStartDate.setDate(defaultStartDate.getDate() - 15);
const defaultStartDateString = defaultStartDate.toISOString().split('T')[0];
// Today for end date
const defaultEndDate = new Date();
const defaultEndDateString = defaultEndDate.toISOString().split('T')[0];
const [startDate, setStartDate] = useState<string>(defaultStartDateString);
const [endDate, setEndDate] = useState<string>(defaultEndDateString);
const { register, handleSubmit, setValue } = useForm<IBacktestsFormInput>({
defaultValues: {
startDate: defaultStartDateString,
endDate: defaultEndDateString
}
});
const [selectedAccount, setSelectedAccount] = useState<string>('')
const [selectedTimeframe, setSelectedTimeframe] = useState<Timeframe>(Timeframe.OneHour)
const [selectedLoopQuantity, setLoopQuantity] = React.useState<number>(
showLoopSlider ? 3 : 1
)
const [balance, setBalance] = React.useState<number>(10000)
const [days, setDays] = React.useState<number>(-17)
const [balance, setBalance] = useState<number>(10000)
const [customMoneyManagement, setCustomMoneyManagement] =
React.useState<MoneyManagement>()
React.useState<MoneyManagement | undefined>(undefined)
const [selectedMoneyManagement, setSelectedMoneyManagement] =
useState<string>()
const [showCustomMoneyManagement, setShowCustomMoneyManagement] =
@@ -55,21 +72,20 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
const moneyManagementClient = new MoneyManagementClient({}, apiUrl)
const backtestClient = new BacktestClient({}, apiUrl)
const { register, handleSubmit } = useForm<IBacktestsFormInput>()
const onSubmit: SubmitHandler<IBacktestsFormInput> = async (form) => {
const { scenarioName, tickers } = form
console.log(customMoneyManagement)
closeModal()
for (let sIndex = 0; sIndex < scenarioName.length; sIndex++) {
for (let tIndex = 0; tIndex < tickers.length; tIndex++) {
await runBacktest(
form,
form.tickers[tIndex],
form.scenarioName[sIndex],
customMoneyManagement,
1
)
}
if (!form.scenarioName) {
const t = new Toast('Please select a scenario', false);
return;
}
if (!form.tickers || form.tickers.length === 0) {
const t = new Toast('Please select at least one ticker', false);
return;
}
for (const ticker of form.tickers) {
const scenarioName = form.scenarioName;
await runBacktest(form, ticker, scenarioName, customMoneyManagement, 0);
}
}
@@ -81,19 +97,23 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
loopCount: number
) {
const t = new Toast(ticker + ' is running')
await backtestClient
// Use the name of the money management strategy if custom is not provided
const moneyManagementName = customMoneyManagement ? undefined : selectedMoneyManagement
backtestClient
.backtest_Run(
form.accountName,
form.botType,
ticker as Ticker,
scenarioName,
form.timeframe,
false,
days,
false, // watchOnly
balance,
selectedMoneyManagement,
moneyManagementName,
new Date(form.startDate), // startDate
new Date(form.endDate), // endDate
form.save,
showCustomMoneyManagement ? customMoneyManagement : undefined
customMoneyManagement
)
.then((backtest: Backtest) => {
t.update('success', `${backtest.ticker} Backtest Succeeded`)
@@ -113,7 +133,7 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
}
})
.catch((err) => {
t.update('error', 'Error :' + err)
t.update('error', 'Error: ' + err)
})
}
@@ -126,28 +146,34 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
}
function onMoneyManagementChange(e: any) {
if (e.target.value == 'custom') {
if (e.target.value === 'custom') {
setShowCustomMoneyManagement(true)
} else {
setShowCustomMoneyManagement(false)
setCustomMoneyManagement(undefined)
setSelectedMoneyManagement(e.target.value)
}
}
const { data: accounts } = useQuery({
queryFn: () => accountClient.account_GetAccounts(),
queryKey: ['accounts'],
})
const { data: scenarios } = useQuery({
queryFn: () => scenarioClient.scenario_GetScenarios(),
queryKey: ['scenarios'],
})
const { data: accounts } = useQuery({
queryFn: async () => { return await accountClient.account_GetAccounts() },
queryKey: ['accounts'],
})
// Set the first scenario as default when scenarios are loaded
useEffect(() => {
if (scenarios && scenarios.length > 0 && scenarios[0].name) {
setValue('scenarioName', scenarios[0].name);
}
}, [scenarios, setValue]);
useEffect(() => {
if (accounts) {
if (accounts && accounts.length > 0) {
setSelectedAccount(accounts[0].name)
setSelectedTimeframe(Timeframe.FiveMinutes)
}
@@ -164,16 +190,16 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
})
const { data: moneyManagements } = useQuery({
enabled: !!selectedTimeframe,
queryFn: async () => {
return await moneyManagementClient.moneyManagement_GetMoneyManagements();
},
queryKey: ['moneyManagements', selectedTimeframe],
queryKey: ['moneyManagements'],
})
useEffect(() => {
if (moneyManagements){
if (moneyManagements && moneyManagements.length > 0){
setSelectedMoneyManagement(moneyManagements[0].name)
setCustomMoneyManagement(undefined)
}
}, [moneyManagements])
@@ -189,28 +215,33 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
return (
<Modal
titleHeader="Run backtest"
showModal={showModal}
onClose={closeModal}
onSubmit={handleSubmit(onSubmit)}
onClose={closeModal}
titleHeader="Run Backtest"
>
<FormInput label="Account" htmlFor="accountName">
<select
className="select select-bordered w-full h-auto max-w-xs"
{...register('accountName', {
onChange(e) {
setSelectedAccountEvent(e)
},
})}
>
{accounts.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
<div className="space-y-4">
<FormInput label="Account" htmlFor="accountName">
<select
className="select select-bordered w-full"
{...register('accountName', {
onChange(e) {
setSelectedAccountEvent(e)
},
})}
>
<option value="" disabled>
Select an account
</option>
))}
</select>
</FormInput>
{accounts.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</select>
</FormInput>
<FormInput label="Timeframe" htmlFor="timeframe">
<select
className="select w-full max-w-xs"
@@ -219,6 +250,7 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
setSelectedTimeframeEvent(event)
},
})}
value={selectedTimeframe}
>
{Object.keys(Timeframe).map((item) => (
<option key={item} value={item}>
@@ -228,33 +260,37 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
</select>
</FormInput>
<FormInput label="Money Management" htmlFor="moneyManagement">
<select
className="select w-full max-w-xs"
{...register('moneyManagement', {
onChange(event) {
onMoneyManagementChange(event)
},
})}
>
{moneyManagements.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
<option key="custom" value="custom">
<FormInput label="Money Management" htmlFor="moneyManagement">
<select
className="select select-bordered w-full"
{...register('moneyManagement', {
onChange(event) {
onMoneyManagementChange(event)
},
})}
>
{moneyManagements.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
<option key="custom" value="custom">
Custom
</option>
</select>
</FormInput>
</select>
</FormInput>
<CustomMoneyManagement
onCreateMoneyManagement={setCustomMoneyManagement}
timeframe={selectedTimeframe || Timeframe.FifteenMinutes}
showCustomMoneyManagement={showCustomMoneyManagement}
></CustomMoneyManagement>
{showCustomMoneyManagement && (
<div className="mt-6">
<CustomMoneyManagement
onCreateMoneyManagement={setCustomMoneyManagement}
timeframe={selectedTimeframe || Timeframe.FiveMinutes}
showCustomMoneyManagement={showCustomMoneyManagement}
></CustomMoneyManagement>
</div>
)}
<FormInput label="Type" htmlFor="botType">
<FormInput label="Type" htmlFor="botType">
<select className="select w-full max-w-xs" {...register('botType')}>
{[BotType.ScalpingBot, BotType.FlippingBot].map((item) => (
<option key={item} value={item}>
@@ -264,89 +300,94 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
</select>
</FormInput>
<FormInput label="Days" htmlFor="days">
<input
id="days"
type="number"
value={days}
onChange={(e: any) => setDays(e.target.value)}
step="1"
min="-360"
max="-1"
></input>
</FormInput>
<FormInput label="Balance" htmlFor="balance">
<input
id="balance"
type='number'
value={balance}
onChange={(e: any) => setBalance(e.target.value)}
step="1000"
min="1000"
max="100000"
></input>
</FormInput>
<FormInput label="Scenario" htmlFor="scenarioName">
<select
className="select select-bordered w-full"
{...register('scenarioName')}
>
<option value="" disabled>Select a scenario</option>
{scenarios.map((item) => (
<option key={item.name || 'unnamed'} value={item.name || ''}>
{item.name || 'Unnamed Scenario'}
</option>
))}
</select>
</FormInput>
<FormInput label="Scenario" htmlFor="scenarioName">
<select
multiple
className="select select-bordered w-full h-auto max-w-xs"
{...register('scenarioName')}
>
{scenarios.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</select>
</FormInput>
<FormInput label="Tickers" htmlFor="tickers">
<select
multiple
className="select select-bordered w-full h-auto max-w-xs"
{...register('tickers')}
>
{tickers ? (
tickers.map((item) => (
<FormInput label="Tickers" htmlFor="tickers">
<select
className="select select-bordered w-full"
multiple
{...register('tickers')}
>
{tickers?.map((item) => (
<option key={item} value={item}>
{item}
</option>
))
) : (
<option key="NoTicker" value="No Ticker">
No ticker
</option>
)}
</select>
</FormInput>
{/* Loop Quantity */}
{showLoopSlider ? (
<FormInput label="Loop" htmlFor="loopQuantity">
<Slider
id="takeProfit"
value={selectedLoopQuantity}
onChange={(e: any) => setLoopQuantity(e.target.value)}
step="1"
min="1"
max="20"
></Slider>
))}
</select>
</FormInput>
) : null}
<div className="form-control">
<label htmlFor="save" className="label w-full cursor-pointer">
<span className="label mr-6">Save backtest</span>
<input
type="checkbox"
className="checkbox checkbox-primary"
{...register('save')}
/>
</label>
<div className="grid grid-cols-2 gap-4">
<FormInput label="Balance" htmlFor="balance">
<input
type="number"
className="input input-bordered w-full"
value={balance}
onChange={(e) => setBalance(Number(e.target.value))}
/>
</FormInput>
<FormInput label="Save" htmlFor="save">
<input
type="checkbox"
className="toggle toggle-primary"
{...register('save')}
/>
</FormInput>
</div>
<div className="grid grid-cols-2 gap-4">
<FormInput label="Start Date" htmlFor="startDate">
<input
type="date"
className="input input-bordered w-full"
value={startDate}
onChange={(e) => {
setStartDate(e.target.value);
setValue('startDate', e.target.value);
}}
/>
</FormInput>
<FormInput label="End Date" htmlFor="endDate">
<input
type="date"
className="input input-bordered w-full"
value={endDate}
onChange={(e) => {
setEndDate(e.target.value);
setValue('endDate', e.target.value);
}}
/>
</FormInput>
</div>
{showLoopSlider && (
<FormInput label="Loop" htmlFor="loop">
<Slider
id="loopSlider"
min="1"
max="10"
value={selectedLoopQuantity.toString()}
onChange={(e) => setLoopQuantity(Number(e.target.value))}
></Slider>
</FormInput>
)}
</div>
<div className="modal-action">
<button type="submit" className="btn">
Run
@@ -355,4 +396,5 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
</Modal>
)
}
export default BacktestModal

View File

@@ -1,17 +1,29 @@
import { TradeChart, CardPositionItem } from '..'
import { IBotRowDetails } from '../../../global/type'
import { Backtest, MoneyManagement } from '../../../generated/ManagingApi'
import { CardPosition, CardText } from '../../mollecules'
const BacktestRowDetails: React.FC<IBotRowDetails> = ({
candles,
positions,
walletBalances,
strategiesValues,
signals,
optimizedMoneyManagement,
statistics,
moneyManagement
interface IBacktestRowDetailsProps {
backtest: Backtest;
optimizedMoneyManagement: {
stopLoss: number;
takeProfit: number;
};
}
const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
backtest,
optimizedMoneyManagement
}) => {
const {
candles,
positions,
walletBalances,
strategiesValues,
signals,
statistics,
moneyManagement
} = backtest;
return (
<>
<div className="grid grid-flow-row">
@@ -66,7 +78,7 @@ const BacktestRowDetails: React.FC<IBotRowDetails> = ({
<CardText
title="Optimized Money Management"
content={
"SL: " +optimizedMoneyManagement?.stopLoss.toFixed(2) + "% TP: " + optimizedMoneyManagement?.takeProfit.toFixed(2) + "%"
"SL: " + backtest.optimizedMoneyManagement?.stopLoss.toFixed(2) + "% TP: " + backtest.optimizedMoneyManagement?.takeProfit.toFixed(2) + "%"
}
></CardText>

View File

@@ -3,39 +3,70 @@ import {
ChevronRightIcon,
PlayIcon,
TrashIcon,
EyeIcon
} from '@heroicons/react/solid'
import React, { useEffect, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import useApiUrlStore from '../../../app/store/apiStore'
import type {
Backtest,
MoneyManagement,
StartBotRequest,
Ticker,
} from '../../../generated/ManagingApi'
import { BacktestClient, BotClient } from '../../../generated/ManagingApi'
import { BacktestClient, BotClient, MoneyManagementClient } from '../../../generated/ManagingApi'
import type { IBacktestCards } from '../../../global/type'
import { Toast, SelectColumnFilter, Table, CardText } from '../../mollecules'
import BotNameModal from '../../mollecules/Modal/BotNameModal'
import BacktestRowDetails from './backtestRowDetails'
const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching }) => {
const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktests }) => {
const [rows, setRows] = useState<Backtest[]>([])
const { apiUrl } = useApiUrlStore()
const [optimizedMoneyManagement, setOptimizedMoneyManagement] = useState({
stopLoss: 0,
takeProfit: 0,
})
const [showBotNameModal, setShowBotNameModal] = useState(false)
const [isForWatchOnly, setIsForWatchOnly] = useState(false)
const [currentBacktest, setCurrentBacktest] = useState<Backtest | null>(null)
const [selectedMoneyManagement, setSelectedMoneyManagement] = useState<string>('')
async function runBot(backtest: Backtest, isForWatchOnly: boolean) {
// Fetch money managements
const { data: moneyManagements } = useQuery({
queryFn: async () => {
const moneyManagementClient = new MoneyManagementClient({}, apiUrl)
return await moneyManagementClient.moneyManagement_GetMoneyManagements()
},
queryKey: ['moneyManagements'],
})
// Set the first money management as default when the data is loaded
useEffect(() => {
if (moneyManagements && moneyManagements.length > 0) {
setSelectedMoneyManagement(moneyManagements[0].name)
}
}, [moneyManagements])
async function runBot(botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string) {
const t = new Toast('Bot is starting')
const client = new BotClient({}, apiUrl)
// Check if the money management name is "custom" or contains "custom"
const isCustomMoneyManagement =
!moneyManagementName ||
moneyManagementName.toLowerCase() === 'custom' ||
moneyManagementName.toLowerCase().includes('custom');
const request: StartBotRequest = {
accountName: backtest.accountName,
botName: backtest.ticker + '-' + backtest.timeframe.toString(),
botName: botName,
botType: backtest.botType,
isForWatchOnly: isForWatchOnly,
moneyManagementName: '',
// Only use the money management name if it's not a custom money management
moneyManagementName: isCustomMoneyManagement ? '' : moneyManagementName,
scenario: backtest.scenario,
ticker: backtest.ticker as Ticker,
timeframe: backtest.timeframe,
@@ -44,13 +75,28 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching }) => {
await client
.bot_Start(request)
.then((botStatus: string) => {
t.update('info', 'Bot status :' + botStatus)
t.update('info', 'Bot status: ' + botStatus)
})
.catch((err) => {
t.update('error', 'Error :' + err)
t.update('error', 'Error: ' + err)
})
}
const handleOpenBotNameModal = (backtest: Backtest, isForWatchOnly: boolean) => {
setCurrentBacktest(backtest)
setIsForWatchOnly(isForWatchOnly)
setShowBotNameModal(true)
}
const handleCloseBotNameModal = () => {
setShowBotNameModal(false)
}
const handleSubmitBotName = (botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string) => {
runBot(botName, backtest, isForWatchOnly, moneyManagementName)
setShowBotNameModal(false)
}
async function deleteBacktest(id: string) {
const t = new Toast('Deleting backtest')
const client = new BacktestClient({}, apiUrl)
@@ -59,6 +105,11 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching }) => {
.backtest_DeleteBacktest(id)
.then(() => {
t.update('success', 'Backtest deleted')
// Remove the deleted backtest from the list
if (list) {
const updatedList = list.filter(backtest => backtest.id !== id);
setBacktests(updatedList);
}
})
.catch((err) => {
t.update('error', err)
@@ -143,12 +194,6 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching }) => {
accessor: 'botType',
disableSortBy: true,
},
// {
// Filter: SelectColumnFilter,
// Header: 'Account',
// accessor: 'accountName',
// disableSortBy: true,
// },
],
},
{
@@ -223,40 +268,40 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching }) => {
accessor: 'id',
disableFilters: true,
},
// {
// Cell: ({ cell }) => (
// <>
// <div className="tooltip" data-tip="Run watcher">
// <button
// data-value={cell.row.values.name}
// onClick={() => runBot(cell.row.values, true)}
// >
// <EyeIcon className="text-primary w-4"></EyeIcon>
// </button>
// </div>
// </>
// ),
// Header: '',
// accessor: 'watcher',
// disableFilters: true,
// },
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Run in watch-only mode">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBotNameModal(cell.row.original as Backtest, true)}
>
<EyeIcon className="text-primary w-4"></EyeIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'watcher',
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Run bot">
<button
data-value={cell.row.values.name}
onClick={() => runBot(cell.row.values, false)}
onClick={() => handleOpenBotNameModal(cell.row.original as Backtest, false)}
>
<PlayIcon className="text-primary-focus w-4"></PlayIcon>
<PlayIcon className="text-primary w-4"></PlayIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'bot',
accessor: 'runner',
disableFilters: true,
},
}
],
},
],
@@ -264,76 +309,72 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching }) => {
)
useEffect(() => {
setRows(list!)
if (list!.length > 0) {
// const optimized = list![0].optimizedMoneyManagement
// setOptimizedMoneyManagement({
// stopLoss: optimized.stopLoss,
// takeProfit: optimized.takeProfit,
// })
// Get average optimized money management for every backtest
const optimized = list!.map((b) => b.optimizedMoneyManagement)
const stopLoss = optimized.reduce((acc, curr) => acc + (curr?.stopLoss ?? 0), 0)
const takeProfit = optimized.reduce(
(acc, curr) => acc + (curr?.takeProfit ?? 0),
0
)
setOptimizedMoneyManagement({
stopLoss: stopLoss / optimized.length,
takeProfit: takeProfit / optimized.length,
})
if (list) {
setRows(list)
// Calculate average optimized money management
if (list.length > 0) {
const optimized = list.map((b) => b.optimizedMoneyManagement);
const stopLoss = optimized.reduce((acc, curr) => acc + (curr?.stopLoss ?? 0), 0);
const takeProfit = optimized.reduce((acc, curr) => acc + (curr?.takeProfit ?? 0), 0);
setOptimizedMoneyManagement({
stopLoss: stopLoss / optimized.length,
takeProfit: takeProfit / optimized.length,
});
}
}
}, [list])
const renderRowSubComponent = React.useCallback(
({ row }: any) => (
<>
<BacktestRowDetails
candles={row.original.candles}
positions={row.original.positions}
walletBalances={row.original.walletBalances}
strategiesValues={row.original.strategiesValues}
signals={row.original.signals}
optimizedMoneyManagement={row.original.optimizedMoneyManagement}
statistics={row.original.statistics}
moneyManagement={row.original.moneyManagement}
></BacktestRowDetails>
</>
),
[]
)
return (
<div
className="flex flex-wrap"
style={{ display: 'flex', justifyContent: 'center', width: '110%' }}
>
<>
{isFetching ? (
<progress className="progress progress-primary w-56"></progress>
) : (<>
<div className='w-full'>
<CardText
title="Average Optimized Money Management"
content={
"SL: " +optimizedMoneyManagement.stopLoss.toFixed(2) + "% | TP: " + optimizedMoneyManagement.takeProfit.toFixed(2) + "% " +
"| R/R: " + (optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss).toFixed(2)
}
></CardText>
<div className="flex justify-center">
<progress className="progress progress-primary w-56"></progress>
</div>
<Table
columns={columns}
data={rows}
renderRowSubCompontent={renderRowSubComponent}
showPagination={true}
/>
) : (
<>
{list && list.length > 0 && (
<div className="mb-4">
<CardText
title="Average Optimized Money Management"
content={
"SL: " + optimizedMoneyManagement.stopLoss.toFixed(2) + "% | TP: " +
optimizedMoneyManagement.takeProfit.toFixed(2) + "% | R/R: " +
(optimizedMoneyManagement.takeProfit / optimizedMoneyManagement.stopLoss || 0).toFixed(2)
}
/>
</div>
)}
<Table
columns={columns}
data={rows}
renderRowSubCompontent={({ row }: any) => (
<BacktestRowDetails
backtest={row.original}
optimizedMoneyManagement={optimizedMoneyManagement}
/>
)}
/>
{showBotNameModal && currentBacktest && moneyManagements && (
<BotNameModal
showModal={showBotNameModal}
onClose={handleCloseBotNameModal}
backtest={currentBacktest}
isForWatchOnly={isForWatchOnly}
onSubmitBotName={(botName, backtest, isForWatchOnly, moneyManagementName) =>
handleSubmitBotName(botName, backtest, isForWatchOnly, moneyManagementName)
}
moneyManagements={moneyManagements}
selectedMoneyManagement={selectedMoneyManagement}
setSelectedMoneyManagement={setSelectedMoneyManagement}
/>
)}
</>
)}
</div>
</>
)
}

View File

@@ -556,7 +556,7 @@ const TradeChart = ({
paneCount++
}
if (walletBalances != null) {
if (walletBalances != null && walletBalances.length > 0) {
const walletSeries = chart.current.addBaselineSeries({
baseValue: {price: walletBalances[0].value, type: 'price'},
pane: paneCount,

View File

@@ -10,8 +10,8 @@ export const privyWagmiConfig = createConfig({
chains: [mainnet, arbitrum],
transports: {
// You can customize RPC providers here if needed
[mainnet.id]: http(`https://ethereum.publicnode.com`),
[arbitrum.id]: http(`https://arbitrum-one.publicnode.com`),
[mainnet.id]: http(),
[arbitrum.id]: http(),
},
})

View File

@@ -299,6 +299,44 @@ export class BacktestClient extends AuthorizedApiBase {
return Promise.resolve<FileResponse>(null as any);
}
backtest_Backtest(id: string): Promise<Backtest> {
let url_ = this.baseUrl + "/Backtest/{id}";
if (id === undefined || id === null)
throw new Error("The parameter 'id' must be defined.");
url_ = url_.replace("{id}", encodeURIComponent("" + id));
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBacktest_Backtest(_response);
});
}
protected processBacktest_Backtest(response: Response): Promise<Backtest> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Backtest;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<Backtest>(null as any);
}
backtest_DeleteBacktests(): Promise<FileResponse> {
let url_ = this.baseUrl + "/Backtest/deleteAll";
url_ = url_.replace(/[?&]$/, "");
@@ -339,7 +377,7 @@ export class BacktestClient extends AuthorizedApiBase {
return Promise.resolve<FileResponse>(null as any);
}
backtest_Run(accountName: string | null | undefined, botType: BotType | undefined, ticker: Ticker | undefined, scenarioName: string | null | undefined, timeframe: Timeframe | undefined, watchOnly: boolean | undefined, days: number | undefined, balance: number | undefined, moneyManagementName: string | null | undefined, save: boolean | undefined, moneyManagement: MoneyManagement | undefined): Promise<Backtest> {
backtest_Run(accountName: string | null | undefined, botType: BotType | undefined, ticker: Ticker | undefined, scenarioName: string | null | undefined, timeframe: Timeframe | undefined, watchOnly: boolean | undefined, balance: number | undefined, moneyManagementName: string | null | undefined, startDate: Date | undefined, endDate: Date | undefined, save: boolean | undefined, moneyManagement: MoneyManagement | undefined): Promise<Backtest> {
let url_ = this.baseUrl + "/Backtest/Run?";
if (accountName !== undefined && accountName !== null)
url_ += "accountName=" + encodeURIComponent("" + accountName) + "&";
@@ -361,16 +399,20 @@ export class BacktestClient extends AuthorizedApiBase {
throw new Error("The parameter 'watchOnly' cannot be null.");
else if (watchOnly !== undefined)
url_ += "watchOnly=" + encodeURIComponent("" + watchOnly) + "&";
if (days === null)
throw new Error("The parameter 'days' cannot be null.");
else if (days !== undefined)
url_ += "days=" + encodeURIComponent("" + days) + "&";
if (balance === null)
throw new Error("The parameter 'balance' cannot be null.");
else if (balance !== undefined)
url_ += "balance=" + encodeURIComponent("" + balance) + "&";
if (moneyManagementName !== undefined && moneyManagementName !== null)
url_ += "moneyManagementName=" + encodeURIComponent("" + moneyManagementName) + "&";
if (startDate === null)
throw new Error("The parameter 'startDate' cannot be null.");
else if (startDate !== undefined)
url_ += "startDate=" + encodeURIComponent(startDate ? "" + startDate.toISOString() : "") + "&";
if (endDate === null)
throw new Error("The parameter 'endDate' cannot be null.");
else if (endDate !== undefined)
url_ += "endDate=" + encodeURIComponent(endDate ? "" + endDate.toISOString() : "") + "&";
if (save === null)
throw new Error("The parameter 'save' cannot be null.");
else if (save !== undefined)
@@ -542,11 +584,11 @@ export class BotClient extends AuthorizedApiBase {
}
bot_StopAll(): Promise<string> {
let url_ = this.baseUrl + "/Bot/StopAll";
let url_ = this.baseUrl + "/Bot/stop-all";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
method: "POST",
headers: {
"Accept": "application/json"
}
@@ -618,11 +660,11 @@ export class BotClient extends AuthorizedApiBase {
}
bot_RestartAll(): Promise<string> {
let url_ = this.baseUrl + "/Bot/RestartAll";
let url_ = this.baseUrl + "/Bot/restart-all";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
method: "POST",
headers: {
"Accept": "application/json"
}
@@ -1488,6 +1530,41 @@ export class SettingsClient extends AuthorizedApiBase {
}
return Promise.resolve<boolean>(null as any);
}
settings_CreateDefaultConfiguration(): Promise<boolean> {
let url_ = this.baseUrl + "/Settings/create-default-config";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "POST",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processSettings_CreateDefaultConfiguration(_response);
});
}
protected processSettings_CreateDefaultConfiguration(response: Response): Promise<boolean> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as boolean;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<boolean>(null as any);
}
}
export class TradingClient extends AuthorizedApiBase {
@@ -1999,11 +2076,14 @@ export interface Backtest {
botType: BotType;
accountName: string;
candles: Candle[];
startDate: Date;
endDate: Date;
statistics: PerformanceMetrics;
fees: number;
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
optimizedMoneyManagement: MoneyManagement;
moneyManagement: MoneyManagement;
user: User;
strategiesValues: { [key in keyof typeof StrategyType]?: StrategiesResultBase; };
score: number;
}
@@ -2086,6 +2166,7 @@ export interface Position {
signalIdentifier?: string | null;
identifier: string;
initiator: PositionInitiator;
user?: User | null;
}
export enum TradeDirection {
@@ -2101,6 +2182,7 @@ export interface MoneyManagement {
stopLoss: number;
takeProfit: number;
leverage: number;
user?: User | null;
}
export enum Timeframe {
@@ -2189,6 +2271,7 @@ export interface Signal extends ValueObject {
exchange: TradingExchanges;
strategyType: StrategyType;
signalType: SignalType;
user?: User | null;
}
export enum SignalStatus {
@@ -2372,26 +2455,17 @@ export interface TradingBot {
signals: Signal[];
positions: Position[];
candles: Candle[];
riskLevel: RiskLevel;
winRate: number;
profitAndLoss: number;
timeframe: Timeframe;
ticker: Ticker;
scenario: string;
exchange: TradingExchanges;
isForWatchingOnly: boolean;
botType: BotType;
accountName: string;
moneyManagement: MoneyManagement;
}
export enum RiskLevel {
Low = "Low",
Medium = "Medium",
High = "High",
Adaptive = "Adaptive",
}
export interface SpotlightOverview {
spotlights: Spotlight[];
dateTime: Date;
@@ -2408,6 +2482,7 @@ export interface Scenario {
name?: string | null;
strategies?: Strategy[] | null;
loopbackPeriod?: number | null;
user?: User | null;
}
export interface Strategy {
@@ -2423,6 +2498,7 @@ export interface Strategy {
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
user?: User | null;
}
export interface TickerSignal {
@@ -2434,6 +2510,13 @@ export interface TickerSignal {
oneDay: Signal[];
}
export enum RiskLevel {
Low = "Low",
Medium = "Medium",
High = "High",
Adaptive = "Adaptive",
}
export interface LoginRequest {
name: string;
address: string;

View File

@@ -99,17 +99,18 @@ export type ISpotlightBadge = {
price?: number | undefined
}
export type IBacktestsFormInput = {
export type IBacktestsFormInput = {
accountName: string
tickers: string[]
botType: BotType
timeframe: Timeframe
scenarioName: string
days: number
save: boolean
balance: number
moneyManagement: MoneyManagement
loop: number
startDate: string
endDate: string
}
export type IBacktestCards = {
@@ -159,11 +160,12 @@ export type IBacktestFormInput = {
botType: BotType
ticker: Ticker
timeframe: Timeframe
days: number
save: boolean
scenarioName: string
balance: number
moneyManagement: MoneyManagement
startDate: string
endDate: string
}
export type IOpenPositionFormInput = {
@@ -219,7 +221,7 @@ export type IPropsComponent = {
value?: string | number
href?: string
as?: 'navlink' | 'link' | 'newtab'
className?: string | ((prop: = { isActive: boolean }) => string)
className?: string | ((prop: { isActive: boolean }) => string)
onClick?: () => void
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
id: string
@@ -239,7 +241,7 @@ export type IMyLinkProps = {
href: string
children: React.ReactNode
as?: 'navlink' | 'link' | 'newtab'
className?: string | ((prop: = { isActive: boolean }) => string)
className?: string | ((prop: { isActive: boolean }) => string)
onClick?: () => void
}

View File

@@ -29,7 +29,7 @@ function baseBadgeClass(isOutlined = false) {
function cardClasses(botStatus: string) {
const classes =
'card bg-base-300 shadow-md' + (botStatus == 'Up' ? ' shadow-primary' : '')
'card bg-base-300 shadow-md ' + (botStatus == 'Up' ? 'shadow-success' : '')
return classes
}
@@ -196,12 +196,6 @@ const BotList: React.FC<IBotList> = ({ list }) => {
content={bot.scenario}
></CardText>
</div>
<div>
<CardText
title="Exchange"
content={bot.exchange.toString()}
></CardText>
</div>
</div>
</div>
<div className="columns-2">

View File

@@ -0,0 +1,196 @@
import React, { useState } from 'react'
import type { SubmitHandler } from 'react-hook-form'
import { useForm } from 'react-hook-form'
import {
Account,
RiskLevel,
Ticker,
TradeDirection,
TradingClient,
} from '../../../generated/ManagingApi'
import Modal from '../../../components/mollecules/Modal/Modal'
import useApiUrlStore from '../../../app/store/apiStore'
import { FormInput, Toast } from '../../../components/mollecules'
import type { IOpenPositionFormInput } from '../../../global/type'
interface GmxPositionModalProps {
isOpen: boolean
onClose: () => void
account: Account
}
const GmxPositionModal: React.FC<GmxPositionModalProps> = ({
isOpen,
onClose,
account,
}) => {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const { apiUrl } = useApiUrlStore()
const client = new TradingClient({}, apiUrl)
const [selectedTicker, setSelectedTicker] = useState<Ticker>(Ticker.BTC)
const [selectedDirection, setSelectedDirection] = useState<TradeDirection>(
TradeDirection.Long
)
const { register, handleSubmit } = useForm<IOpenPositionFormInput>()
function setSelectedDirectionEvent(e: React.ChangeEvent<HTMLInputElement>) {
setSelectedDirection(e.target.value as TradeDirection)
}
function setSelectedTickerEvent(e: React.ChangeEvent<HTMLInputElement>) {
setSelectedTicker(e.target.value as Ticker)
}
const onSubmit: SubmitHandler<IOpenPositionFormInput> = async (form) => {
const t = new Toast(`Opening ${form.direction} ${form.ticker} on ${account.name}`)
setIsLoading(true)
setError(null)
try {
await client.trading_Trade(
account.name,
null, // moneyManagementName - assuming none selected
form.direction,
form.ticker,
form.riskLevel,
false, // isForPaperTrading - adjust if needed
form.stopLoss, // Pass stopLoss price to openPrice parameter
undefined // moneyManagement - not passing the full object
)
t.update('success', 'Position opened successfully')
onClose() // Close modal on success
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'An unknown error occurred'
setError(errorMessage)
t.update('error', `Error: ${errorMessage}`)
} finally {
setIsLoading(false)
}
}
const modalContent = (
<>
{isLoading ? (
<div className="text-center py-4">
<span className="loading loading-spinner loading-md"></span>
<p>Opening GMX position...</p>
</div>
) : error ? (
<div className="alert alert-error mb-4">
<p>{error}</p>
</div>
) : (
<>
<div className="mb-4">
<p className="mb-2">
<strong>Account:</strong> {account.name}
</p>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-wrap gap-4 mb-4">
<FormInput label="Direction" htmlFor="direction">
<select
className="select select-bordered w-full h-auto max-w-xs"
{...register('direction', {
onChange(e) {
setSelectedDirectionEvent(e)
},
value: selectedDirection,
})}
>
{Object.keys(TradeDirection).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
<FormInput label="Ticker" htmlFor="ticker">
<select
className="select select-bordered w-full h-auto max-w-xs"
{...register('ticker', {
onChange(e) {
setSelectedTickerEvent(e)
},
value: selectedTicker,
})}
>
{Object.keys(Ticker).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
<FormInput label="Risk" htmlFor="riskLevel">
<select
className="select select-bordered w-full h-auto max-w-xs"
{...register('riskLevel')}
>
{Object.keys(RiskLevel).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
<div className="collapse bg-base-200 w-full">
<input type="checkbox" />
<div className="collapse-title text-xs font-medium">
Add TP/SL
</div>
<div className="collapse-content flex flex-col gap-2">
<FormInput label="Stop Loss" htmlFor="stopLoss">
<input
type="number"
step="any"
placeholder="Enter SL price (used as openPrice)"
className="input input-bordered w-full h-auto max-w-xs"
{...register('stopLoss', { valueAsNumber: true })}
/>
</FormInput>
</div>
</div>
<button
type="submit"
className="btn btn-primary w-full mt-2"
disabled={isLoading}
>
{isLoading ? (
<span className="loading loading-spinner"></span>
) : (
`${selectedDirection} ${selectedTicker}`
)}
</button>
</div>
</form>
<div className="mt-4">
{/* Optionally keep or adjust info message */}
{/* <div className="alert alert-info"> */}
{/* <p className="text-sm"> */}
{/* <strong>Note:</strong> Ensure account has sufficient margin.*/}
{/* </p>*/}
{/* </div> */}
</div>
</>
)}
</>
)
return (
<Modal
showModal={isOpen}
onClose={onClose}
titleHeader="Open GMX Position"
>
{modalContent}
</Modal>
)
}
export default GmxPositionModal

View File

@@ -0,0 +1,465 @@
import React, { useState, useEffect } from 'react'
import { useDelegatedActions, usePrivy, useWallets, WalletWithMetadata } from '@privy-io/react-auth'
import { Account, AccountType } from '../../../generated/ManagingApi'
import canonicalize from 'canonicalize' // Support JSON canonicalization
import elliptic from 'elliptic' // Elliptic curve cryptography for browsers
import * as base64 from 'base64-js' // Base64 encoding/decoding
import Modal from '../../../components/mollecules/Modal/Modal'
// Replace with your app configuration - these should come from environment variables in a real app
const PRIVY_APP_ID = 'insert-your-app-id'
const PRIVY_APP_SECRET = 'insert-your-app-secret' // Don't hardcode in production!
const PRIVY_AUTHORIZATION_KEY = 'wallet-auth:insert-your-private-key-here' // Don't hardcode in production!
// Initialize elliptic curve - P-256 is the curve used by Privy (NIST P-256)
const EC = new elliptic.ec('p256')
interface PrivyDelegationModalProps {
isOpen: boolean
onClose: () => void
account: Account
}
// Use Web Crypto API to hash the message with SHA-256
async function sha256Hash(message: Uint8Array): Promise<Uint8Array> {
const hashBuffer = await crypto.subtle.digest('SHA-256', message)
return new Uint8Array(hashBuffer)
}
async function generateAuthorizationSignature({url, body}: {url: string; body: object}) {
try {
// First, canonicalize the request body - this is crucial for consistent signatures
const canonicalizedBody = JSON.parse(canonicalize(body) as string);
// Create the payload object that follows Privy's specification
const payload = {
version: 1,
method: 'POST',
url,
body: canonicalizedBody, // Use the canonicalized body here
headers: {
'privy-app-id': PRIVY_APP_ID
}
};
// JSON-canonicalize the payload to ensure deterministic serialization
const serializedPayload = canonicalize(payload) as string;
console.log('Canonicalized payload:', serializedPayload);
// Extract private key from the authorization key by removing the prefix
const privateKeyBase64 = PRIVY_AUTHORIZATION_KEY.replace('wallet-auth:', '');
// Decode the base64 private key to get the raw bytes
const privateKeyBytes = base64.toByteArray(privateKeyBase64);
// Import the private key into elliptic
const privateKey = EC.keyFromPrivate(privateKeyBytes);
// Convert the serialized payload to UTF-8 bytes
const messageBytes = new TextEncoder().encode(serializedPayload);
// Hash the message using SHA-256 with Web Crypto API
const messageHash = await sha256Hash(messageBytes);
// Sign the hash with the private key - use Uint8Array instead of Buffer
// Using the array directly instead of creating a Buffer
const signature = privateKey.sign(Array.from(messageHash));
// Convert signature to the DER format and then to base64
const signatureDer = signature.toDER();
const signatureBase64 = base64.fromByteArray(new Uint8Array(signatureDer));
return {
payload: serializedPayload,
signature: signatureBase64,
body: canonicalizedBody, // Return the canonicalized body
canonicalizedBodyString: canonicalize(canonicalizedBody) as string // Return the string form too
};
} catch (err) {
console.error('Error generating signature:', err);
throw err;
}
}
// Function to send the actual request with the generated signature
async function sendPrivyRequest(url: string, body: any, signature: string): Promise<any> {
try {
// Create basic auth token for Privy API
const authToken = btoa(`${PRIVY_APP_ID}:${PRIVY_APP_SECRET}`);
// Set up the request headers
const headers = {
'Authorization': `Basic ${authToken}`,
'privy-app-id': PRIVY_APP_ID,
'privy-authorization-signature': signature,
'Content-Type': 'application/json'
};
console.log('Sending request with headers:', headers);
console.log('Body:', body);
// Make the API call
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: body // Use the exact same canonicalized body string
});
// Get the response body
const responseText = await response.text();
// Try to parse as JSON if possible
try {
const responseJson = JSON.parse(responseText);
if (!response.ok) {
throw new Error(`API error (${response.status}): ${JSON.stringify(responseJson)}`);
}
return responseJson;
} catch (parseError) {
// If not valid JSON, return as text
if (!response.ok) {
throw new Error(`API error (${response.status}): ${responseText}`);
}
return { text: responseText };
}
} catch (error) {
console.error('Error sending Privy request:', error);
throw error;
}
}
const PrivyDelegationModal: React.FC<PrivyDelegationModalProps> = ({
isOpen,
onClose,
account,
}) => {
const [isDelegated, setIsDelegated] = useState<boolean>(false)
const [isLoading, setIsLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)
const [signatureResult, setSignatureResult] = useState<{
payload: string,
signature: string,
body?: any,
canonicalizedBodyString?: string
} | null>(null)
const [isSigningMessage, setIsSigningMessage] = useState<boolean>(false)
const [isSendingRequest, setIsSendingRequest] = useState<boolean>(false)
const [messageToSign, setMessageToSign] = useState<string>('Hello, Ethereum')
const [apiResponse, setApiResponse] = useState<any>(null)
const { delegateWallet, revokeWallets } = useDelegatedActions()
const { user } = usePrivy()
const { ready, wallets } = useWallets()
// Find the embedded wallet to delegate from the array of the user's wallets
const walletToDelegate = wallets.find(
(wallet) => wallet.walletClientType === 'privy' && wallet.address === account.key
)
useEffect(() => {
if (user && ready) {
checkDelegationStatus()
}
}, [user, ready])
const checkDelegationStatus = () => {
setIsLoading(true)
setError(null)
try {
// Check if the wallet is already delegated by inspecting the user's linked accounts
const delegatedWallet = user?.linkedAccounts.find(
(linkedAccount): linkedAccount is WalletWithMetadata =>
linkedAccount.type === 'wallet' &&
linkedAccount.delegated &&
linkedAccount.address.toLowerCase() === account.key?.toLowerCase()
)
setIsDelegated(!!delegatedWallet)
} catch (err: any) {
console.error('Error checking delegation status:', err)
setError(`Failed to check delegation status: ${err.message}`)
} finally {
setIsLoading(false)
}
}
const handleRequestDelegation = async () => {
try {
setIsLoading(true)
setError(null)
if (!walletToDelegate) {
throw new Error('No embedded wallet found to delegate')
}
// Fixes the linter error by using the correct parameter type
await delegateWallet({
address: walletToDelegate.address,
chainType: 'ethereum'
})
setIsDelegated(true)
} catch (err: any) {
console.error('Error requesting delegation:', err)
setError(`Failed to request delegation: ${err.message}`)
} finally {
setIsLoading(false)
}
}
const handleRevokeDelegation = async () => {
try {
setIsLoading(true)
setError(null)
if (!walletToDelegate) {
throw new Error('No embedded wallet found to revoke delegation')
}
// Fix for linter error - revokeWallets doesn't take parameters
await revokeWallets()
setIsDelegated(false)
} catch (err: any) {
console.error('Error revoking delegation:', err)
setError(`Failed to revoke delegation: ${err.message}`)
} finally {
setIsLoading(false)
}
}
const handleSignMessage = async () => {
try {
setIsSigningMessage(true)
setSignatureResult(null)
setApiResponse(null)
setError(null)
if (!walletToDelegate) {
throw new Error('No embedded wallet found to sign with')
}
// Prepare the request body
const body = {
address: walletToDelegate.address,
chain_type: 'ethereum',
method: 'personal_sign',
params: {
message: messageToSign,
encoding: 'utf-8'
}
}
// Generate the signature using browser crypto
const result = await generateAuthorizationSignature({
url: 'https://auth.privy.io/api/v1/wallets/rpc',
body
})
setSignatureResult(result)
// For debugging in the console
console.log("Generated Authorization Signature:", result.signature);
console.log("Payload:", result.payload);
console.log("Body:", result.body);
console.log("Canonicalized Body String:", result.canonicalizedBodyString);
// Create a curl command that could be used to make the request
// Note: We're using the canonicalized body from the result to ensure it matches what was signed
const curlCommand = `curl --request POST https://auth.privy.io/api/v1/wallets/rpc \\
-u "${PRIVY_APP_ID}:YOUR_APP_SECRET" \\
-H "privy-app-id: ${PRIVY_APP_ID}" \\
-H "privy-authorization-signature: ${result.signature}" \\
-H 'Content-Type: application/json' \\
-d '${result.canonicalizedBodyString}'`;
console.log("Curl command for testing:");
console.log(curlCommand);
} catch (err: any) {
console.error('Error signing message:', err)
setError(`Failed to sign message: ${err.message}`)
} finally {
setIsSigningMessage(false)
}
}
const handleSendRequest = async () => {
if (!signatureResult?.canonicalizedBodyString || !signatureResult?.signature) {
setError('No signature available. Please generate a signature first.');
return;
}
try {
setIsSendingRequest(true);
setApiResponse(null);
setError(null);
// Send the actual request using the same canonicalized body that was used for signature
const response = await sendPrivyRequest(
'https://auth.privy.io/api/v1/wallets/rpc',
signatureResult.canonicalizedBodyString,
signatureResult.signature
);
setApiResponse(response);
console.log('API Response:', response);
} catch (err: any) {
console.error('Error sending request:', err);
setError(`Failed to send request: ${err.message}`);
} finally {
setIsSendingRequest(false);
}
};
// Modal content to be passed to the Modal component
const modalContent = (
<>
{isLoading ? (
<div className="text-center py-4">
<span className="loading loading-spinner loading-md"></span>
<p>Checking delegation status...</p>
</div>
) : error ? (
<div className="alert alert-error mb-4">
<p>{error}</p>
</div>
) : (
<>
<div className="mb-4">
<p className="mb-2">
<strong>Wallet Address:</strong> {account.key}
</p>
<p className="mb-2">
<strong>Delegation Status:</strong>{' '}
<span className={isDelegated ? 'text-success' : 'text-error'}>
{isDelegated ? 'Enabled' : 'Disabled'}
</span>
</p>
</div>
<div className="flex justify-center mb-4">
{isDelegated ? (
<button
onClick={handleRevokeDelegation}
className="btn btn-error"
disabled={isLoading}
>
Revoke Delegation
</button>
) : (
<button
onClick={handleRequestDelegation}
className="btn btn-primary"
disabled={isLoading}
>
Enable Delegation
</button>
)}
</div>
<div className="divider">Message Signing</div>
<div className="mb-4">
<label className="form-control w-full">
<div className="label">
<span className="label-text">Message to sign</span>
</div>
<input
type="text"
value={messageToSign}
onChange={(e) => setMessageToSign(e.target.value)}
className="input input-bordered w-full"
disabled={isSigningMessage}
/>
</label>
</div>
<div className="flex flex-col gap-2 mb-4">
<button
onClick={handleSignMessage}
className="btn btn-secondary"
disabled={isSigningMessage || !messageToSign}
>
{isSigningMessage ? (
<>
<span className="loading loading-spinner loading-sm"></span>
Generating Signature...
</>
) : (
'Generate Signature'
)}
</button>
{signatureResult && (
<button
onClick={handleSendRequest}
className="btn btn-primary"
disabled={isSendingRequest || !signatureResult}
>
{isSendingRequest ? (
<>
<span className="loading loading-spinner loading-sm"></span>
Sending Request...
</>
) : (
'Send Request Using Signature'
)}
</button>
)}
</div>
{signatureResult && (
<div className="mb-4">
<div className="label">
<span className="label-text font-bold">Generated Signature:</span>
</div>
<div className="bg-gray-100 p-2 rounded overflow-x-auto">
<pre className="text-xs break-all whitespace-pre-wrap">{signatureResult.signature}</pre>
</div>
</div>
)}
{apiResponse && (
<div className="mb-4">
<div className="label">
<span className="label-text font-bold">API Response:</span>
</div>
<div className="bg-gray-100 p-2 rounded overflow-x-auto">
<pre className="text-xs break-all whitespace-pre-wrap">
{JSON.stringify(apiResponse, null, 2)}
</pre>
</div>
</div>
)}
<div className="mt-4">
<div className="alert alert-info">
<p className="text-sm">
<strong>Note:</strong> This component ensures that the exact same canonicalized payload is used
for both signature generation and request sending, maintaining signature integrity.
</p>
</div>
</div>
</>
)}
</>
)
return (
<Modal
showModal={isOpen}
onClose={onClose}
titleHeader="Wallet Delegation"
>
{modalContent}
</Modal>
)
}
export default PrivyDelegationModal

View File

@@ -5,6 +5,8 @@ import {
TrashIcon,
} from '@heroicons/react/solid'
import React, { useEffect, useState, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import { FiPlus, FiTrash2, FiKey, FiTrendingUp } from 'react-icons/fi'
import useApiUrlStore from '../../../app/store/apiStore'
import {
@@ -13,9 +15,11 @@ import {
Toast,
} from '../../../components/mollecules'
import type { Account } from '../../../generated/ManagingApi'
import { AccountClient } from '../../../generated/ManagingApi'
import { AccountClient, AccountType } from '../../../generated/ManagingApi'
import AccountRowDetails from './accountRowDetails'
import PrivyDelegationModal from './PrivyDelegationModal'
import GmxPositionModal from './GmxPositionModal'
interface IAccountList {
list: Account[]
@@ -25,6 +29,10 @@ interface IAccountList {
const AccountTable: React.FC<IAccountList> = ({ list, isFetching }) => {
const [rows, setRows] = useState<Account[]>([])
const { apiUrl } = useApiUrlStore()
const navigate = useNavigate()
const [modalAccount, setModalAccount] = useState<Account | null>(null)
const [isDelegationModalOpen, setIsDelegationModalOpen] = useState(false)
const [isGmxPositionModalOpen, setIsGmxPositionModalOpen] = useState(false)
async function deleteAcount(accountName: string) {
const t = new Toast('Deleting money management')
@@ -93,28 +101,46 @@ const AccountTable: React.FC<IAccountList> = ({ list, isFetching }) => {
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Delete account">
Cell: ({ row }: any) => {
const account = row.original
return (
<div className="flex space-x-2">
<button
data-value={cell.row.values.name}
onClick={() => deleteAcount(cell.row.values.name)}
className="btn btn-sm btn-error btn-outline"
onClick={() => deleteAcount(account.name)}
>
<TrashIcon className="text-accent w-4"></TrashIcon>
<FiTrash2 />
</button>
{account.type === AccountType.Privy && (
<button
className="btn btn-sm btn-primary btn-outline"
onClick={() => {
setModalAccount(account)
setIsDelegationModalOpen(true)
}}
>
<FiKey />
<span className="ml-1">Delegate</span>
</button>
)}
{account.type === AccountType.Privy && (
<button
className="btn btn-sm btn-success btn-outline"
onClick={() => {
setModalAccount(account)
setIsGmxPositionModalOpen(true)
}}
>
<FiTrendingUp />
<span className="ml-1">GMX</span>
</button>
)}
</div>
<div className="tooltip" data-tip="Copy Key">
<button
data-value={cell.row.values.name}
onClick={() => {
navigator.clipboard.writeText(cell.row.values.name)
}}
>
<ClipboardCopyIcon className="text-accent w-4"></ClipboardCopyIcon>
</button>
</div>
</>
),
)
},
Header: 'Actions',
accessor: 'id',
disableFilters: true,
@@ -128,18 +154,21 @@ const AccountTable: React.FC<IAccountList> = ({ list, isFetching }) => {
}, [list])
const renderRowSubComponent = React.useCallback(
({ row }: any) => (
<>
{row.original.balances != undefined ? (
<AccountRowDetails
showTotal={true}
balances={row.original.balances}
></AccountRowDetails>
) : (
<div>No balances</div>
)}
</>
),
({ row }: { row: any }) => {
const { balances } = row.original
return (
<>
{balances && balances.length > 0 ? (
<AccountRowDetails
balances={balances}
showTotal={true}
></AccountRowDetails>
) : (
<div>No balances</div>
)}
</>
)
},
[]
)
@@ -156,6 +185,27 @@ const AccountTable: React.FC<IAccountList> = ({ list, isFetching }) => {
renderRowSubCompontent={renderRowSubComponent}
/>
)}
{modalAccount && (
<>
<PrivyDelegationModal
isOpen={isDelegationModalOpen}
onClose={() => {
setIsDelegationModalOpen(false)
setModalAccount(null)
}}
account={modalAccount}
/>
<GmxPositionModal
isOpen={isGmxPositionModalOpen}
onClose={() => {
setIsGmxPositionModalOpen(false)
setModalAccount(null)
}}
account={modalAccount}
/>
</>
)}
</>
)
}

View File

@@ -0,0 +1,115 @@
import React, { useState } from 'react'
import { toast } from 'react-toastify'
import { SettingsClient } from '../../../generated/ManagingApi'
import useApiUrlStore from '../../../app/store/apiStore'
import { GmxSdk } from '@gmx-io/sdk'
import { createClient, createTestClient, createWalletClient, http, publicActions, walletActions } from 'viem'
import { getClient, getConnectorClient } from '@wagmi/core'
import { privyWagmiConfig } from '../../../config/privy'
import { arbitrum } from 'viem/chains'
import { GmxSdkConfig } from '@gmx-io/sdk/build/src/types/sdk'
const DefaultConfig: React.FC = () => {
const [isCreating, setIsCreating] = useState(false)
const { apiUrl } = useApiUrlStore()
const client = createWalletClient({
account: "0x9f7198eb1b9Ccc0Eb7A07eD228d8FbC12963ea33",
chain: arbitrum,
transport: http(),
})
const testClient = createTestClient({
chain: arbitrum,
mode: "hardhat",
transport: http(),
})
.extend(publicActions)
.extend(walletActions);
const arbitrumSdkConfig: GmxSdkConfig = {
chainId: arbitrum.id,
account: "0x9f7198eb1b9Ccc0Eb7A07eD228d8FbC12963ea33",
oracleUrl: "https://arbitrum-api.gmxinfra.io",
rpcUrl: "https://arb1.arbitrum.io/rpc",
walletClient: client,
subsquidUrl: "https://gmx.squids.live/gmx-synthetics-arbitrum:live/api/graphql",
subgraphUrl: "https://subgraph.satsuma-prod.com/3b2ced13c8d9/gmx/synthetics-arbitrum-stats/api",
};
const sdk = new GmxSdk(arbitrumSdkConfig)
console.log(sdk)
const createDefaultConfig = async () => {
try {
const client = new SettingsClient({}, apiUrl)
setIsCreating(true)
const response = await client.settings_CreateDefaultConfiguration()
.then((response) => {
if (response) {
toast.success('Default configuration created successfully!')
} else {
toast.error(`Failed to create default configuration`)
}
})
} catch (error) {
toast.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
} finally {
setIsCreating(false)
}
}
return (
<div className="max-w-3xl mx-auto p-4">
<div className="card bg-base-100 shadow-xl p-6">
<h2 className="text-2xl font-bold mb-4">Default Configuration</h2>
<div className="mb-6">
<p className="mb-2">Create a default configuration to get started quickly with backtesting:</p>
<div className="bg-base-200 p-4 rounded-lg mb-4">
<h3 className="text-lg font-semibold mb-2">Money Management:</h3>
<ul className="list-disc pl-6 mb-2">
<li>Name: Default Hourly</li>
<li>Timeframe: 1 Hour</li>
<li>Balance at Risk: 25%</li>
<li>Stop Loss: 2%</li>
<li>Take Profit: 4%</li>
<li>Leverage: 1x</li>
</ul>
<h3 className="text-lg font-semibold mb-2">Strategy:</h3>
<ul className="list-disc pl-6 mb-2">
<li>Type: StcTrend</li>
<li>Name: Default StcTrend</li>
<li>SignalType: Both</li>
<li>FastPeriods: 23</li>
<li>SlowPeriods: 50</li>
<li>StochPeriods: 10</li>
</ul>
<h3 className="text-lg font-semibold mb-2">Scenario:</h3>
<ul className="list-disc pl-6">
<li>Name: Default Scenario</li>
<li>Contains: Default StcTrend strategy</li>
</ul>
</div>
</div>
<div className="flex justify-center">
<button
onClick={createDefaultConfig}
disabled={isCreating}
className="btn btn-primary"
>
{isCreating ? 'Creating...' : 'Create Default Configuration'}
</button>
</div>
</div>
</div>
)
}
export default DefaultConfig

View File

@@ -5,6 +5,7 @@ import { Tabs } from '../../components/mollecules'
import AccountSettings from './account/accountSettings'
import MoneyManagementSettings from './moneymanagement/moneyManagement'
import Theme from './theme'
import DefaultConfig from './defaultConfig/defaultConfig'
type TabsType = {
label: string
@@ -29,6 +30,11 @@ const tabs: TabsType = [
index: 3,
label: 'Theme',
},
{
Component: DefaultConfig,
index: 4,
label: 'Quick Start Config',
},
]
const Settings: React.FC = () => {