Update composite And update scenario

This commit is contained in:
2025-06-02 21:57:34 +07:00
parent 7fce1fa59e
commit 71bcaea76d
7 changed files with 244 additions and 80 deletions

View File

@@ -581,7 +581,12 @@ public class TradingBot : Bot, ITradingBot
// ==> Flip the position // ==> Flip the position
if (Config.FlipPosition) if (Config.FlipPosition)
{ {
await LogInformation("Try to flip the position because of an opposite direction signal"); // Check if current position is in profit before flipping
var isPositionInProfit = await IsPositionInProfit(openedPosition, lastPrice);
if (isPositionInProfit)
{
await LogInformation("Try to flip the position because of an opposite direction signal and current position is in profit");
await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true); await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true);
await SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped); await SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped);
await OpenPosition(signal); await OpenPosition(signal);
@@ -589,6 +594,17 @@ public class TradingBot : Bot, ITradingBot
$"Position {previousSignal.Identifier} flipped by {signal.Identifier} at {lastPrice}$"); $"Position {previousSignal.Identifier} flipped by {signal.Identifier} at {lastPrice}$");
} }
else else
{
await LogInformation(
$"Position {previousSignal.Identifier} is not in profit (entry: {openedPosition.Open.Price}, current: {lastPrice}). " +
$"Signal {signal.Identifier} will wait for position to become profitable before flipping.");
// Keep signal in waiting status to check again on next execution
SetSignalStatus(signal.Identifier, SignalStatus.WaitingForPosition);
return;
}
}
else
{ {
await LogInformation( await LogInformation(
$"A position is already open for signal {previousSignal.Identifier}. Position flipping is currently not enable, the position will not be flipped."); $"A position is already open for signal {previousSignal.Identifier}. Position flipping is currently not enable, the position will not be flipped.");
@@ -1167,6 +1183,28 @@ public class TradingBot : Bot, ITradingBot
Logger.LogInformation($"Manually opened position {position.Identifier} for signal {signal.Identifier}"); Logger.LogInformation($"Manually opened position {position.Identifier} for signal {signal.Identifier}");
return position; return position;
} }
/// <summary>
/// Checks if a position is currently in profit based on current market price
/// </summary>
/// <param name="position">The position to check</param>
/// <param name="currentPrice">The current market price</param>
/// <returns>True if position is in profit, false otherwise</returns>
private async Task<bool> IsPositionInProfit(Position position, decimal currentPrice)
{
if (position.OriginDirection == TradeDirection.Long)
{
return currentPrice >= position.Open.Price;
}
else if (position.OriginDirection == TradeDirection.Short)
{
return currentPrice <= position.Open.Price;
}
else
{
throw new ArgumentException("Invalid position direction");
}
}
} }
public class TradingBotBackup public class TradingBotBackup

View File

@@ -82,7 +82,6 @@ public static class TradingBox
} }
// Ensure limitedCandles is ordered chronologically // Ensure limitedCandles is ordered chronologically
loopbackPeriod = 20;
var orderedCandles = limitedCandles.OrderBy(c => c.Date).ToList(); var orderedCandles = limitedCandles.OrderBy(c => c.Date).ToList();
var loopback = loopbackPeriod.HasValue && loopbackPeriod > 1 ? loopbackPeriod.Value : 1; var loopback = loopbackPeriod.HasValue && loopbackPeriod > 1 ? loopbackPeriod.Value : 1;
var candleLoopback = orderedCandles.TakeLast(loopback).ToList(); var candleLoopback = orderedCandles.TakeLast(loopback).ToList();

View File

