Update composite And update scenario
This commit is contained in:
@@ -581,12 +581,28 @@ public class TradingBot : Bot, ITradingBot
|
||||
// ==> Flip the position
|
||||
if (Config.FlipPosition)
|
||||
{
|
||||
await LogInformation("Try to flip the position because of an opposite direction signal");
|
||||
await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true);
|
||||
await SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped);
|
||||
await OpenPosition(signal);
|
||||
await LogInformation(
|
||||
$"Position {previousSignal.Identifier} flipped by {signal.Identifier} at {lastPrice}$");
|
||||
// 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 SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped);
|
||||
await OpenPosition(signal);
|
||||
await LogInformation(
|
||||
$"Position {previousSignal.Identifier} flipped by {signal.Identifier} at {lastPrice}$");
|
||||
}
|
||||
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
|
||||
{
|
||||
@@ -1167,6 +1183,28 @@ public class TradingBot : Bot, ITradingBot
|
||||
Logger.LogInformation($"Manually opened position {position.Identifier} for signal {signal.Identifier}");
|
||||
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
|
||||
|
||||
@@ -82,7 +82,6 @@ public static class TradingBox
|
||||
}
|
||||
|
||||
// Ensure limitedCandles is ordered chronologically
|
||||
loopbackPeriod = 20;
|
||||
var orderedCandles = limitedCandles.OrderBy(c => c.Date).ToList();
|
||||
var loopback = loopbackPeriod.HasValue && loopbackPeriod > 1 ? loopbackPeriod.Value : 1;
|
||||
var candleLoopback = orderedCandles.TakeLast(loopback).ToList();
|
||||
|
||||
@@ -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
|
||||
@@ -8,3 +8,4 @@ export { default as SpotLightBadge } from './SpotLightBadge/SpotLightBadge'
|
||||
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'
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
Scenario,
|
||||
Signal,
|
||||
StrategiesResultBase,
|
||||
Strategy,
|
||||
StrategyType,
|
||||
Ticker,
|
||||
Timeframe,
|
||||
@@ -189,6 +190,8 @@ export type IScenarioFormInput = {
|
||||
}
|
||||
export type IScenarioList = {
|
||||
list: Scenario[]
|
||||
strategies?: Strategy[]
|
||||
setScenarios?: React.Dispatch<React.SetStateAction<Scenario[]>>
|
||||
}
|
||||
|
||||
export type IMoneyManagementList = {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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 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 {ScenarioClient} from '../../generated/ManagingApi'
|
||||
import type {IScenarioFormInput} from '../../global/type'
|
||||
@@ -15,7 +14,6 @@ const ScenarioList: React.FC = () => {
|
||||
const [strategies, setStrategies] = useState<Strategy[]>([])
|
||||
const [scenarios, setScenarios] = useState<Scenario[]>([])
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const { register, handleSubmit } = useForm<IScenarioFormInput>()
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const client = new ScenarioClient({}, apiUrl)
|
||||
|
||||
@@ -32,8 +30,7 @@ const ScenarioList: React.FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const onSubmit: SubmitHandler<IScenarioFormInput> = async (form) => {
|
||||
closeModal()
|
||||
const handleSubmit = async (form: IScenarioFormInput) => {
|
||||
await createScenario(form)
|
||||
}
|
||||
|
||||
@@ -59,61 +56,14 @@ const ScenarioList: React.FC = () => {
|
||||
<button className="btn" onClick={openModal}>
|
||||
Create new scenario
|
||||
</button>
|
||||
<ScenarioTable list={scenarios} />
|
||||
<Modal
|
||||
{...{
|
||||
onClose: closeModal,
|
||||
onSubmit: handleSubmit(onSubmit),
|
||||
showModal,
|
||||
titleHeader: 'Scenario Builder',
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<ScenarioTable list={scenarios} strategies={strategies} setScenarios={setScenarios} />
|
||||
<ScenarioModal
|
||||
showModal={showModal}
|
||||
onClose={closeModal}
|
||||
onSubmit={handleSubmit}
|
||||
strategies={strategies}
|
||||
isUpdate={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,72 @@
|
||||
import { TrashIcon } from '@heroicons/react/solid'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {PencilIcon, TrashIcon} from '@heroicons/react/solid'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
|
||||
import useApiUrlStore from '../../app/store/apiStore'
|
||||
import { Toast, Table } from '../../components/mollecules'
|
||||
import type { Scenario, Strategy } from '../../generated/ManagingApi'
|
||||
import { ScenarioClient } from '../../generated/ManagingApi'
|
||||
import type { IScenarioList } from '../../global/type'
|
||||
import {Table, Toast} from '../../components/mollecules'
|
||||
import {ScenarioModal} from '../../components/organism'
|
||||
import type {Scenario, Strategy} from '../../generated/ManagingApi'
|
||||
import {ScenarioClient} from '../../generated/ManagingApi'
|
||||
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 [showUpdateModal, setShowUpdateModal] = useState(false)
|
||||
const [selectedScenario, setSelectedScenario] = useState<Scenario | null>(null)
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const client = new ScenarioClient({}, apiUrl)
|
||||
|
||||
async function deleteScenario(id: string) {
|
||||
const t = new Toast('Deleting scenario')
|
||||
const client = new ScenarioClient({}, apiUrl)
|
||||
|
||||
await client
|
||||
.scenario_DeleteScenario(id)
|
||||
.then(() => {
|
||||
t.update('success', 'Scenario deleted')
|
||||
// Refetch scenarios after deletion
|
||||
if (setScenarios) {
|
||||
client.scenario_GetScenarios().then((scenarios) => {
|
||||
setScenarios(scenarios)
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((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(
|
||||
() => [
|
||||
{
|
||||
@@ -50,18 +93,28 @@ const ScenarioTable: React.FC<IScenarioList> = ({ list }) => {
|
||||
},
|
||||
{
|
||||
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">
|
||||
<button
|
||||
data-value={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>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
),
|
||||
Header: '',
|
||||
Header: 'Actions',
|
||||
accessor: 'id',
|
||||
disableFilters: true,
|
||||
},
|
||||
@@ -76,6 +129,16 @@ const ScenarioTable: React.FC<IScenarioList> = ({ list }) => {
|
||||
return (
|
||||
<div className="flex flex-wrap">
|
||||
<Table columns={columns} data={rows} showPagination={true} />
|
||||
|
||||
{/* Update Modal */}
|
||||
<ScenarioModal
|
||||
showModal={showUpdateModal}
|
||||
onClose={closeUpdateModal}
|
||||
onSubmit={handleUpdateSubmit}
|
||||
strategies={strategies}
|
||||
scenario={selectedScenario}
|
||||
isUpdate={true}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user