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:
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user