@@ -0,0 +1,110 @@
import React, {useEffect} from 'react'
import {SubmitHandler, useForm} from 'react-hook-form'
import {Modal} from '../../mollecules'
import type {Scenario, Strategy} from '../../../generated/ManagingApi'
import type {IScenarioFormInput} from '../../../global/type'
interface ScenarioModalProps {
showModal: boolean
onClose: () => void
onSubmit: (data: IScenarioFormInput) => Promise<void>
strategies: Strategy[]
scenario?: Scenario | null // For update mode
isUpdate?: boolean
}
const ScenarioModal: React.FC<ScenarioModalProps> = ({
showModal,
onClose,
onSubmit,
strategies,
scenario = null,
isUpdate = false
}) => {
const { register, handleSubmit, reset, setValue } = useForm<IScenarioFormInput>()
// Reset form when modal opens/closes or scenario changes
useEffect(() => {
if (showModal) {
if (isUpdate && scenario) {
// Pre-populate form for update
setValue('name', scenario.name || '')
setValue('loopbackPeriod', scenario.loopbackPeriod || 0)
setValue('strategies', scenario.strategies?.map(s => s.name || '') || [])
} else {
// Reset form for create
reset()
}
}
}, [showModal, isUpdate, scenario, setValue, reset])
const handleFormSubmit: SubmitHandler<IScenarioFormInput> = async (data) => {
onClose()
await onSubmit(data)
}
const titleHeader = isUpdate ? 'Update Scenario' : 'Scenario Builder'
const submitButtonText = isUpdate ? 'Update' : 'Build'
return (
<Modal
onClose={onClose}
onSubmit={handleSubmit(handleFormSubmit)}
showModal={showModal}
titleHeader={titleHeader}
>
<div className="form-control mb-5">
<div className="input-group">
<label htmlFor="name" className="label mr-6">
Name
</label>
<input
className="bg-inherit w-full max-w-xs"
{...register('name')}
disabled={isUpdate} // Disable name editing in update mode
/>
</div>
</div>
<div className="form-control">
<div className="input-group">
<label htmlFor="strategies" className="label mr-6">
Strategies
</label>
<select
multiple
className="select select-bordered h-28 w-full max-w-xs"
{...register('strategies')}
>
{strategies.map((item) => (
<option key={item.name} value={item.name || ''}>
{item.signalType} - {item.name}
</option>
))}
</select>
</div>
</div>
<div className="form-control mb-5">
<div className="input-group">
<label htmlFor="loopbackPeriod" className="label mr-6">
Loopback period
</label>
<input
type="number"
className="bg-inherit w-full max-w-xs"
{...register('loopbackPeriod', { valueAsNumber: true })}
/>
</div>
</div>
<div className="modal-action">
<button type="submit" className="btn">
{submitButtonText}
</button>
</div>
</Modal>
)
}
export default ScenarioModal

View File

@@ -8,3 +8,4 @@ export { default as SpotLightBadge } from './SpotLightBadge/SpotLightBadge'
export { default as StatusBadge } from './StatusBadge/StatusBadge' export { default as StatusBadge } from './StatusBadge/StatusBadge'
export { default as PositionsList } from './Positions/PositionList' export { default as PositionsList } from './Positions/PositionList'
export { default as WorkflowCanvas } from './Workflow/workflowCanvas' export { default as WorkflowCanvas } from './Workflow/workflowCanvas'
export { default as ScenarioModal } from './ScenarioModal'

View File

