Remove workflow
Some checks failed
Build & Deploy / build-and-deploy (push) Has been cancelled
.NET / build (push) Has been cancelled

This commit is contained in:
2025-08-16 01:33:15 +07:00
parent ae4d5b8abe
commit 9841219e8b
31 changed files with 4 additions and 1585 deletions

View File

@@ -107,22 +107,6 @@
- [x] Add button to display money management use by the bot - [x] Add button to display money management use by the bot
- [ ] POST POWNER - On the modarl, When simple bot selected, show only a select for the workflow - [ ] POST POWNER - On the modarl, When simple bot selected, show only a select for the workflow
## Workflow
- [x] List all workflow saved in
- [x] Use https://reactflow.dev/ to display a workflow (map flow to nodes and children to edges)
- [x] On update Nodes : https://codesandbox.io/s/dank-waterfall-8jfcf4?file=/src/App.js
- [x] Save workflow
- [ ] Reset workflow
- [ ] Add flows : Close Position, SendMessage
- [ ] On Flow.tsx : Display inputs/outputs names on the node
- [ ] Setup file tree UI for available flows : https://codesandbox.io/s/nlzui
- [x] Create a workflow type that will encapsulate a list of flows
- [x] Each flow will have parameters, inputs and outputs that will be used by the children flows
- [ ] Flow can handle multiple parents
- [ ] Run Simple bot base on a workflow
- [ ] Run backtest based on a workflow
- [ ] Add flows : ClosePosition, Scenario
## Backtests ## Backtests

View File

@@ -1,27 +0,0 @@
```mermaid
classDiagram
Workflow <|-- Flow
class Workflow{
String Name
Usage Usage : Trading|Task
Flow[] Flows
String Description
}
class Flow{
String Name
CategoryType Category
FlowType Type
FlowParameters Parameters
String Description
FlowType? AcceptedInput
OutputType[]? Outputs
Flow[]? ChildrenFlow
Flow? ParentFlow
Output? Output : Signal|Text|Candles
MapInput(AcceptedInput, ParentFlow.Output)
Run(ParentFlow.Output)
LoadChildren()
ExecuteChildren()
}
```

View File

@@ -1,72 +0,0 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Workflows;
using Managing.Domain.Workflows.Synthetics;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Managing.Api.Controllers
{
/// <summary>
/// Controller for managing workflows, including creating, retrieving, and deleting workflows.
/// Requires authorization for access.
/// </summary>
[Authorize]
public class WorkflowController : BaseController
{
private readonly IWorkflowService _workflowService;
/// <summary>
/// Initializes a new instance of the <see cref="WorkflowController"/> class.
/// </summary>
/// <param name="WorkflowService">Service for managing workflows.</param>
/// <param name="userService">Service for user-related operations.</param>
public WorkflowController(IWorkflowService WorkflowService, IUserService userService) : base(userService)
{
_workflowService = WorkflowService;
}
/// <summary>
/// Creates a new workflow or updates an existing one based on the provided workflow request.
/// </summary>
/// <param name="workflowRequest">The workflow request containing the details of the workflow to be created or updated.</param>
/// <returns>The created or updated workflow.</returns>
[HttpPost]
public async Task<ActionResult<Workflow>> PostWorkflow([ModelBinder]SyntheticWorkflow workflowRequest)
{
return Ok(await _workflowService.InsertOrUpdateWorkflow(workflowRequest));
}
/// <summary>
/// Retrieves all workflows.
/// </summary>
/// <returns>A list of all workflows.</returns>
[HttpGet]
public ActionResult<IEnumerable<SyntheticWorkflow>> GetWorkflows()
{
return Ok(_workflowService.GetWorkflows());
}
/// <summary>
/// Retrieves all available flows.
/// </summary>
/// <returns>A list of all available flows.</returns>
[HttpGet]
[Route("flows")]
public async Task<ActionResult<IEnumerable<IFlow>>> GetAvailableFlows()
{
return Ok(await _workflowService.GetAvailableFlows());
}
/// <summary>
/// Deletes a workflow by name.
/// </summary>
/// <param name="name">The name of the workflow to delete.</param>
/// <returns>An ActionResult indicating the outcome of the operation.</returns>
[HttpDelete]
public ActionResult DeleteWorkflow(string name)
{
return Ok(_workflowService.DeleteWorkflow(name));
}
}
}

View File

@@ -1,12 +0,0 @@
using Managing.Domain.Workflows.Synthetics;
namespace Managing.Application.Abstractions.Repositories;
public interface IWorkflowRepository
{
bool DeleteWorkflow(string name);
Task<SyntheticWorkflow> GetWorkflow(string name);
IEnumerable<SyntheticWorkflow> GetWorkflows();
Task InsertWorkflow(SyntheticWorkflow workflow);
Task UpdateWorkflow(SyntheticWorkflow workflow);
}

View File

