Sync with latest version of clone executor
This commit is contained in:
1 parent
3dd3e6dbd6
commit
9a2d671a97
14 files changed
+500
-452
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@superalgos/operations-client",
|
||||
"version": "0.0.25",
|
||||
"version": "0.0.27",
|
||||
"description": "Superalgos Operations client-side web app",
|
||||
"main": "src/index.js",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const clone = gql`
|
||||
fragment clone on operations_Clone {
|
||||
id
|
||||
authId
|
||||
teamId
|
||||
botId
|
||||
mode
|
||||
resumeExecution
|
||||
beginDatetime
|
||||
endDatetime
|
||||
waitTime
|
||||
state
|
||||
stateDatetime
|
||||
createDatetime
|
||||
lastLogs
|
||||
runAsTeam
|
||||
summaryDate
|
||||
buyAverage
|
||||
sellAverage
|
||||
marketRate
|
||||
combinedProfitsA
|
||||
combinedProfitsB
|
||||
assetA
|
||||
assetB
|
||||
botType
|
||||
teamName
|
||||
botName
|
||||
botAvatar
|
||||
teamAvatar
|
||||
processName
|
||||
keyId
|
||||
botSlug
|
||||
}
|
||||
`
|
||||
@@ -1,5 +1,36 @@
|
||||
import { clone } from './clone'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export {
|
||||
clone
|
||||
}
|
||||
export const clone = gql`
|
||||
fragment clone on operations_Clone {
|
||||
id
|
||||
authId
|
||||
teamId
|
||||
botId
|
||||
mode
|
||||
resumeExecution
|
||||
beginDatetime
|
||||
endDatetime
|
||||
waitTime
|
||||
state
|
||||
stateDatetime
|
||||
createDatetime
|
||||
lastLogs
|
||||
runAsTeam
|
||||
summaryDate
|
||||
buyAverage
|
||||
sellAverage
|
||||
marketRate
|
||||
combinedProfitsA
|
||||
combinedProfitsB
|
||||
assetA
|
||||
assetB
|
||||
botType
|
||||
teamName
|
||||
botName
|
||||
botAvatar
|
||||
teamAvatar
|
||||
processName
|
||||
keyId
|
||||
botSlug
|
||||
}
|
||||
`
|
||||
@@ -29,22 +29,36 @@ const tradingStartModesList = {
|
||||
const indicatorStartModes = {
|
||||
allMonths: 'All Months',
|
||||
oneMonth: 'One Month',
|
||||
noTime: 'No Time'
|
||||
noTime: 'Head of the Market'
|
||||
}
|
||||
|
||||
const processNames = {
|
||||
const sensorProcessNames = {
|
||||
Live: "Live-Trades",
|
||||
HoleFixing: "Hole-Fixing"
|
||||
}
|
||||
|
||||
const indicatorProcessNames = {
|
||||
Daily: 'Multi-Period-Daily',
|
||||
Market: 'Multi-Period-Market',
|
||||
Min: "One-Min-Daily-Candles-Volumes",
|
||||
Live: "Poloniex-Live-Trades",
|
||||
HoleFixing: "Poloniex-Hole-Fixing"
|
||||
Market: 'Multi-Period-Market'
|
||||
}
|
||||
|
||||
const exchanges = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Poloniex'
|
||||
}
|
||||
]
|
||||
const tradingProcessNames = {
|
||||
TradingProcess: "Trading-Process"
|
||||
}
|
||||
|
||||
export { tradingStartModes, tradingStartModesList, exchanges, indicatorStartModes, availableMonths, processNames, availableTimePeriods }
|
||||
const botTypes = {
|
||||
Trading: "Trading",
|
||||
Indicator: "Indicator",
|
||||
Sensor: "Sensor"
|
||||
}
|
||||
|
||||
const exchanges = {
|
||||
Coss: "Coss",
|
||||
Poloniex: "Poloniex",
|
||||
Binance: "Binance"
|
||||
}
|
||||
|
||||
export {
|
||||
tradingStartModes, tradingStartModesList, exchanges, indicatorStartModes, availableMonths,
|
||||
availableTimePeriods, sensorProcessNames, indicatorProcessNames, tradingProcessNames, botTypes
|
||||
}
|
||||
+387
-381
@@ -6,24 +6,28 @@ import TopBar from '../../BannerTopBar'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import classNames from 'classnames'
|
||||
import styles from './styles'
|
||||
import { tradingStartModes, indicatorStartModes, availableMonths, processNames, availableTimePeriods } from '../../../GraphQL/models'
|
||||
import { isDefined, getIndicatorYears } from '../../../utils'
|
||||
import {
|
||||
MenuItem, Button, TextField, FormControl, InputLabel, Input, Typography,
|
||||
Paper, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions,
|
||||
Select, FormControlLabel, Checkbox, FormHelperText
|
||||
tradingStartModes, indicatorStartModes, availableMonths, availableTimePeriods,
|
||||
sensorProcessNames, indicatorProcessNames, tradingProcessNames, botTypes, exchanges
|
||||
} from '../../../GraphQL/models'
|
||||
|
||||
import {
|
||||
MenuItem, Button, TextField, FormControl, InputLabel, Input, Typography,
|
||||
Paper, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions,
|
||||
Select, FormControlLabel, Checkbox, FormHelperText
|
||||
} from '@material-ui/core'
|
||||
import { DateTimePicker } from 'material-ui-pickers'
|
||||
import { ChevronLeft, ChevronRight } from '@material-ui/icons'
|
||||
|
||||
class AddClone extends Component {
|
||||
|
||||
constructor(props){
|
||||
constructor(props) {
|
||||
super(props)
|
||||
let user = localStorage.getItem('user')
|
||||
this.state = {
|
||||
user: JSON.parse(user),
|
||||
selectedBot: {'id':''},
|
||||
selectedBot: { 'id': '' },
|
||||
mode: '',
|
||||
resumeExecution: true,
|
||||
beginDatetime: DateTime.local().minus({ days: 8 }).startOf('day'),
|
||||
@@ -36,12 +40,13 @@ class AddClone extends Component {
|
||||
teams: [],
|
||||
teamId: '',
|
||||
keyId: '',
|
||||
exchangeName: exchanges.Coss,
|
||||
processName: '',
|
||||
|
||||
// Indicator Bot
|
||||
startYear: 2019,
|
||||
endYear: 2019,
|
||||
month: 1,
|
||||
processName: '',
|
||||
|
||||
//TradingBot
|
||||
timePeriod: '01-hs',
|
||||
@@ -61,7 +66,7 @@ class AddClone extends Component {
|
||||
|
||||
render() {
|
||||
const { classes } = this.props
|
||||
if( !isDefined(this.state.user) ) {
|
||||
if (!isDefined(this.state.user)) {
|
||||
return (
|
||||
<TopBar
|
||||
size='big'
|
||||
@@ -71,18 +76,18 @@ class AddClone extends Component {
|
||||
/>
|
||||
)
|
||||
} else return (
|
||||
<React.Fragment>
|
||||
<TopBar
|
||||
size='medium'
|
||||
title='Create a bot clone'
|
||||
text='Create a bot clone here.'
|
||||
backgroundUrl='https://superalgos.org/img/photos/ecosystem.jpg'
|
||||
/>
|
||||
<React.Fragment>
|
||||
<TopBar
|
||||
size='medium'
|
||||
title='Create a bot clone'
|
||||
text='Create a bot clone here.'
|
||||
backgroundUrl='https://superalgos.org/img/photos/ecosystem.jpg'
|
||||
/>
|
||||
|
||||
<div className='container'>
|
||||
<div className='container'>
|
||||
<Paper className={classNames('container', classes.root)}>
|
||||
|
||||
<form noValidate autoComplete="off" onSubmit={this.submitForm.bind(this)}>
|
||||
<form noValidate autoComplete="off" onSubmit={this.submitForm.bind(this)}>
|
||||
<Typography className={classes.typography} variant='h5' gutterBottom>
|
||||
Create a Bot Clone
|
||||
</Typography>
|
||||
@@ -114,31 +119,31 @@ class AddClone extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
if(!isDefined(data.teams_TeamsByOwner)){
|
||||
return (
|
||||
<Select
|
||||
value='noteams'
|
||||
input={<Input />}
|
||||
displayEmpty
|
||||
className={classes.selectEmpty}
|
||||
disabled
|
||||
>
|
||||
<MenuItem key='1' value='noteams'>You don't have any teams yet.</MenuItem>
|
||||
</Select>
|
||||
)
|
||||
} else{
|
||||
if (!isDefined(data.teams_TeamsByOwner)) {
|
||||
return (
|
||||
<Select
|
||||
value='noteams'
|
||||
input={<Input />}
|
||||
displayEmpty
|
||||
className={classes.selectEmpty}
|
||||
disabled
|
||||
>
|
||||
<MenuItem key='1' value='noteams'>You don't have any teams yet.</MenuItem>
|
||||
</Select>
|
||||
)
|
||||
} else {
|
||||
this.teams = data.teams_TeamsByOwner
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={this.state.selectedBot.id}
|
||||
displayEmpty
|
||||
className={classes.selectEmpty}
|
||||
onBlur={(e)=>this.setState({botError:false})}
|
||||
error={this.state.botError}
|
||||
onChange={e => this.setSelectedBot(e.target.value)}
|
||||
>
|
||||
<Select
|
||||
value={this.state.selectedBot.id}
|
||||
displayEmpty
|
||||
className={classes.selectEmpty}
|
||||
onBlur={(e) => this.setState({ botError: false })}
|
||||
error={this.state.botError}
|
||||
onChange={e => this.setSelectedBot(e.target.value)}
|
||||
>
|
||||
{
|
||||
data.teams_TeamsByOwner.map(team => (
|
||||
team.fb.map(financialBeing => (
|
||||
@@ -148,347 +153,356 @@ class AddClone extends Component {
|
||||
))
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</Select>
|
||||
)
|
||||
}}
|
||||
</Query>
|
||||
<FormHelperText>{this.state.selectedBot.kind}</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
{ this.state.selectedBot.kind === "Trading" &&
|
||||
<React.Fragment>
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
Available Running Modes are Backtest and Live.
|
||||
{this.state.selectedBot.kind === botTypes.Trading &&
|
||||
<React.Fragment>
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
Available Running Modes are Backtest and Live.
|
||||
<ul>
|
||||
<li> Backtest mode does NOT put real orders at the exchange;
|
||||
it simulates the order execution at the specified price.
|
||||
|
||||
<li> Backtest mode does NOT put real orders at the exchange;
|
||||
it simulates the order execution at the specified price.
|
||||
</li>
|
||||
<li>
|
||||
Live mode DOES put real orders at the exchange.
|
||||
<li>
|
||||
Live mode DOES put real orders at the exchange.
|
||||
</li>
|
||||
</ul>
|
||||
</Typography>
|
||||
</ul>
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
select
|
||||
label="Running Mode"
|
||||
className={classNames(classes.margin, classes.textField, classes.form)}
|
||||
value={this.state.mode}
|
||||
onChange={(e)=> this.setState({mode:e.target.value})}
|
||||
onBlur={(e)=>this.setState({modeError:false})}
|
||||
error={this.state.modeError}
|
||||
fullWidth
|
||||
>
|
||||
{Object.keys(tradingStartModes).map(option => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{tradingStartModes[option]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{ this.state.mode == "backtest" &&
|
||||
<React.Fragment>
|
||||
<DateTimePicker
|
||||
autoOk
|
||||
disableFuture
|
||||
format="DD' at 'HH:mm"
|
||||
ampm={false}
|
||||
showTabs={false}
|
||||
leftArrowIcon={<ChevronLeft />}
|
||||
rightArrowIcon={<ChevronRight />}
|
||||
value={this.state.beginDatetime}
|
||||
label='Backtest Start Date'
|
||||
onChange={newVal => this.setState({ beginDatetime: newVal })}
|
||||
className={classNames(classes.form, classes.textField)}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField label="Running Mode"
|
||||
select
|
||||
className={classNames(classes.margin, classes.textField, classes.form)}
|
||||
value={this.state.mode}
|
||||
onChange={(e) => this.setState({ mode: e.target.value })}
|
||||
onBlur={(e) => this.setState({ modeError: false })}
|
||||
error={this.state.modeError}
|
||||
fullWidth
|
||||
>
|
||||
{Object.keys(tradingStartModes).map(option => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{tradingStartModes[option]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<DateTimePicker
|
||||
autoOk
|
||||
disableFuture
|
||||
format="DD' at 'HH:mm"
|
||||
ampm={false}
|
||||
showTabs={false}
|
||||
leftArrowIcon={<ChevronLeft />}
|
||||
rightArrowIcon={<ChevronRight />}
|
||||
value={this.state.endDatetime}
|
||||
label='Backtest End Date'
|
||||
onChange={newVal => this.setState({ endDatetime: newVal })}
|
||||
className={classNames(classes.form, classes.textField)}
|
||||
fullWidth
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
{this.state.mode == "backtest" &&
|
||||
<React.Fragment>
|
||||
<DateTimePicker
|
||||
autoOk
|
||||
disableFuture
|
||||
format="DD' at 'HH:mm"
|
||||
ampm={false}
|
||||
showTabs={false}
|
||||
leftArrowIcon={<ChevronLeft />}
|
||||
rightArrowIcon={<ChevronRight />}
|
||||
value={this.state.beginDatetime}
|
||||
label='Backtest Start Date'
|
||||
onChange={newVal => this.setState({ beginDatetime: newVal })}
|
||||
className={classNames(classes.form, classes.textField)}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
{ this.state.mode == "live" &&
|
||||
<React.Fragment>
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
Select one of your available Exchange API Keys to be used by this clone.
|
||||
<DateTimePicker
|
||||
autoOk
|
||||
disableFuture
|
||||
format="DD' at 'HH:mm"
|
||||
ampm={false}
|
||||
showTabs={false}
|
||||
leftArrowIcon={<ChevronLeft />}
|
||||
rightArrowIcon={<ChevronRight />}
|
||||
value={this.state.endDatetime}
|
||||
label='Backtest End Date'
|
||||
onChange={newVal => this.setState({ endDatetime: newVal })}
|
||||
className={classNames(classes.form, classes.textField)}
|
||||
fullWidth
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
{this.state.mode == "live" &&
|
||||
<React.Fragment>
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
Select one of your available Exchange API Keys to be used by this clone.
|
||||
</Typography>
|
||||
|
||||
<Query
|
||||
query={keys.GET_ALL_KEYS}
|
||||
>
|
||||
{({ loading, error, data }) => {
|
||||
if (loading) return (
|
||||
<TextField
|
||||
select
|
||||
label="Exchange API Key"
|
||||
className={classNames(classes.margin, classes.textField, classes.form)}
|
||||
value={this.state.keyId}
|
||||
onChange={(e)=> this.setState({keyId:e.target.value})}
|
||||
onBlur={(e)=>this.setState({keyIdError:false})}
|
||||
error={this.state.keyIdError}
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem key='1' value='Loading keys...'>
|
||||
'Loading keys...'
|
||||
<Query
|
||||
query={keys.GET_ALL_KEYS}
|
||||
>
|
||||
{({ loading, error, data }) => {
|
||||
if (loading) return (
|
||||
<TextField
|
||||
select
|
||||
label="Exchange API Key"
|
||||
className={classNames(classes.margin, classes.textField, classes.form)}
|
||||
value={this.state.keyId}
|
||||
onChange={(e) => this.setState({ keyId: e.target.value })}
|
||||
onBlur={(e) => this.setState({ keyIdError: false })}
|
||||
error={this.state.keyIdError}
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem key='1' value='Loading keys...'>
|
||||
'Loading keys...'
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
);
|
||||
if (error) return `Error! ${error.message}`;
|
||||
const list = data.keyVault_AvailableKeys.map((key) => (
|
||||
<MenuItem key={key.id} value={key.id}>
|
||||
{key.key}
|
||||
</MenuItem>
|
||||
));
|
||||
return (
|
||||
<TextField
|
||||
select
|
||||
label="Exchange API Key"
|
||||
className={classNames(classes.margin, classes.textField, classes.form)}
|
||||
value={this.state.keyId}
|
||||
onChange={(e)=> this.setState({keyId:e.target.value})}
|
||||
onBlur={(e)=>this.setState({keyIdError:false})}
|
||||
error={this.state.keyIdError}
|
||||
fullWidth
|
||||
>
|
||||
{ list }
|
||||
</TextField>
|
||||
);
|
||||
}}
|
||||
</Query>
|
||||
</TextField>
|
||||
);
|
||||
if (error) return `Error! ${error.message}`;
|
||||
const list = data.keyVault_AvailableKeys.map((key) => (
|
||||
<MenuItem key={key.id} value={key.id}>
|
||||
{key.key}
|
||||
</MenuItem>
|
||||
));
|
||||
return (
|
||||
<TextField
|
||||
select
|
||||
label="Exchange API Key"
|
||||
className={classNames(classes.margin, classes.textField, classes.form)}
|
||||
value={this.state.keyId}
|
||||
onChange={(e) => this.setState({ keyId: e.target.value })}
|
||||
onBlur={(e) => this.setState({ keyIdError: false })}
|
||||
error={this.state.keyIdError}
|
||||
fullWidth
|
||||
>
|
||||
{list}
|
||||
</TextField>
|
||||
);
|
||||
}}
|
||||
</Query>
|
||||
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
Because we are still in early alpha-testing phase,
|
||||
Live Trading Clones are limited to an initial investment
|
||||
of 0.001 BTC. If you choose to run this bot or any modified
|
||||
version of the code, you are doing it at your own risk.
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
Because we are still in early alpha-testing phase,
|
||||
Live Trading Clones are limited to an initial investment
|
||||
of 0.001 BTC. If you choose to run this bot or any modified
|
||||
version of the code, you are doing it at your own risk.
|
||||
</Typography>
|
||||
|
||||
</React.Fragment>
|
||||
}
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
|
||||
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
The Time Period in which the bot will operate.
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
The Time Period in which the bot will operate.
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
select
|
||||
label="Time Period"
|
||||
className={classNames(classes.margin, classes.textField, classes.form)}
|
||||
value={this.state.timePeriod}
|
||||
onChange={(e)=> this.setState({timePeriod:e.target.value})}
|
||||
fullWidth
|
||||
>
|
||||
{availableTimePeriods.map((option, index) => (
|
||||
<TextField label="Time Period"
|
||||
select
|
||||
className={classNames(classes.margin, classes.textField, classes.form)}
|
||||
value={this.state.timePeriod}
|
||||
onChange={(e) => this.setState({ timePeriod: e.target.value })}
|
||||
fullWidth
|
||||
>
|
||||
{availableTimePeriods.map((option, index) => (
|
||||
<MenuItem key={index} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
{this.state.selectedBot.kind === botTypes.Indicator &&
|
||||
<React.Fragment>
|
||||
<TextField label="Process Name"
|
||||
select
|
||||
className={classNames(classes.textField, classes.form)}
|
||||
value={this.state.processName}
|
||||
onChange={(e) => this.setState({ processName: e.target.value })}
|
||||
onBlur={(e) => this.setState({ processNameError: false })}
|
||||
error={this.state.processNameError}
|
||||
fullWidth
|
||||
>
|
||||
{Object.keys(indicatorProcessNames).map((option, index) => (
|
||||
<MenuItem key={index} value={indicatorProcessNames[option]}>
|
||||
{indicatorProcessNames[option]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
{this.state.selectedBot.kind === botTypes.Sensor &&
|
||||
<React.Fragment>
|
||||
<TextField label="Process Name"
|
||||
select
|
||||
className={classNames(classes.textField, classes.form)}
|
||||
value={this.state.processName}
|
||||
onChange={(e) => this.setState({ processName: e.target.value })}
|
||||
onBlur={(e) => this.setState({ processNameError: false })}
|
||||
error={this.state.processNameError}
|
||||
fullWidth
|
||||
>
|
||||
{Object.keys(sensorProcessNames).map((option, index) => (
|
||||
<MenuItem key={index} value={sensorProcessNames[option]}>
|
||||
{sensorProcessNames[option]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
{(this.state.selectedBot.kind === botTypes.Indicator
|
||||
|| this.state.selectedBot.kind === botTypes.Sensor) &&
|
||||
<React.Fragment>
|
||||
<TextField label="Running Mode"
|
||||
select
|
||||
className={classNames(classes.margin, classes.textField, classes.form)}
|
||||
value={this.state.mode}
|
||||
onChange={(e) => this.setState({ mode: e.target.value })}
|
||||
onBlur={(e) => this.setState({ modeError: false })}
|
||||
error={this.state.modeError}
|
||||
fullWidth
|
||||
>
|
||||
{Object.keys(indicatorStartModes).map(option => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{indicatorStartModes[option]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{this.state.mode === "allMonths" &&
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
id="beginYearInput"
|
||||
select
|
||||
label="Start Year"
|
||||
className={classNames(classes.textField, classes.form)}
|
||||
value={this.state.startYear}
|
||||
onChange={(e) => this.setState({ startYear: e.target.value })}
|
||||
fullWidth
|
||||
>
|
||||
{getIndicatorYears().map((option, index) => (
|
||||
<MenuItem key={option} value={index}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
id="endYearInput"
|
||||
select
|
||||
label="End Year"
|
||||
className={classNames(classes.textField, classes.form)}
|
||||
value={this.state.endYear}
|
||||
onChange={(e) => this.setState({ endYear: e.target.value })}
|
||||
fullWidth
|
||||
>
|
||||
{getIndicatorYears().map((option, index) => (
|
||||
<MenuItem key={option} value={index}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
{this.state.mode === "oneMonth" &&
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
id="beginYearInput"
|
||||
select
|
||||
label="Year"
|
||||
className={classNames(classes.textField, classes.form)}
|
||||
value={this.state.startYear}
|
||||
onChange={(e) => this.setState({ startYear: e.target.value })}
|
||||
fullWidth
|
||||
>
|
||||
{getIndicatorYears().map((option, index) => (
|
||||
<MenuItem key={index} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{ false &&
|
||||
<React.Fragment>
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
If Run as Team is selected, the clone will be taken from the
|
||||
team respository instead of your own team member folder.
|
||||
Competitions runs from the team respository.
|
||||
</Typography>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={this.state.runAsTeam}
|
||||
onChange={(e)=>this.setState({runAsTeam:e.target.checked })}
|
||||
value="runAsTeam"
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="Run as Team"
|
||||
className={classNames(classes.form, classes.textField)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
{ (this.state.selectedBot.kind === "Indicator"
|
||||
|| this.state.selectedBot.kind === "Sensor") &&
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
select
|
||||
label="Process Name"
|
||||
className={classNames(classes.textField, classes.form)}
|
||||
value={this.state.processName}
|
||||
onChange={(e)=> this.setState({processName:e.target.value})}
|
||||
onBlur={(e)=>this.setState({processNameError:false})}
|
||||
error={this.state.processNameError}
|
||||
fullWidth
|
||||
>
|
||||
{Object.keys(processNames).map(option => (
|
||||
<MenuItem key={option} value={processNames[option]}>
|
||||
{processNames[option]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
|
||||
<TextField
|
||||
select
|
||||
label="Running Mode"
|
||||
className={classNames(classes.margin, classes.textField, classes.form)}
|
||||
value={this.state.mode}
|
||||
onChange={(e)=> this.setState({mode:e.target.value})}
|
||||
onBlur={(e)=>this.setState({modeError:false})}
|
||||
error={this.state.modeError}
|
||||
fullWidth
|
||||
>
|
||||
{Object.keys(indicatorStartModes).map(option => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{indicatorStartModes[option]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{ this.state.mode === "allMonths" &&
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
id="beginYearInput"
|
||||
select
|
||||
label="Start Year"
|
||||
className={classNames(classes.textField, classes.form)}
|
||||
value={this.state.startYear}
|
||||
onChange={(e)=> this.setState({startYear:e.target.value})}
|
||||
fullWidth
|
||||
>
|
||||
{getIndicatorYears().map((option, index) => (
|
||||
<MenuItem key={option} value={index}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
id="endYearInput"
|
||||
select
|
||||
label="End Year"
|
||||
className={classNames(classes.textField, classes.form)}
|
||||
value={this.state.endYear}
|
||||
onChange={(e)=> this.setState({endYear:e.target.value})}
|
||||
fullWidth
|
||||
>
|
||||
{getIndicatorYears().map((option, index) => (
|
||||
<MenuItem key={option} value={index}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
{ this.state.mode === "oneMonth" &&
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
id="beginYearInput"
|
||||
select
|
||||
label="Year"
|
||||
className={classNames(classes.textField, classes.form)}
|
||||
value={this.state.startYear}
|
||||
onChange={(e)=> this.setState({startYear:e.target.value})}
|
||||
fullWidth
|
||||
>
|
||||
{getIndicatorYears().map((option, index) => (
|
||||
<MenuItem key={index} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
id="endMonthInput"
|
||||
select
|
||||
label="Month"
|
||||
className={classNames(classes.textField, classes.form)}
|
||||
value={this.state.month}
|
||||
onChange={(e)=> this.setState({month:e.target.value})}
|
||||
fullWidth
|
||||
>
|
||||
{availableMonths.map((option, index) => (
|
||||
<MenuItem key={index} value={index+1}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
{ this.state.mode === "noTime" &&
|
||||
<React.Fragment>
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
The Resume Execution option let's you pick up the context of
|
||||
the last execution and continue from there.
|
||||
</Typography>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={this.state.resumeExecution}
|
||||
onChange={(e)=>this.setState({resumeExecution:e.target.checked })}
|
||||
value="resumeExecution"
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="Resume Execution"
|
||||
className={classNames(classes.form, classes.textField)}
|
||||
/>
|
||||
{ !this.state.resumeExecution &&
|
||||
<DateTimePicker
|
||||
autoOk
|
||||
disableFuture
|
||||
format="DD' at 'HH:mm"
|
||||
ampm={false}
|
||||
showTabs={false}
|
||||
leftArrowIcon={<ChevronLeft />}
|
||||
rightArrowIcon={<ChevronRight />}
|
||||
value={this.state.beginDatetime}
|
||||
label='Start Date'
|
||||
onChange={newVal => this.setState({ beginDatetime: newVal })}
|
||||
className={classNames(classes.form, classes.textField)}
|
||||
fullWidth
|
||||
/>
|
||||
}
|
||||
|
||||
</React.Fragment>
|
||||
}
|
||||
</React.Fragment>
|
||||
<TextField
|
||||
id="endMonthInput"
|
||||
select
|
||||
label="Month"
|
||||
className={classNames(classes.textField, classes.form)}
|
||||
value={this.state.month}
|
||||
onChange={(e) => this.setState({ month: e.target.value })}
|
||||
fullWidth
|
||||
>
|
||||
{availableMonths.map((option, index) => (
|
||||
<MenuItem key={index} value={index + 1}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</React.Fragment>
|
||||
}
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
You will be able to see the clone information under Active Clones.
|
||||
Whenever you decide to stop the clone you can delete it. On History Clones will find all deleted clones.
|
||||
|
||||
{(this.state.mode === "noTime" && this.state.selectedBot.kind !== botTypes.Sensor ) &&
|
||||
<React.Fragment>
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
The Resume Execution option let's you pick up the context of
|
||||
the last execution and continue from there.
|
||||
</Typography>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={this.state.resumeExecution}
|
||||
onChange={(e) => this.setState({ resumeExecution: e.target.checked })}
|
||||
value="resumeExecution"
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label="Resume Execution"
|
||||
className={classNames(classes.form, classes.textField)}
|
||||
/>
|
||||
{!this.state.resumeExecution &&
|
||||
<DateTimePicker
|
||||
autoOk
|
||||
disableFuture
|
||||
format="DD' at 'HH:mm"
|
||||
ampm={false}
|
||||
showTabs={false}
|
||||
leftArrowIcon={<ChevronLeft />}
|
||||
rightArrowIcon={<ChevronRight />}
|
||||
value={this.state.beginDatetime}
|
||||
label='Start Date'
|
||||
onChange={newVal => this.setState({ beginDatetime: newVal })}
|
||||
className={classNames(classes.form, classes.textField)}
|
||||
fullWidth
|
||||
/>
|
||||
}
|
||||
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
<TextField label="Exchange"
|
||||
select
|
||||
className={classNames(classes.margin, classes.textField, classes.form)}
|
||||
value={this.state.exchangeName}
|
||||
onChange={(e) => this.setState({ exchangeName: e.target.value })}
|
||||
fullWidth
|
||||
>
|
||||
{Object.keys(exchanges).map((option, index) => (
|
||||
<MenuItem key={index} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
You will be able to see the clone information under Active Clones.
|
||||
Whenever you decide to stop the clone you can delete it. On History Clones will find all deleted clones.
|
||||
</Typography>
|
||||
|
||||
<div className={classes.actionButton} >
|
||||
<Button
|
||||
type="submit"
|
||||
variant='contained' color='secondary'>
|
||||
Clone Bot!
|
||||
<Button
|
||||
type="submit"
|
||||
variant='contained' color='secondary'>
|
||||
Clone Bot!
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
@@ -511,19 +525,19 @@ class AddClone extends Component {
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Paper>
|
||||
</Paper>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
setSelectedBot(botId){
|
||||
if(isDefined(this.teams)){
|
||||
setSelectedBot(botId) {
|
||||
if (isDefined(this.teams)) {
|
||||
for (var j = 0; j < this.teams.length; j++) {
|
||||
for (var i = 0; i < this.teams[j].fb.length; i++) {
|
||||
if(this.teams[j].fb[i].id === botId){
|
||||
this.setState({selectedBot:this.teams[j].fb[i]})
|
||||
this.setState({teamId:this.teams[j].id})
|
||||
if (this.teams[j].fb[i].id === botId) {
|
||||
this.setState({ selectedBot: this.teams[j].fb[i] })
|
||||
this.setState({ teamId: this.teams[j].id })
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -531,16 +545,16 @@ class AddClone extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async submitForm(e){
|
||||
async submitForm(e) {
|
||||
e.preventDefault()
|
||||
let error = this.validate()
|
||||
if (!error){
|
||||
if (!error) {
|
||||
let serverResponse = await this.createCloneOnServer()
|
||||
error = serverResponse.errors || !isDefined(serverResponse.data.operations_AddClone)
|
||||
if(error){
|
||||
if (error) {
|
||||
this.state.serverResponse = serverResponse.errors[0].message || serverResponse.errors[0]
|
||||
this.state.serverError = true
|
||||
}else{
|
||||
} else {
|
||||
this.state.serverResponse = "The new clone was sucessfully created."
|
||||
this.state.serverError = false
|
||||
}
|
||||
@@ -549,24 +563,25 @@ class AddClone extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async createCloneOnServer(){
|
||||
async createCloneOnServer() {
|
||||
let variables = {
|
||||
clone:{
|
||||
clone: {
|
||||
teamId: this.state.teamId,
|
||||
botId: this.state.selectedBot.id,
|
||||
mode: this.state.mode,
|
||||
resumeExecution: this.state.resumeExecution,
|
||||
botType: this.getBotTypeFromKind(this.state.selectedBot.kind)
|
||||
botType: this.state.selectedBot.kind,
|
||||
exchangeName: this.state.exchangeName
|
||||
}
|
||||
}
|
||||
|
||||
if(this.state.selectedBot.kind === "Trading"){
|
||||
if (this.state.selectedBot.kind === "Trading") {
|
||||
variables.clone.runAsTeam = this.state.runAsTeam
|
||||
variables.clone.processName = 'Trading-Process'
|
||||
variables.clone.timePeriod = this.state.timePeriod
|
||||
if(this.state.mode === "backtest"){
|
||||
variables.clone.beginDatetime = this.state.beginDatetime.valueOf() / 1000|0
|
||||
variables.clone.endDatetime = this.state.endDatetime.valueOf() / 1000|0
|
||||
if (this.state.mode === "backtest") {
|
||||
variables.clone.beginDatetime = this.state.beginDatetime.valueOf() / 1000 | 0
|
||||
variables.clone.endDatetime = this.state.endDatetime.valueOf() / 1000 | 0
|
||||
variables.clone.waitTime = this.state.waitTime
|
||||
} else {
|
||||
variables.clone.keyId = this.state.keyId
|
||||
@@ -577,7 +592,7 @@ class AddClone extends Component {
|
||||
variables.clone.endYear = this.state.endYear
|
||||
variables.clone.month = this.state.month
|
||||
variables.clone.runAsTeam = true
|
||||
variables.clone.beginDatetime = this.state.beginDatetime.valueOf() / 1000|0
|
||||
variables.clone.beginDatetime = this.state.beginDatetime.valueOf() / 1000 | 0
|
||||
}
|
||||
|
||||
return this.props.addCloneMutation({
|
||||
@@ -585,15 +600,6 @@ class AddClone extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
getBotTypeFromKind(kind){
|
||||
if(kind === 'Trading')
|
||||
return 'Trading'
|
||||
else if(kind === 'Indicator')
|
||||
return 'Indicator'
|
||||
else if(kind === 'Sensor')
|
||||
return 'Extraction'
|
||||
}
|
||||
|
||||
handleNewCloneConfirmationOpen = () => {
|
||||
this.setState({ isNewCloneConfirmationOpen: true })
|
||||
};
|
||||
@@ -603,15 +609,15 @@ class AddClone extends Component {
|
||||
isNewCloneConfirmationOpen: false
|
||||
})
|
||||
|
||||
if(!this.state.serverError)
|
||||
if (!this.state.serverError)
|
||||
this.setState({
|
||||
selectedBot: {'id':''},
|
||||
selectedBot: { 'id': '' },
|
||||
mode: '',
|
||||
resumeExecution: true,
|
||||
beginDatetime: DateTime.local().minus({ days: 8 }).startOf('day'),
|
||||
endDatetime: DateTime.local(),
|
||||
waitTime: 1,
|
||||
state : '',
|
||||
state: '',
|
||||
stateDatetime: 0,
|
||||
createDatetime: 0,
|
||||
runAsTeam: false,
|
||||
@@ -633,21 +639,21 @@ class AddClone extends Component {
|
||||
})
|
||||
};
|
||||
|
||||
validate(){
|
||||
validate() {
|
||||
let isError = false
|
||||
|
||||
if(this.state.selectedBot.length < 1) {
|
||||
if (this.state.selectedBot.length < 1) {
|
||||
isError = true
|
||||
this.setState(state => ({ selectedBotError: true }));
|
||||
}
|
||||
|
||||
if(this.state.mode.length < 1) {
|
||||
if (this.state.mode.length < 1) {
|
||||
isError = true
|
||||
this.setState(state => ({ modeError: true }));
|
||||
}
|
||||
|
||||
if(this.state.selectedBot.kind !== "Trading"
|
||||
&& this.state.processName.length < 1 ) {
|
||||
if (this.state.selectedBot.kind !== "Trading"
|
||||
&& this.state.processName.length < 1) {
|
||||
isError = true
|
||||
this.setState(state => ({ processNameError: true }));
|
||||
}
|
||||
@@ -658,6 +664,6 @@ class AddClone extends Component {
|
||||
}
|
||||
|
||||
export default compose(
|
||||
graphql(clones.OPERATIONS_ADD_CLONE, { name:'addCloneMutation' }),
|
||||
graphql(clones.OPERATIONS_ADD_CLONE, { name: 'addCloneMutation' }),
|
||||
withStyles(styles)
|
||||
)(AddClone)
|
||||
@@ -3,8 +3,6 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
/src/config/clone-deployment.json
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
|
||||
+7
-7
@@ -4,10 +4,10 @@ The Superalgos Operations Server manages the clone life cycle inside the **Super
|
||||
The main function includes storing the metadata information of the clones and handling the interaction with the orchestration software which is [Kubernetes](https://Kubernetes.io).
|
||||
|
||||
# Execution sequence
|
||||
The operations module (or Clones) integrates with other system modules to enable secure and encapsulated execution. On the clone creation process the encapsulation is achieved by deploying a **Cloud** Image on a **Kubernates** container.
|
||||
The operations module (or Clones) integrates with other system modules to enable secure and encapsulated execution. On the clone creation process the encapsulation is achieved by deploying a **Clone Executor** Image on a **Kubernates** container.
|
||||
- [Keys](https://github.com/Superalgos/KeyVaultModule): Manages the signature of the messages sent to the external exchanges.
|
||||
- [Teams](https://github.com/Superalgos/TeamsModule): Provide team and bot authorization.
|
||||
- [Cloud](https://github.com/Superalgos/AACloud): Receive the execution parameters and runs an independent instance of the platform.
|
||||
- [Clone Executor](https://github.com/Superalgos/CloneExecutor): Receive the execution parameters and runs an independent instance of the platform.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -19,13 +19,13 @@ Clones ->> Keys: authorizeCloneToUseKey
|
||||
Keys ->> Clones: geCloneTeamAndBot
|
||||
Keys ->> Teams: getTeamMembers
|
||||
Keys -->> Clones: authorizeCloneToUseKey
|
||||
Clones ->> Cloud: createClone
|
||||
Cloud ->> Keys: signTransaction
|
||||
Cloud ->> Clones: updateExection
|
||||
Clones ->> Clone Executor: createClone
|
||||
Clone Executor ->> Keys: signTransaction
|
||||
Clone Executor ->> Clones: updateExection
|
||||
|
||||
Note left of Cloud: signTransaction and <br/> updateExection are <br/> called n number <br/> of times
|
||||
Note left of Clone Executor: signTransaction and <br/> updateExection are <br/> called n number <br/> of times
|
||||
User ->> Clones: getCloneExecutionInformation
|
||||
Cloud ->> Clones: cloneExecutionComplete
|
||||
Clone Executor ->> Clones: cloneExecutionComplete
|
||||
User ->> Clones: deleteClone
|
||||
Clones ->> Keys: releaseKey
|
||||
```
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"apiVersion": "batch/v1",
|
||||
"kind": "Job",
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "clone-executor"
|
||||
},
|
||||
"name": "clone-executor"
|
||||
},
|
||||
"spec": {
|
||||
"template": {
|
||||
"spec": {
|
||||
"restartPolicy": "OnFailure",
|
||||
"containers": [
|
||||
{
|
||||
"name": "clone-executor",
|
||||
"image": "superalgos/clone-executor:v1.0",
|
||||
"command": ["node", "Run.js"],
|
||||
"env": [
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "clone-executor"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
export const Trading = 'Trading'
|
||||
export const Indicator = 'Indicator'
|
||||
export const Extraction = 'Extraction'
|
||||
export const Sensor = 'Sensor'
|
||||
|
||||
export const BotTypesEnum = [
|
||||
Trading,
|
||||
Indicator,
|
||||
Extraction
|
||||
Sensor
|
||||
]
|
||||
|
||||
export default BotTypesEnum
|
||||
@@ -4,7 +4,7 @@ import { KubernateError } from '../errors'
|
||||
import { Client, config } from 'kubernetes-client'
|
||||
import deploymentManifest from '../config/clone-deployment.json'
|
||||
import { BACKTEST, COMPETITION, LIVE, NO_TIME } from '../enums/CloneMode'
|
||||
import { Trading, Indicator, Extraction } from '../enums/BotTypes'
|
||||
import { Trading, Indicator, Sensor } from '../enums/BotTypes'
|
||||
|
||||
const createClone = async (clone) => {
|
||||
try {
|
||||
@@ -80,6 +80,10 @@ const createClone = async (clone) => {
|
||||
'name': 'PROCESS',
|
||||
'value': clone.processName
|
||||
})
|
||||
env.push({
|
||||
'name': 'EXCHANGE_NAME',
|
||||
'value': clone.exchangeName
|
||||
})
|
||||
|
||||
logger.debug('createClone Trading Configuration.')
|
||||
if (clone.botType === Trading) {
|
||||
@@ -130,7 +134,7 @@ const createClone = async (clone) => {
|
||||
'value': clone.keyId
|
||||
})
|
||||
}
|
||||
} else if (clone.botType === Indicator || clone.botType === Extraction) {
|
||||
} else if (clone.botType === Indicator || clone.botType === Sensor) {
|
||||
if (clone.mode === NO_TIME) {
|
||||
if (!clone.resumeExecution) {
|
||||
env.push({
|
||||
@@ -159,7 +163,7 @@ const createClone = async (clone) => {
|
||||
}
|
||||
|
||||
deploymentManifest.spec.template.spec.containers[0].env = env
|
||||
logger.debug('ENV %s: ', JSON.stringify(env))
|
||||
|
||||
await client.apis.batch.v1.namespaces('default').jobs.post(
|
||||
{ body: deploymentManifest })
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ const cloneDetails = (selectedBot, clone) => {
|
||||
clone.teamSlug = selectedBot.team.slug
|
||||
clone.teamAvatar = selectedBot.team.profile.avatar
|
||||
clone.userLoggedIn = selectedBot.team.members[0].member.alias
|
||||
clone.botType = selectedBot.kind
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ const resolve = async(parent, { botType }, context) => {
|
||||
let bot = allUserBots[j].node
|
||||
if(clones[i].botId === bot.id){
|
||||
let clone = cloneDetails(bot, clones[i])
|
||||
clone.botType = bot.kind
|
||||
cloneResponse.push(clone)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ const resolve = async (parent, { botType, queryLogs }, context) => {
|
||||
let bot = allUserBots[j].node
|
||||
if(clones[i].botId === bot.id){
|
||||
let clone = cloneDetails(bot, clones[i])
|
||||
clone.botType = bot.kind
|
||||
cloneResponse.push(clone)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ const Type = new GraphQLInputObjectType({
|
||||
processName: { type: new GraphQLNonNull(GraphQLString) },
|
||||
botType: { type: new GraphQLNonNull(GraphQLString) },
|
||||
keyId: { type: GraphQLString },
|
||||
timePeriod: { type: GraphQLString }
|
||||
timePeriod: { type: GraphQLString },
|
||||
exchangeName: { type: GraphQLString }
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user