@@ -19,6 +19,7 @@ import type {
Scenario, Scenario,
Signal, Signal,
StrategiesResultBase, StrategiesResultBase,
Strategy,
StrategyType, StrategyType,
Ticker, Ticker,
Timeframe, Timeframe,
@@ -189,6 +190,8 @@ export type IScenarioFormInput = {
} }
export type IScenarioList = { export type IScenarioList = {
list: Scenario[] list: Scenario[]
strategies?: Strategy[]
setScenarios?: React.Dispatch<React.SetStateAction<Scenario[]>>
} }
export type IMoneyManagementList = { export type IMoneyManagementList = {

View File

@@ -1,10 +1,9 @@
import React, {useEffect, useState} from 'react' import React, {useEffect, useState} from 'react'
import type {SubmitHandler} from 'react-hook-form'
import {useForm} from 'react-hook-form'
import 'react-toastify/dist/ReactToastify.css' import 'react-toastify/dist/ReactToastify.css'
import useApiUrlStore from '../../app/store/apiStore' import useApiUrlStore from '../../app/store/apiStore'
import {Modal, Toast} from '../../components/mollecules' import {Toast} from '../../components/mollecules'
import {ScenarioModal} from '../../components/organism'
import type {Scenario, Strategy} from '../../generated/ManagingApi' import type {Scenario, Strategy} from '../../generated/ManagingApi'
import {ScenarioClient} from '../../generated/ManagingApi' import {ScenarioClient} from '../../generated/ManagingApi'
import type {IScenarioFormInput} from '../../global/type' import type {IScenarioFormInput} from '../../global/type'
@@ -15,7 +14,6 @@ const ScenarioList: React.FC = () => {
const [strategies, setStrategies] = useState<Strategy[]>([]) const [strategies, setStrategies] = useState<Strategy[]>([])
const [scenarios, setScenarios] = useState<Scenario[]>([]) const [scenarios, setScenarios] = useState<Scenario[]>([])
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const { register, handleSubmit } = useForm<IScenarioFormInput>()
const { apiUrl } = useApiUrlStore() const { apiUrl } = useApiUrlStore()
const client = new ScenarioClient({}, apiUrl) const client = new ScenarioClient({}, apiUrl)
@@ -32,8 +30,7 @@ const ScenarioList: React.FC = () => {
}) })
} }
const onSubmit: SubmitHandler<IScenarioFormInput> = async (form) => { const handleSubmit = async (form: IScenarioFormInput) => {
closeModal()
await createScenario(form) await createScenario(form)
} }
@@ -59,61 +56,14 @@ const ScenarioList: React.FC = () => {
<button className="btn" onClick={openModal}> <button className="btn" onClick={openModal}>
Create new scenario Create new scenario
</button> </button>
<ScenarioTable list={scenarios} /> <ScenarioTable list={scenarios} strategies={strategies} setScenarios={setScenarios} />
<Modal <ScenarioModal
{...{ showModal={showModal}
onClose: closeModal, onClose={closeModal}
onSubmit: handleSubmit(onSubmit), onSubmit={handleSubmit}
showModal, strategies={strategies}
titleHeader: 'Scenario Builder', isUpdate={false}
}} />
>
<div className="form-control mb-5">
<div className="input-group">
<label htmlFor="name" className="label mr-6">
Name
</label>
<input
className="bg-inherit w-full max-w-xs"
{...register('name')}
></input>
</div>
</div>
<div className="form-control">
<div className="input-group">
<label htmlFor="strategies" className="label mr-6">
Strategies
</label>
<select
multiple
className="select select-bordered h-28 w-full max-w-xs"
{...register('strategies')}
>
{strategies.map((item) => (
<option key={item.name} value={item.name}>
{item.signalType} - {item.name}
</option>
))}
</select>
</div>
</div>
<div className="form-control mb-5">
<div className="input-group">
<label htmlFor="name" className="label mr-6">
Loopback period
</label>
<input
className="bg-inherit w-full max-w-xs"
{...register('loopbackPeriod')}
></input>
</div>
</div>
<div className="modal-action">
<button type="submit" className="btn">
Build
</button>
</div>
</Modal>
</div> </div>
) )
} }

View File