@@ -1,54 +0,0 @@
using Managing.Domain.Workflows;
using Xunit;
using static Managing.Common.Enums;
namespace Managing.Application.Tests;
public class WorkflowTests : BaseTests
{
[Fact]
public async Task Should_Create_Workflow_with_Feed_Ticker_Flow()
{
// Arrange
var workflow = new Workflow
{
Name = "Bot trading",
Usage = WorkflowUsage.Trading,
Description = "Basic trading Workflow",
Flows = new List<IFlow>()
};
// var rsiDivFlow = new RsiDiv()
// {
// Parameters = "{\"Period\": 14,\"Timeframe\":1}",
// Children = new List<IFlow>(),
// };
// var tickerFlow = new FeedTicker(_exchangeService)
// {
// Parameters = "{\"Exchange\": 3,\"Ticker\":9,\"Timeframe\":1}",
// Children = new List<IFlow>()
// {
// rsiDivFlow
// }
// };
// workflow.Flows.Add(tickerFlow);
// Act
await workflow.Execute();
// Assert
foreach (var f in workflow.Flows)
{
Assert.False(string.IsNullOrEmpty(f.Output));
}
Assert.NotNull(workflow);
Assert.NotNull(workflow.Flows);
Assert.Single(workflow.Flows);
Assert.Equal("Feed Ticker", workflow.Name);
Assert.Equal(WorkflowUsage.Trading, workflow.Usage);
Assert.Equal("Basic trading Workflow", workflow.Description);
}
}

View File

@@ -1,9 +0,0 @@
using Managing.Domain.Workflows;
using Managing.Domain.Workflows.Synthetics;
namespace Managing.Application.Abstractions;
public interface IFlowFactory
{
IFlow BuildFlow(SyntheticFlow request);
}

View File

@@ -1,12 +0,0 @@
using Managing.Domain.Workflows;
using Managing.Domain.Workflows.Synthetics;
namespace Managing.Application.Abstractions;
public interface IWorkflowService
{
bool DeleteWorkflow(string name);
Task<IEnumerable<IFlow>> GetAvailableFlows();
IEnumerable<SyntheticWorkflow> GetWorkflows();
Task<Workflow> InsertOrUpdateWorkflow(SyntheticWorkflow workflowRequest);
}

View File

@@ -1,19 +0,0 @@
using static Managing.Common.Enums;
namespace Managing.Domain.Workflows;
public abstract class FlowBase : IFlow
{
public abstract Guid Id { get; }
public abstract string Name { get; }
public abstract FlowType Type { get; }
public abstract string Description { get; }
public abstract List<FlowOutput> AcceptedInputs { get; }
public abstract List<IFlow> Children { get; set; }
public abstract List<FlowParameter> Parameters { get; set; }
public abstract Guid ParentId { get; }
public abstract string Output { get; set; }
public abstract List<FlowOutput> OutputTypes { get; }
public abstract Task Execute(string input);
public abstract void MapParameters();
}

View File

@@ -1,7 +0,0 @@
namespace Managing.Domain.Workflows;
public class FlowParameter
{
public dynamic Value { get; set; }
public string Name { get; set; }
}

View File

@@ -1,26 +0,0 @@
using System.ComponentModel.DataAnnotations;
using static Managing.Common.Enums;
namespace Managing.Domain.Workflows;
public interface IFlow
{
[Required]
Guid Id { get; }
[Required]
string Name { get; }
[Required]
FlowType Type { get; }
[Required]
string Description { get; }
[Required]
List<FlowOutput> AcceptedInputs { get; }
List<IFlow> Children { get; set; }
[Required]
List<FlowParameter> Parameters { get; set; }
Guid ParentId { get; }
string Output { get; set; }
[Required]
List<FlowOutput> OutputTypes { get; }
Task Execute(string input);
}

View File

@@ -1,15 +0,0 @@
using System.ComponentModel.DataAnnotations;
using static Managing.Common.Enums;
namespace Managing.Domain.Workflows.Synthetics;
public class SyntheticFlow
{
[Required]
public string Id { get; set; }
public string ParentId { get; set; }
[Required]
public FlowType Type { get; set; }
[Required]
public List<SyntheticFlowParameter> Parameters { get; set; }
}

View File

@@ -1,11 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Managing.Domain.Workflows.Synthetics;
public class SyntheticFlowParameter
{
[Required]
public string Value { get; set; }
[Required]
public string Name { get; set; }
}

View File

@@ -1,16 +0,0 @@
using System.ComponentModel.DataAnnotations;
using static Managing.Common.Enums;
namespace Managing.Domain.Workflows.Synthetics;
public class SyntheticWorkflow
{
[Required]
public string Name { get; set; }
[Required]
public WorkflowUsage Usage { get; set; }
[Required]
public string Description { get; set; }
[Required]
public List<SyntheticFlow> Flows { get; set; }
}

View File

@@ -1,24 +0,0 @@
using System.ComponentModel.DataAnnotations;
using static Managing.Common.Enums;
namespace Managing.Domain.Workflows;
public class Workflow
{
[Required]
public string Name { get; set; }
[Required]
public WorkflowUsage Usage { get; set; }
[Required]
public List<IFlow> Flows { get; set; }
[Required]
public string Description { get; set; }
public async Task Execute()
{
foreach (var flow in Flows)
{
await flow.Execute(string.Empty);
}
}
}

View File

