Update composite And update scenario
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 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'
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 type { Scenario, Strategy } from '../../generated/ManagingApi'
|
import {ScenarioModal} from '../../components/organism'
|
||||||
import { ScenarioClient } from '../../generated/ManagingApi'
|
import type {Scenario, Strategy} from '../../generated/ManagingApi'
|
||||||
import type { IScenarioList } from '../../global/type'
|
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 [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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user