@@ -1,29 +1,72 @@
import { TrashIcon } from '@heroicons/react/solid' import {PencilIcon, TrashIcon} from '@heroicons/react/solid'
import React, {useEffect, useState} from 'react' import React, {useEffect, useState} from 'react'
import useApiUrlStore from '../../app/store/apiStore' import useApiUrlStore from '../../app/store/apiStore'
import { Toast, Table } from '../../components/mollecules' import {Table, Toast} from '../../components/mollecules'
import {ScenarioModal} from '../../components/organism'
import type {Scenario, Strategy} from '../../generated/ManagingApi' import type {Scenario, Strategy} from '../../generated/ManagingApi'
import {ScenarioClient} from '../../generated/ManagingApi' import {ScenarioClient} from '../../generated/ManagingApi'
import type { IScenarioList } from '../../global/type' import type {IScenarioFormInput, IScenarioList} from '../../global/type'
const ScenarioTable: React.FC<IScenarioList> = ({ list }) => { const ScenarioTable: React.FC<IScenarioList> = ({ list, strategies = [], setScenarios }) => {
const [rows, setRows] = useState<Scenario[]>([]) const [rows, setRows] = useState<Scenario[]>([])
const [showUpdateModal, setShowUpdateModal] = useState(false)
const [selectedScenario, setSelectedScenario] = useState<Scenario | null>(null)
const { apiUrl } = useApiUrlStore() const { apiUrl } = useApiUrlStore()
const client = new ScenarioClient({}, apiUrl)
async function deleteScenario(id: string) { async function deleteScenario(id: string) {
const t = new Toast('Deleting scenario') const t = new Toast('Deleting scenario')
const client = new ScenarioClient({}, apiUrl)
await client await client
.scenario_DeleteScenario(id) .scenario_DeleteScenario(id)
.then(() => { .then(() => {
t.update('success', 'Scenario deleted') t.update('success', 'Scenario deleted')
// Refetch scenarios after deletion
if (setScenarios) {
client.scenario_GetScenarios().then((scenarios) => {
setScenarios(scenarios)
})
}
}) })
.catch((err) => { .catch((err) => {
t.update('error', err) t.update('error', err)
}) })
} }
async function updateScenario(form: IScenarioFormInput) {
const t = new Toast('Updating scenario')
await client
.scenario_UpdateScenario(form.name, form.loopbackPeriod, form.strategies)
.then(() => {
t.update('success', 'Scenario updated')
// Refetch scenarios after update since the API returns FileResponse
if (setScenarios) {
client.scenario_GetScenarios().then((scenarios) => {
setScenarios(scenarios)
})
}
})
.catch((err) => {
t.update('error', err)
})
}
function openUpdateModal(scenario: Scenario) {
setSelectedScenario(scenario)
setShowUpdateModal(true)
}
function closeUpdateModal() {
setShowUpdateModal(false)
setSelectedScenario(null)
}
const handleUpdateSubmit = async (form: IScenarioFormInput) => {
await updateScenario(form)
}
const columns = React.useMemo( const columns = React.useMemo(
() => [ () => [
{ {
@@ -50,18 +93,28 @@ const ScenarioTable: React.FC<IScenarioList> = ({ list }) => {
}, },
{ {
Cell: ({ cell }: any) => ( Cell: ({ cell }: any) => (
<> <div className="flex gap-2">
<div className="tooltip" data-tip="Update scenario">
<button
data-value={cell.row.values.name}
onClick={() => openUpdateModal(cell.row.original)}
className="btn btn-ghost btn-sm"
>
<PencilIcon className="text-info w-4" />
</button>
</div>
<div className="tooltip" data-tip="Delete scenario"> <div className="tooltip" data-tip="Delete scenario">
<button <button
data-value={cell.row.values.name} data-value={cell.row.values.name}
onClick={() => deleteScenario(cell.row.values.name)} onClick={() => deleteScenario(cell.row.values.name)}
className="btn btn-ghost btn-sm"
> >
<TrashIcon className="text-accent w-4"></TrashIcon> <TrashIcon className="text-accent w-4" />
</button> </button>
</div> </div>
</> </div>
), ),
Header: '', Header: 'Actions',
accessor: 'id', accessor: 'id',
disableFilters: true, disableFilters: true,
}, },
@@ -76,6 +129,16 @@ const ScenarioTable: React.FC<IScenarioList> = ({ list }) => {
return ( return (
<div className="flex flex-wrap"> <div className="flex flex-wrap">
<Table columns={columns} data={rows} showPagination={true} /> <Table columns={columns} data={rows} showPagination={true} />
{/* Update Modal */}
<ScenarioModal
showModal={showUpdateModal}
onClose={closeUpdateModal}
onSubmit={handleUpdateSubmit}
strategies={strategies}
scenario={selectedScenario}
isUpdate={true}
/>
</div> </div>
) )
} }