@@ -1,11 +1,10 @@
import { Suspense, lazy } from 'react' import {lazy, Suspense} from 'react'
import {Route, Routes} from 'react-router-dom' import {Route, Routes} from 'react-router-dom'
import LayoutMain from '../../layouts' import LayoutMain from '../../layouts'
import DeskWidget from '../../pages/desk/deskWidget' import DeskWidget from '../../pages/desk/deskWidget'
import Scenario from '../../pages/scenarioPage/scenario' import Scenario from '../../pages/scenarioPage/scenario'
import Tools from '../../pages/toolsPage/tools' import Tools from '../../pages/toolsPage/tools'
import Workflows from '../../pages/workflow/workflows'
const Backtest = lazy(() => import('../../pages/backtestPage/backtest')) const Backtest = lazy(() => import('../../pages/backtestPage/backtest'))
const Bots = lazy(() => import('../../pages/botsPage/bots')) const Bots = lazy(() => import('../../pages/botsPage/bots'))
@@ -48,17 +47,6 @@ const MyRoutes = () => {
/> />
</Route> </Route>
<Route path="/workflow" element={<LayoutMain />}>
<Route
index
element={
<Suspense fallback={null}>
<Workflows />
</Suspense>
}
/>
</Route>
<Route path="/settings" element={<LayoutMain />}> <Route path="/settings" element={<LayoutMain />}>
<Route <Route
index index

View File

@@ -1,28 +0,0 @@
import { create } from 'zustand'
import type { IFlow } from '../../generated/ManagingApi'
import { WorkflowClient } from '../../generated/ManagingApi'
type FlowStore = {
setFlows: (flows: IFlow[]) => void
getFlows: (apiUrl: string) => void
flows: IFlow[]
}
export const useFlowStore = create<FlowStore>((set) => ({
flows: [] as IFlow[],
getFlows: async (apiUrl) => {
const client = new WorkflowClient({}, apiUrl)
await client.workflow_GetAvailableFlows().then((data) => {
set(() => ({
flows: data,
}))
})
},
setFlows: (flows) => {
set((state) => ({
...state,
flows: flows,
}))
},
}))

View File

@@ -1,13 +0,0 @@
import type { IWorkflowStore } from '../workflowStore'
export const WorkflowSelector = (state: IWorkflowStore) => ({
edges: state.edges,
initWorkFlow: state.initWorkFlow,
nodes: state.nodes,
onConnect: state.onConnect,
onEdgesChange: state.onEdgesChange,
onNodesChange: state.onNodesChange,
resetWorkflow: state.resetWorkflow,
setNodes: state.setNodes,
updateNodeData: state.updateNodeData,
})

View File

@@ -1,113 +0,0 @@
import type {
Connection,
Edge,
EdgeChange,
Node,
NodeChange,
OnNodesChange,
OnEdgesChange,
OnConnect,
} from 'reactflow'
import { addEdge, applyNodeChanges, applyEdgeChanges } from 'reactflow'
import { create } from 'zustand'
import type {
SyntheticFlowParameter,
FlowParameter,
IFlow,
} from '../../generated/ManagingApi'
export type IWorkflowStore = {
nodes: Node<IFlow>[]
initialNodes: Node<IFlow>[]
edges: Edge[]
initialEdges: Edge[]
onNodesChange: OnNodesChange
onEdgesChange: OnEdgesChange
onConnect: OnConnect
updateNodeData: (nodeId: string, parameterName: string, value: string) => void
initWorkFlow: (nodes: Node<IFlow>[], edges: Edge[]) => void
setNodes: (nodes: Node<IFlow>[]) => void
resetWorkflow: () => void
}
// this is our useStore hook that we can use in our components to get parts of the store and call actions
const useWorkflowStore = create<IWorkflowStore>((set, get) => ({
edges: [],
initWorkFlow: (nodes: Node<IFlow>[], edges: Edge[]) => {
set({
edges: edges,
initialEdges: edges,
initialNodes: nodes,
nodes: nodes,
})
},
initialEdges: [],
initialNodes: [],
nodes: [],
onConnect: (connection: Connection, callback: void) => {
set({
edges: addEdge(connection, get().edges),
})
},
onEdgesChange: (changes: EdgeChange[]) => {
set({
edges: applyEdgeChanges(changes, get().edges),
})
},
onNodesChange: (changes: NodeChange[]) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
})
},
resetWorkflow: () => {
set({
edges: get().initialEdges,
initialEdges: get().initialEdges,
initialNodes: get().initialNodes,
nodes: get().initialNodes,
})
},
setNodes: (nodes: Node<IFlow>[]) => {
set({
nodes: nodes,
})
},
updateNodeData: (nodeId: string, parameterName: string, value: string) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
node.data.parameters = updateParameters(
node.data.parameters,
parameterName,
value
)
}
return node
}),
})
},
}))
const updateParameters = (
parameters: FlowParameter[],
name: string,
value: string
) => {
if (!parameters.find((parameter) => parameter.name === name)) {
parameters.push({
name: name,
value: value,
} as SyntheticFlowParameter)
} else {
parameters = parameters.map((parameter) => {
if (parameter.name === name) {
parameter.value = value
}
return parameter
})
}
return parameters
}
export default useWorkflowStore

View File

@@ -1,22 +0,0 @@
import React from 'react'
import type { IFlow } from '../../../generated/ManagingApi'
type IFlowItem = {
onDragStart: (event: any, data: string) => void
flow: IFlow
}
const FlowItem: React.FC<IFlowItem> = ({ onDragStart, flow }) => {
return (
<div
className="btn btn-primary btn-xs w-full h-full my-2"
onDragStart={(event) => onDragStart(event, `${flow.type}`)}
draggable
>
{flow.name}
</div>
)
}
export default FlowItem

