+
= ({ list, setBacktests }) => {
React.useState(false)
const [selectedMoneyManagement, setSelectedMoneyManagement] =
React.useState()
+ const [showBotNameModal, setShowBotNameModal] = useState(false)
+ const [isForWatchOnly, setIsForWatchOnly] = useState(false)
+ const [currentBacktest, setCurrentBacktest] = useState(null)
+ const [selectedMoneyManagementName, setSelectedMoneyManagementName] = useState('')
- 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) {
+ setSelectedMoneyManagementName(moneyManagements[0].name)
+ }
+ }, [moneyManagements])
+
+ async function runBot(botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) {
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.config.accountName,
- name: backtest.config.ticker + '-' + backtest.config.timeframe?.toString(),
- botType: BotType.ScalpingBot,
+ name: botName,
+ botType: backtest.config.botType,
isForWatchOnly: isForWatchOnly,
- moneyManagementName: backtest.config.moneyManagement?.name,
+ // Only use the money management name if it's not a custom money management, otherwise use optimized
+ moneyManagementName: isCustomMoneyManagement ?
+ (backtest.optimizedMoneyManagement?.name || backtest.config.moneyManagement?.name) :
+ moneyManagementName,
scenario: backtest.config.scenarioName,
ticker: backtest.config.ticker as Ticker,
timeframe: backtest.config.timeframe,
- initialTradingBalance: 1000,
+ initialTradingBalance: initialTradingBalance,
+ cooldownPeriod: backtest.config.cooldownPeriod,
+ maxLossStreak: backtest.config.maxLossStreak,
+ maxPositionTimeHours: backtest.config.maxPositionTimeHours,
+ flipOnlyWhenInProfit: backtest.config.flipOnlyWhenInProfit
}
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, initialTradingBalance: number) => {
+ runBot(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
+ setShowBotNameModal(false)
+ }
+
async function runOptimizedBacktest(backtest: Backtest) {
const t = new Toast('Optimized backtest is running')
const client = new BacktestClient({}, apiUrl)
@@ -85,23 +142,26 @@ const BacktestCards: React.FC = ({ list, setBacktests }) => {
const startDate = backtest.candles[0].date
const endDate = backtest.candles[backtest.candles.length - 1].date
+ // Create optimized backtest config
+ const optimizedConfig: TradingBotConfig = {
+ ...backtest.config,
+ name: `${backtest.config.ticker}-${backtest.config.scenarioName}-Optimized`,
+ moneyManagement: backtest.optimizedMoneyManagement || backtest.config.moneyManagement
+ }
+
+ const request: RunBacktestRequest = {
+ config: optimizedConfig,
+ startDate: startDate,
+ endDate: endDate,
+ balance: backtest.walletBalances[0].value,
+ watchOnly: false,
+ save: false,
+ moneyManagementName: undefined, // We're passing the moneyManagement object directly
+ moneyManagement: optimizedConfig.moneyManagement
+ }
+
await client
- .backtest_Run(
- backtest.config.accountName,
- backtest.config.botType,
- backtest.config.ticker as Ticker,
- backtest.config.scenarioName,
- backtest.config.timeframe,
- false, // watchOnly
- backtest.walletBalances[0].value, // balance
- '', // moneyManagementName (empty since we're passing the optimized moneyManagement object)
- startDate, // startDate
- endDate, // endDate
- false, // save
- backtest.config.cooldownPeriod,
- backtest.config.maxLossStreak,
- backtest.config.moneyManagement as MoneyManagement, // moneyManagement object
- )
+ .backtest_Run(request)
.then((backtest: Backtest) => {
t.update('success', `${backtest.config.ticker} Backtest Succeeded`)
setBacktests((arr) => [...arr, backtest])
@@ -162,7 +222,7 @@ const BacktestCards: React.FC = ({ list, setBacktests }) => {
@@ -170,7 +230,7 @@ const BacktestCards: React.FC = ({ list, setBacktests }) => {
@@ -303,6 +363,21 @@ const BacktestCards: React.FC = ({ list, setBacktests }) => {
moneyManagement={selectedMoneyManagement}
onClose={() => setShowMoneyManagementModal(false)}
/>
+
+ {showBotNameModal && currentBacktest && moneyManagements && (
+
+ handleSubmitBotName(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
+ }
+ moneyManagements={moneyManagements}
+ selectedMoneyManagement={selectedMoneyManagementName}
+ setSelectedMoneyManagement={setSelectedMoneyManagementName}
+ />
+ )}
)
}
diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx
index 40a25b0..2a2d582 100644
--- a/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx
+++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx
@@ -4,15 +4,17 @@ import {type SubmitHandler, useForm} from 'react-hook-form'
import useApiUrlStore from '../../../app/store/apiStore'
import {
- AccountClient,
- BacktestClient,
- BotType,
- DataClient,
- MoneyManagement,
- MoneyManagementClient,
- ScenarioClient,
- Ticker,
- Timeframe,
+ AccountClient,
+ BacktestClient,
+ BotType,
+ DataClient,
+ MoneyManagement,
+ MoneyManagementClient,
+ RunBacktestRequest,
+ ScenarioClient,
+ Ticker,
+ Timeframe,
+ TradingBotConfig,
} from '../../../generated/ManagingApi'
import type {BacktestModalProps, IBacktestsFormInput,} from '../../../global/type'
import {Loader, Slider} from '../../atoms'
@@ -42,8 +44,10 @@ const BacktestModal: React.FC
= ({
defaultValues: {
startDate: defaultStartDateString,
endDate: defaultEndDateString,
- cooldownPeriod: 1, // Default cooldown period of 1 minute
- maxLossStreak: 0 // Default max loss streak of 0 (no limit)
+ cooldownPeriod: 10, // Default cooldown period of 10 minutes
+ maxLossStreak: 0, // Default max loss streak of 0 (no limit)
+ maxPositionTimeHours: null, // Default to null (disabled)
+ flipOnlyWhenInProfit: true // Default to true
}
});
const [selectedAccount, setSelectedAccount] = useState('')
@@ -100,28 +104,48 @@ const BacktestModal: React.FC = ({
loopCount: number
): Promise {
const t = new Toast(ticker + ' is running')
- // Use the name of the money management strategy if custom is not provided
- const moneyManagementName = customMoneyManagement ? undefined : selectedMoneyManagement
console.log(customMoneyManagement)
try {
- const backtest = await backtestClient.backtest_Run(
- form.accountName,
- form.botType,
- ticker as Ticker,
- scenarioName,
- form.timeframe,
- false, // watchOnly
- balance,
- moneyManagementName,
- new Date(form.startDate), // startDate
- new Date(form.endDate), // endDate
- form.save,
- form.cooldownPeriod, // Use the cooldown period from the form
- form.maxLossStreak, // Add the max loss streak parameter
- customMoneyManagement
- );
+ // Create the TradingBotConfig
+ const tradingBotConfig: TradingBotConfig = {
+ accountName: form.accountName,
+ ticker: ticker as Ticker,
+ scenarioName: scenarioName,
+ timeframe: form.timeframe,
+ botType: form.botType,
+ isForWatchingOnly: false, // Always false for backtests
+ isForBacktest: true, // Always true for backtests
+ cooldownPeriod: form.cooldownPeriod || 1,
+ maxLossStreak: form.maxLossStreak || 0,
+ maxPositionTimeHours: form.maxPositionTimeHours || null,
+ flipOnlyWhenInProfit: form.flipOnlyWhenInProfit ?? true,
+ flipPosition: form.botType === BotType.FlippingBot, // Set based on bot type
+ name: `Backtest-${scenarioName}-${ticker}-${new Date().toISOString()}`,
+ botTradingBalance: 0, // Will be set in the request
+ moneyManagement: customMoneyManagement || moneyManagements?.find(m => m.name === selectedMoneyManagement) || moneyManagements?.[0] || {
+ name: 'placeholder',
+ leverage: 1,
+ stopLoss: 0.01,
+ takeProfit: 0.02,
+ timeframe: form.timeframe
+ }
+ };
+
+ // Create the RunBacktestRequest
+ const request: RunBacktestRequest = {
+ config: tradingBotConfig,
+ startDate: new Date(form.startDate),
+ endDate: new Date(form.endDate),
+ balance: balance,
+ watchOnly: false,
+ save: form.save || false,
+ moneyManagementName: customMoneyManagement ? undefined : selectedMoneyManagement,
+ moneyManagement: customMoneyManagement
+ };
+
+ const backtest = await backtestClient.backtest_Run(request);
t.update('success', `${backtest.config.ticker} Backtest Succeeded`)
setBacktests((arr) => [...arr, backtest])
@@ -228,64 +252,79 @@ const BacktestModal: React.FC = ({
titleHeader="Run Backtest"
>
-
-
) : null}
diff --git a/src/Managing.WebApp/src/components/organism/index.tsx b/src/Managing.WebApp/src/components/organism/index.tsx
index f14abd9..a123f7b 100644
--- a/src/Managing.WebApp/src/components/organism/index.tsx
+++ b/src/Managing.WebApp/src/components/organism/index.tsx
@@ -9,3 +9,4 @@ export { default as StatusBadge } from './StatusBadge/StatusBadge'
export { default as PositionsList } from './Positions/PositionList'
export { default as WorkflowCanvas } from './Workflow/workflowCanvas'
export { default as ScenarioModal } from './ScenarioModal'
+export { default as BotNameModal } from './BotNameModal/BotNameModal'
diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts
index 0b47e5f..f53309a 100644
--- a/src/Managing.WebApp/src/generated/ManagingApi.ts
+++ b/src/Managing.WebApp/src/generated/ManagingApi.ts
@@ -337,57 +337,11 @@ export class BacktestClient extends AuthorizedApiBase {
return Promise.resolve(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, balance: number | undefined, moneyManagementName: string | null | undefined, startDate: Date | undefined, endDate: Date | undefined, save: boolean | undefined, cooldownPeriod: number | undefined, maxLossStreak: number | undefined, moneyManagement: MoneyManagement | undefined): Promise {
- let url_ = this.baseUrl + "/Backtest/Run?";
- if (accountName !== undefined && accountName !== null)
- url_ += "accountName=" + encodeURIComponent("" + accountName) + "&";
- if (botType === null)
- throw new Error("The parameter 'botType' cannot be null.");
- else if (botType !== undefined)
- url_ += "botType=" + encodeURIComponent("" + botType) + "&";
- if (ticker === null)
- throw new Error("The parameter 'ticker' cannot be null.");
- else if (ticker !== undefined)
- url_ += "ticker=" + encodeURIComponent("" + ticker) + "&";
- if (scenarioName !== undefined && scenarioName !== null)
- url_ += "scenarioName=" + encodeURIComponent("" + scenarioName) + "&";
- if (timeframe === null)
- throw new Error("The parameter 'timeframe' cannot be null.");
- else if (timeframe !== undefined)
- url_ += "timeframe=" + encodeURIComponent("" + timeframe) + "&";
- if (watchOnly === null)
- throw new Error("The parameter 'watchOnly' cannot be null.");
- else if (watchOnly !== undefined)
- url_ += "watchOnly=" + encodeURIComponent("" + watchOnly) + "&";
- 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)
- url_ += "save=" + encodeURIComponent("" + save) + "&";
- if (cooldownPeriod === null)
- throw new Error("The parameter 'cooldownPeriod' cannot be null.");
- else if (cooldownPeriod !== undefined)
- url_ += "cooldownPeriod=" + encodeURIComponent("" + cooldownPeriod) + "&";
- if (maxLossStreak === null)
- throw new Error("The parameter 'maxLossStreak' cannot be null.");
- else if (maxLossStreak !== undefined)
- url_ += "maxLossStreak=" + encodeURIComponent("" + maxLossStreak) + "&";
+ backtest_Run(request: RunBacktestRequest): Promise {
+ let url_ = this.baseUrl + "/Backtest/Run";
url_ = url_.replace(/[?&]$/, "");
- const content_ = JSON.stringify(moneyManagement);
+ const content_ = JSON.stringify(request);
let options_: RequestInit = {
body: content_,
@@ -811,6 +765,45 @@ export class BotClient extends AuthorizedApiBase {
}
return Promise.resolve(null as any);
}
+
+ bot_UpdateBotConfig(request: UpdateBotConfigRequest): Promise {
+ let url_ = this.baseUrl + "/Bot/UpdateConfig";
+ url_ = url_.replace(/[?&]$/, "");
+
+ const content_ = JSON.stringify(request);
+
+ let options_: RequestInit = {
+ body: content_,
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "application/json"
+ }
+ };
+
+ return this.transformOptions(options_).then(transformedOptions_ => {
+ return this.http.fetch(url_, transformedOptions_);
+ }).then((_response: Response) => {
+ return this.processBot_UpdateBotConfig(_response);
+ });
+ }
+
+ protected processBot_UpdateBotConfig(response: Response): Promise {
+ 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 string;
+ 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(null as any);
+ }
}
export class DataClient extends AuthorizedApiBase {
@@ -2705,6 +2698,8 @@ export interface TradingBotConfig {
maxLossStreak: number;
flipPosition: boolean;
name: string;
+ maxPositionTimeHours?: number | null;
+ flipOnlyWhenInProfit: boolean;
}
export interface MoneyManagement {
@@ -3106,6 +3101,17 @@ export interface SuperTrendResult extends ResultBase {
lowerBand?: number | null;
}
+export interface RunBacktestRequest {
+ config?: TradingBotConfig | null;
+ startDate?: Date;
+ endDate?: Date;
+ balance?: number;
+ watchOnly?: boolean;
+ save?: boolean;
+ moneyManagementName?: string | null;
+ moneyManagement?: MoneyManagement | null;
+}
+
export interface StartBotRequest {
botType?: BotType;
identifier?: string | null;
@@ -3119,6 +3125,8 @@ export interface StartBotRequest {
cooldownPeriod?: number;
maxLossStreak?: number;
name?: string | null;
+ maxPositionTimeHours?: number | null;
+ flipOnlyWhenInProfit?: boolean;
}
export interface TradingBot {
@@ -3138,6 +3146,8 @@ export interface TradingBot {
moneyManagement: MoneyManagement;
identifier: string;
agentName: string;
+ maxPositionTimeHours: number;
+ flipOnlyWhenInProfit: boolean;
}
export interface OpenPositionManuallyRequest {
@@ -3150,6 +3160,23 @@ export interface ClosePositionRequest {
positionId?: string | null;
}
+export interface UpdateBotConfigRequest {
+ identifier?: string | null;
+ accountName?: string | null;
+ moneyManagementName?: string | null;
+ ticker?: Ticker | null;
+ scenarioName?: string | null;
+ timeframe?: Timeframe | null;
+ isForWatchingOnly?: boolean | null;
+ botTradingBalance?: number | null;
+ cooldownPeriod?: number | null;
+ maxLossStreak?: number | null;
+ maxPositionTimeHours?: number | null;
+ flipOnlyWhenInProfit?: boolean | null;
+ flipPosition?: boolean | null;
+ name?: string | null;
+}
+
export interface TickerInfos {
ticker?: Ticker;
imageUrl?: string | null;
diff --git a/src/Managing.WebApp/src/global/type.tsx b/src/Managing.WebApp/src/global/type.tsx
index 742d8e3..d9eb759 100644
--- a/src/Managing.WebApp/src/global/type.tsx
+++ b/src/Managing.WebApp/src/global/type.tsx
@@ -114,6 +114,8 @@ export type IBacktestsFormInput = {
endDate: string
cooldownPeriod: number
maxLossStreak: number
+ maxPositionTimeHours?: number | null
+ flipOnlyWhenInProfit?: boolean
}
export type IBacktestCards = {