View File

@@ -1,40 +0,0 @@
import { Handle, Position } from 'reactflow'
import type { FlowOutput } from '../../../../generated/ManagingApi'
import type { IFlowProps } from '../../../../global/type'
import { Card } from '../../../mollecules'
const Flow = ({
name,
description,
children,
inputs,
outputs,
isConnectable,
}: IFlowProps) => {
return (
<Card name={name} info={description}>
{inputs?.map((input: FlowOutput) => {
return (
<Handle
type="target"
position={Position.Left}
isConnectable={isConnectable}
/>
)
})}
{children}
{outputs?.map((output: FlowOutput) => {
return (
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
/>
)
})}
</Card>
)
}
export default Flow

View File

@@ -1,62 +0,0 @@
import { useCallback } from 'react'
import type { NodeProps } from 'reactflow'
import { WorkflowSelector } from '../../../../../app/store/selectors/workflowSelector'
import useWorkflowStore from '../../../../../app/store/workflowStore'
import type { IFlow } from '../../../../../generated/ManagingApi'
import { Timeframe, Ticker } from '../../../../../generated/ManagingApi'
import { FormInput } from '../../../../mollecules'
import Flow from '../Flow'
const FeedTicker = ({ data, isConnectable, id }: NodeProps<IFlow>) => {
const { updateNodeData } = useWorkflowStore(WorkflowSelector)
const onTickerChange = useCallback((evt: any) => {
updateNodeData(id, 'Ticker', evt.target.value)
}, [])
const onTimeframeChange = useCallback((evt: any) => {
updateNodeData(id, 'Timeframe', evt.target.value)
}, [])
return (
<Flow
name={data.name}
description={data.description}
inputs={data.acceptedInputs}
outputs={data.outputTypes}
isConnectable={isConnectable}
>
<FormInput label="Timeframe" htmlFor="period">
<select
className="select no-drag w-full max-w-xs"
onChange={onTimeframeChange}
value={
data.parameters.find((p) => p.name === 'Timeframe')?.value || ''
}
>
{Object.keys(Timeframe).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
<FormInput label="Ticker" htmlFor="ticker">
<select
className="select no-drag w-full max-w-xs"
onChange={onTickerChange}
value={data.parameters.find((p) => p.name === 'Ticker')?.value || ''}
>
{Object.keys(Ticker).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
</Flow>
)
}
export default FeedTicker

View File

@@ -1,58 +0,0 @@
import { useCallback } from 'react'
import type { NodeProps } from 'reactflow'
import { WorkflowSelector } from '../../../../../app/store/selectors/workflowSelector'
import useWorkflowStore from '../../../../../app/store/workflowStore'
import type { IFlow } from '../../../../../generated/ManagingApi'
import { Timeframe } from '../../../../../generated/ManagingApi'
import { FormInput } from '../../../../mollecules'
import Flow from '../Flow'
const RsiDivergenceFlow = ({ data, isConnectable, id }: NodeProps<IFlow>) => {
const { updateNodeData } = useWorkflowStore(WorkflowSelector)
const onPeriodChange = useCallback((evt: any) => {
updateNodeData(id, 'Period', evt.target.value)
}, [])
const onTimeframeChange = useCallback((evt: any) => {
updateNodeData(id, 'Timeframe', evt.target.value)
}, [])
return (
<Flow
name={data.name || ''}
description={data.description}
inputs={data.acceptedInputs}
outputs={data.outputTypes}
isConnectable={isConnectable}
>
<FormInput label="Period" htmlFor="period">
<input
id="period"
name="text"
onChange={onPeriodChange}
className="input nodrag w-full max-w-xs"
value={data.parameters.find((p) => p.name === 'Period')?.value || ''}
/>
</FormInput>
<FormInput label="Timeframe" htmlFor="period">
<select
className="select no-drag w-full max-w-xs"
onChange={onTimeframeChange}
value={
data.parameters.find((p) => p.name === 'Timeframe')?.value || ''
}
>
{Object.keys(Timeframe).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
</Flow>
)
}
export default RsiDivergenceFlow

View File

@@ -1,96 +0,0 @@
import { useQuery } from '@tanstack/react-query'
import React, { useCallback, useState } from 'react'
import type { NodeProps } from 'reactflow'
import useApiUrlStore from '../../../../../app/store/apiStore'
import { WorkflowSelector } from '../../../../../app/store/selectors/workflowSelector'
import useWorkflowStore from '../../../../../app/store/workflowStore'
import type { Account, IFlow } from '../../../../../generated/ManagingApi'
import { MoneyManagementClient } from '../../../../../generated/ManagingApi'
import useAccounts from '../../../../../hooks/useAccounts'
import { Loader } from '../../../../atoms'
import { FormInput } from '../../../../mollecules'
import Flow from '../Flow'
const OpenPositionFlow = ({ data, isConnectable, id }: NodeProps<IFlow>) => {
const { updateNodeData } = useWorkflowStore(WorkflowSelector)
const { apiUrl } = useApiUrlStore()
const [selectedAccount, setSelectedAccount] = React.useState<string>()
const [selectedMoneyManagement, setSelectedMoneyManagement] =
useState<string>()
const moneyManagementClient = new MoneyManagementClient({}, apiUrl)
const { data: accounts } = useAccounts({
callback: (data: Account[]) => {
setSelectedAccount(data[0].name)
},
})
const { data: moneyManagements } = useQuery({
onSuccess: (data) => {
if (data) {
setSelectedMoneyManagement(data[0].name)
}
},
queryFn: () => moneyManagementClient.moneyManagement_GetMoneyManagements(),
queryKey: ['moneyManagement'],
})
const onAccountChange = useCallback((evt: any) => {
updateNodeData(id, 'Account', evt.target.value)
}, [])
const onMoneyManagementChange = useCallback((evt: any) => {
updateNodeData(id, 'MoneyManagement', evt.target.value)
}, [])
if (!accounts || !moneyManagements) {
return <Loader />
}
return (
<Flow
name={data.name || ''}
description={data.description}
inputs={data.acceptedInputs}
outputs={data.outputTypes}
isConnectable={isConnectable}
>
<FormInput label="Account" htmlFor="accountName">
<select
className="select select-bordered w-full h-auto max-w-xs"
onChange={(evt) => onAccountChange(evt.target.value)}
value={
data.parameters.find((p) => p.name === 'Account')?.value ||
selectedAccount
}
>
{accounts.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</select>
</FormInput>
<FormInput label="Money Management" htmlFor="moneyManagement">
<select
className="select w-full max-w-xs"
onChange={(evt) => onMoneyManagementChange(evt.target.value)}
value={
data.parameters.find((p) => p.name === 'MoneyManagement')?.value ||
selectedMoneyManagement
}
>
{moneyManagements.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</select>
</FormInput>
</Flow>
)
}
export default OpenPositionFlow

View File

@@ -1,24 +0,0 @@
import { useQuery } from "@tanstack/react-query"
const fetchData = () => {
return {
fake: 'data',
}
}
const ParentComponent = () => {
const { data, isLoading } = useQuery({
queryFn: fetchData,
queryKey: ['data'],
})
if (isLoading) {
return <div>Loading...</div>
}
return (
<div>
<ChildComponent data={data} />
</div>
)
}

View File

@@ -1,255 +0,0 @@
import { useQuery } from '@tanstack/react-query'
import { useCallback, useMemo, useRef, useState } from 'react'
import type { NodeTypes, Node } from 'reactflow'
import ReactFlow, { Controls, Background, ReactFlowProvider } from 'reactflow'
import 'reactflow/dist/style.css'
import useApiUrlStore from '../../../app/store/apiStore'
import { WorkflowSelector } from '../../../app/store/selectors/workflowSelector'
import useWorkflowStore from '../../../app/store/workflowStore'
import type {
FlowType,
IFlow,
SyntheticFlow,
SyntheticFlowParameter,
SyntheticWorkflow,
} from '../../../generated/ManagingApi'
import { WorkflowUsage, WorkflowClient } from '../../../generated/ManagingApi'
import type { IWorkflow, IFlowItem } from '../../../global/type'
import { Toast } from '../../mollecules'
import FeedTicker from './flows/feed/feedTicker'
import RsiDivergenceFlow from './flows/strategies/RsiDivergenceFlow'
import OpenPositionFlow from './flows/trading/OpenPositionFlow'
import WorkflowForm from './workflowForm'
import WorkflowSidebar from './workflowSidebar'
let id = 0
const getId = () => `dndnode_${id++}`
const mapToFlowRequest = (
flow: IFlow,
id: string,
parentId: string
): SyntheticFlow => {
return {
id: id,
parameters: flow.parameters as SyntheticFlowParameter[],
parentId: parentId,
type: flow.type,
}
}
const WorkflowCanvas: React.FC = (props: any) => {
const tabs = props
const properties = tabs['data-props'] as IWorkflow
const reactFlowWrapper = useRef(null)
const [reactFlowInstance, setReactFlowInstance] = useState(null)
const [isUpdated, setIsUpdated] = useState(false)
const { apiUrl } = useApiUrlStore()
const [usage, setUsage] = useState<WorkflowUsage>(WorkflowUsage.Trading)
const [name, setName] = useState<string>(properties.name)
const client = new WorkflowClient({}, apiUrl)
const nodeTypes: NodeTypes = useMemo(
() => ({
FeedTicker: FeedTicker,
OpenPosition: OpenPositionFlow,
RsiDivergence: RsiDivergenceFlow,
}),
[]
)
const {
nodes,
edges,
onNodesChange,
onEdgesChange,
onConnect,
initWorkFlow,
setNodes,
resetWorkflow,
} = useWorkflowStore(WorkflowSelector)
const { data, isLoading } = useQuery({
onSuccess: (data) => {
initWorkFlow([], properties.edges)
if (data) {
setNodes(properties.nodes.map((n, i) => mapToNodeItem(n, data, i)))
}
},
queryFn: () => client.workflow_GetAvailableFlows(),
queryKey: ['availableFlows'],
})
const mapToNodeItem = (
flow: Node<IFlow>,
availableFlows: IFlow[],
index: number
): IFlowItem => {
const nodeData = availableFlows.find((f) => f.type === flow.type)
if (nodeData == null) {
return {} as IFlowItem
}
nodeData.parameters = flow.data.parameters
return {
data: nodeData,
id: flow.id,
isConnectable: nodeData.acceptedInputs?.length > 0,
position: { x: index * 400, y: index * 100 },
type: flow.type as FlowType,
}
}
const isValidConnection = (connection: any) => {
if (reactFlowInstance == null) {
return false
}
const sourceData: IFlow = reactFlowInstance.getNode(connection.source).data
const targetData: IFlow = reactFlowInstance.getNode(connection.target).data
return sourceData.outputTypes?.some(
(output) => targetData.acceptedInputs?.indexOf(output) >= 0
)
}
const handleOnConnect = useCallback((params: any) => {
setIsUpdated(true)
onConnect(params)
}, [])
const onDragOver = useCallback((event: any) => {
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
}, [])
const onDrop = useCallback(
(event: any) => {
event.preventDefault()
if (reactFlowInstance == null || reactFlowWrapper.current == null) {
return
}
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect()
const data = event.dataTransfer.getData('application/reactflow')
if (typeof data === 'undefined' || !data) {
return
}
const properties: string[] = data.split('-')
const position = reactFlowInstance.project({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top,
})
const flow = flows.find((flow) => flow.type === properties[0])
const newNode: IFlowItem = {
data: flow || ({ parameters: [] as SyntheticFlowParameter[] } as IFlow),
id: getId(),
isConnectable:
(flow?.acceptedInputs && flow?.acceptedInputs?.length > 0) ?? false,
position,
type: properties[0] as FlowType,
}
newNode.data.parameters = []
setNodes(nodes.concat(newNode))
},
[reactFlowInstance, data, nodes]
)
const handleOnSave = () => {
const t = new Toast('Saving workflow')
const nodesRequest: SyntheticFlow[] = []
const client = new WorkflowClient({}, apiUrl)
if (edges.length <= 0) {
t.update('error', 'Workflow must have at least one edge')
return
}
for (const node of nodes) {
const data: IFlow = reactFlowInstance.getNode(node.id).data
const parentId = edges.find((edge) => edge.target === node.id)?.source
nodesRequest.push(mapToFlowRequest(data, node.id, parentId || ''))
}
const request: SyntheticWorkflow = {
description: 'Test',
flows: nodesRequest,
name: properties.name,
usage: usage,
}
client
.workflow_PostWorkflow(request)
.then((data) => {
t.update('success', 'Workflow saved')
})
.catch((err) => {
t.update('error', 'Error :' + err)
})
}
const handleOnReset = () => {
resetWorkflow()
}
const handleUsageChange = (event: any) => {
setUsage(event.target.value)
}
return (
<>
<WorkflowForm
name={name}
usage={usage}
handleUsageChange={handleUsageChange}
handleOnSave={handleOnSave}
handleOnReset={handleOnReset}
isUpdated={isUpdated}
setName={setName}
/>
<div className="grid grid-cols-10 gap-4">
<ReactFlowProvider>
<div>
<WorkflowSidebar flows={data} isLoading={isLoading} />
</div>
<div
className="reactflow-wrapper col-span-8"
ref={reactFlowWrapper}
style={{ height: 800 }}
>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={handleOnConnect}
onDrop={onDrop}
onInit={setReactFlowInstance}
onDragOver={onDragOver}
nodeTypes={nodeTypes}
isValidConnection={isValidConnection}
fitView
>
<Controls />
<Background />
</ReactFlow>
</div>
</ReactFlowProvider>
</div>
</>
)
}
export default WorkflowCanvas

View File

@@ -1,69 +0,0 @@
import { WorkflowUsage } from '../../../generated/ManagingApi'
import { FormInput } from '../../mollecules'
type IWorkflowForm = {
name: string
usage: WorkflowUsage
handleUsageChange: (evt: any) => void
handleOnSave: () => void
handleOnReset: () => void
isUpdated: boolean
setName: (name: string) => void
}
const WorkflowForm = ({
name,
handleUsageChange,
handleOnSave,
isUpdated,
handleOnReset,
setName,
}: IWorkflowForm) => {
return (
<>
<div className="flex">
<div className="flex w-full">
<FormInput label="Name" htmlFor="name" inline={true}>
<input
id="name"
name="text"
onChange={(evt) => setName(evt.target.value)}
className="nodrag input"
value={name}
/>
</FormInput>
<FormInput label="Usage" htmlFor="usage" inline={true}>
<select
className="select no-drag w-full max-w-xs"
onChange={handleUsageChange}
>
{Object.keys(WorkflowUsage).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
</div>
<div className="flex justify-end w-full">
<button
className="btn btn-primary"
disabled={!isUpdated}
onClick={handleOnSave}
>
Save
</button>
<button
className="btn btn-primary"
disabled={!isUpdated}
onClick={handleOnReset}
>
Reset
</button>
</div>
</div>
</>
)
}
export default WorkflowForm

View File

@@ -1,35 +0,0 @@
import type { IFlow } from '../../../generated/ManagingApi'
import { Loader } from '../../atoms'
import FlowItem from './flowItem'
type IWorkflowSidebar = {
flows?: IFlow[]
isLoading: boolean
}
const WorkflowSidebar = ({ flows, isLoading = true }: IWorkflowSidebar) => {
const onDragStart = (event: any, data: string) => {
event.dataTransfer.setData('application/reactflow', data)
event.dataTransfer.effectAllowed = 'move'
}
if (isLoading) {
return <Loader />
}
return (
<aside>
<div className="bg-base-200 p-4">
<div className="mb-2 text-lg">Flows</div>
{flows
? flows.map((flow) => (
<FlowItem flow={flow} onDragStart={onDragStart} />
))
: null}
</div>
</aside>
)
}
export default WorkflowSidebar

View File

@@ -3527,169 +3527,6 @@ export class UserClient extends AuthorizedApiBase {
} }
} }
export class WorkflowClient extends AuthorizedApiBase {
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(configuration: IConfig, baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
super(configuration);
this.http = http ? http : window as any;
this.baseUrl = baseUrl ?? "http://localhost:5000";
}
workflow_PostWorkflow(workflowRequest: SyntheticWorkflow): Promise<Workflow> {
let url_ = this.baseUrl + "/Workflow";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(workflowRequest);
let options_: RequestInit = {
body: content_,
method: "POST",
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.processWorkflow_PostWorkflow(_response);
});
}
protected processWorkflow_PostWorkflow(response: Response): Promise<Workflow> {
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 Workflow;
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<Workflow>(null as any);
}
workflow_GetWorkflows(): Promise<SyntheticWorkflow[]> {
let url_ = this.baseUrl + "/Workflow";
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.processWorkflow_GetWorkflows(_response);
});
}
protected processWorkflow_GetWorkflows(response: Response): Promise<SyntheticWorkflow[]> {
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 SyntheticWorkflow[];
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<SyntheticWorkflow[]>(null as any);
}
workflow_DeleteWorkflow(name: string | null | undefined): Promise<FileResponse> {
let url_ = this.baseUrl + "/Workflow?";
if (name !== undefined && name !== null)
url_ += "name=" + encodeURIComponent("" + name) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "DELETE",
headers: {
"Accept": "application/octet-stream"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processWorkflow_DeleteWorkflow(_response);
});
}
protected processWorkflow_DeleteWorkflow(response: Response): Promise<FileResponse> {
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 || status === 206) {
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
if (fileName) {
fileName = decodeURIComponent(fileName);
} else {
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
}
return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; });
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<FileResponse>(null as any);
}
workflow_GetAvailableFlows(): Promise<IFlow[]> {
let url_ = this.baseUrl + "/Workflow/flows";
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.processWorkflow_GetAvailableFlows(_response);
});
}
protected processWorkflow_GetAvailableFlows(response: Response): Promise<IFlow[]> {
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 IFlow[];
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<IFlow[]>(null as any);
}
}
export interface Account { export interface Account {
name: string; name: string;
exchange: TradingExchanges; exchange: TradingExchanges;
@@ -4796,68 +4633,6 @@ export interface LoginRequest {
message: string; message: string;
} }
export interface Workflow {
name: string;
usage: WorkflowUsage;
flows: IFlow[];
description: string;
}
export enum WorkflowUsage {
Trading = "Trading",
Task = "Task",
}
export interface IFlow {
id: string;
name: string;
type: FlowType;
description: string;
acceptedInputs: FlowOutput[];
children?: IFlow[] | null;
parameters: FlowParameter[];
parentId?: string;
output?: string | null;
outputTypes: FlowOutput[];
}
export enum FlowType {
RsiDivergence = "RsiDivergence",
FeedTicker = "FeedTicker",
OpenPosition = "OpenPosition",
}
export enum FlowOutput {
Signal = "Signal",
Candles = "Candles",
Position = "Position",
MoneyManagement = "MoneyManagement",
}
export interface FlowParameter {
value?: any | null;
name?: string | null;
}
export interface SyntheticWorkflow {
name: string;
usage: WorkflowUsage;
description: string;
flows: SyntheticFlow[];
}
export interface SyntheticFlow {
id: string;
parentId?: string | null;
type: FlowType;
parameters: SyntheticFlowParameter[];
}
export interface SyntheticFlowParameter {
value: string;
name: string;
}
export interface FileResponse { export interface FileResponse {
data: Blob; data: Blob;
status: number; status: number;

View File

@@ -1116,68 +1116,6 @@ export interface LoginRequest {
message: string; message: string;
} }
export interface Workflow {
name: string;
usage: WorkflowUsage;
flows: IFlow[];
description: string;
}
export enum WorkflowUsage {
Trading = "Trading",
Task = "Task",
}
export interface IFlow {
id: string;
name: string;
type: FlowType;
description: string;
acceptedInputs: FlowOutput[];
children?: IFlow[] | null;
parameters: FlowParameter[];
parentId?: string;
output?: string | null;
outputTypes: FlowOutput[];
}
export enum FlowType {
RsiDivergence = "RsiDivergence",
FeedTicker = "FeedTicker",
OpenPosition = "OpenPosition",
}
export enum FlowOutput {
Signal = "Signal",
Candles = "Candles",
Position = "Position",
MoneyManagement = "MoneyManagement",
}
export interface FlowParameter {
value?: any | null;
name?: string | null;
}
export interface SyntheticWorkflow {
name: string;
usage: WorkflowUsage;
description: string;
flows: SyntheticFlow[];
}
export interface SyntheticFlow {
id: string;
parentId?: string | null;
type: FlowType;
parameters: SyntheticFlowParameter[];
}
export interface SyntheticFlowParameter {
value: string;
name: string;
}
export interface FileResponse { export interface FileResponse {
data: Blob; data: Blob;
status: number; status: number;

View File

@@ -1,21 +1,16 @@
import type {TableInstance, UsePaginationInstanceProps, UsePaginationState, UseSortByInstanceProps,} from 'react-table' import type {TableInstance, UsePaginationInstanceProps, UsePaginationState, UseSortByInstanceProps,} from 'react-table'
import type {Edge, Node} from 'reactflow'
import type { import type {
Account, Account,
AccountType, AccountType,
Backtest, Backtest,
Balance, Balance,
BotType,
FlowOutput,
FlowType,
IFlow,
IndicatorViewModel, IndicatorViewModel,
LightSignal,
MoneyManagement, MoneyManagement,
Position, Position,
RiskLevel, RiskLevel,
ScenarioViewModel, ScenarioViewModel,
Signal,
Ticker, Ticker,
Timeframe, Timeframe,
TradeDirection, TradeDirection,
@@ -36,48 +31,6 @@ export type TableInstanceWithHooks<T extends object> = TableInstance<T> &
state: UsePaginationState<T> state: UsePaginationState<T>
} }
export type IWidgetProperties = {
id: string
title: string
layout: {
h: number
i: string
w: number
x: number
y: number
minW: number
minH: number
}
}
export type IFlowData = {
name: string
type: string
}
export type IFlowItem = {
data: IFlow
isConnectable: boolean
id: string
position: any
type: FlowType
}
export type IFlowProps = {
name: string
description?: string
children: React.ReactNode
inputs: FlowOutput[]
outputs: FlowOutput[]
isConnectable: boolean
}
export type IWorkflow = {
name: string
nodes: Node<IFlow>[]
edges: Edge[]
}
export type ILoginFormInput = { export type ILoginFormInput = {
name: string name: string
} }
@@ -106,7 +59,6 @@ export type ISpotlightBadge = {
export type IBacktestsFormInput = { export type IBacktestsFormInput = {
accountName: string accountName: string
tickers: string[] tickers: string[]
botType: BotType
timeframe: Timeframe timeframe: Timeframe
scenarioName: string scenarioName: string
save: boolean save: boolean
@@ -161,7 +113,6 @@ export type IMoneyManagementModalProps = {
export type IBacktestFormInput = { export type IBacktestFormInput = {
accountName: string accountName: string
botType: BotType
ticker: Ticker ticker: Ticker
timeframe: Timeframe timeframe: Timeframe
save: boolean save: boolean
@@ -262,7 +213,7 @@ export type ICardPosition = {
} }
export type ICardSignal = { export type ICardSignal = {
signals: Signal[] signals: LightSignal[]
} }
export type INavItemProps = { export type INavItemProps = {

View File

@@ -1,98 +0,0 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import type { Edge, Node } from 'reactflow'
import useApiUrlStore from '../../app/store/apiStore'
import { Loader } from '../../components/atoms'
import { Tabs } from '../../components/mollecules'
import { WorkflowCanvas } from '../../components/organism'
import type {
IFlow,
SyntheticFlow,
SyntheticWorkflow,
} from '../../generated/ManagingApi'
import { WorkflowClient } from '../../generated/ManagingApi'
import type { ITabsType, IWorkflow } from '../../global/type'
const mapWorkflowToTabs = (workflows: SyntheticWorkflow[]): ITabsType => {
return workflows.map((workflow: SyntheticWorkflow, index: number) => {
return {
Component: WorkflowCanvas,
index: index,
label: workflow.name,
props: mapFlowsToNodes(workflow.flows, workflow.name),
}
})
}
const mapFlowsToNodes = (flows: SyntheticFlow[], name: string): IWorkflow => {
const nodes: Node<IFlow>[] = []
const edges: Edge[] = []
flows.forEach((flow: SyntheticFlow) => {
nodes.push(mapFlowToNode(flow))
})
for (const node of nodes) {
const childrenNodes = nodes.filter((n) => n.data.parentId == node.data.id)
if (childrenNodes.length > 0) {
childrenNodes.forEach((childNode) => {
edges.push({
id: `${node.id}-${childNode.id}`,
source: node.id,
target: childNode.id,
})
})
}
}
return { edges, name: name, nodes } as IWorkflow
}
const mapFlowToNode = (flow: SyntheticFlow): Node => {
return {
data: flow,
id: flow.id,
position: { x: 0, y: 0 },
type: flow.type,
}
}
const Workflows: React.FC = () => {
const [selectedTab, setSelectedTab] = useState<number>(1)
const { apiUrl } = useApiUrlStore()
const client = new WorkflowClient({}, apiUrl)
const { data, isLoading } = useQuery({
onSuccess: () => {
setSelectedTab(0)
},
queryFn: () => client.workflow_GetWorkflows(),
queryKey: ['workflows'],
})
useEffect(() => {}, [isLoading])
if (isLoading || data == null) {
return <Loader></Loader>
}
return (
<div>
<div className="container mx-auto">
<Tabs
selectedTab={selectedTab}
onClick={setSelectedTab}
tabs={mapWorkflowToTabs(data)}
addButton={true}
onAddButton={() => {
console.log('add button clicked')
}}
/>
</div>
</div>
)
}
export default Workflows