/********************************************************************************************************************* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * * Permission is hereby granted, free of charge, to any person obtaining a copy of * * this software and associated documentation files (the "Software"), to deal in * * the Software without restriction, including without limitation the rights to * * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * * the Software, and to permit persons to whom the Software is furnished to do so. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * *********************************************************************************************************************/ import { FunctionComponent, useCallback, useEffect, useState, useMemo } from 'react' import { TrainingTemplateData, EMPTY_TrainingTemplateData } from '../../../models' import { Button, Checkbox, ColumnLayout, DatePicker, Form, FormSection, Inline, Input, Multiselect, Select, Stack, Tabs, Text, Toggle, } from 'aws-northstar' import { Container, FormField } from '../../../components/NorthstarEx' import { useHistory, useParams } from 'react-router-dom' import { v4 as uuid } from 'uuid' import { useTrainingTemplateContext } from '../../../contexts/TrainingTemplateContext' import { useAssetContext } from '../../../contexts/AssetContext' import { appvars } from '../../../config' import { dayjsutc } from '../../../utils/dayjs' import { SelectOption } from 'aws-northstar/components/Select' import { selectOptionsFor, valueOptionFor, valueOptions, valueOptionsGen } from '../../../utils/select-option-helper' import { useImmer } from 'use-immer' import { Draft } from 'immer' export const TrainingTemplateEditor: FunctionComponent = () => { const history = useHistory() const { trainingTemplateId } = useParams<{ trainingTemplateId: string }>() const [{ items: trainingTemplateItems, isLoading }, { updateItem, createItem }] = useTrainingTemplateContext() const [{ assets, assetClasses }] = useAssetContext() const editMode = !(trainingTemplateId === 'new' || trainingTemplateId === undefined) const [trainingTemplateData, updateTrainingTemplateData] = useImmer<TrainingTemplateData>(() => { if (editMode && trainingTemplateItems != null) { const sel = trainingTemplateItems.find((x) => x.Id === trainingTemplateId) if (sel != null) { return sel } } return { ...EMPTY_TrainingTemplateData, Id: uuid(), } }) const [assetSelectOptions, setAssetSelectOptions] = useState<SelectOption[]>([]) const [assetClassSelectOptions, setAssetClassSelectOptions] = useState<SelectOption[]>([]) const [formError, setFormError] = useState<string>() // :: assets SelectOptions useEffect(() => { // TODO: sort by ticker const tickers = assets.map((x) => x.ticker) const options: SelectOption[] = selectOptionsFor(tickers) // const selector = (asset: AssetData) => asset.ticker // const options: SelectOption[] = selectOptionsGrouped(assets, 'assetClass', selector, selector) setAssetSelectOptions(options) }, [assets]) // :: assetClass SelectOptions useEffect(() => { setAssetClassSelectOptions(selectOptionsFor(assetClasses)) }, [assetClasses]) // :: selectValue :: predictedAsset const predictedAssetValueOption = useMemo(() => { return valueOptionFor(assetSelectOptions, trainingTemplateData.predictedAsset) }, [trainingTemplateData.predictedAsset, assetSelectOptions]) const assetsForTAValueOptions = useMemo(() => { return valueOptions(assetSelectOptions, trainingTemplateData.feMeta.taSettings.assets) }, [trainingTemplateData.feMeta.taSettings.assets, assetSelectOptions]) const baseAssetsAllChecked = useMemo(() => { return trainingTemplateData.feMeta.baseAssets === 'all' }, [trainingTemplateData.feMeta.baseAssets]) const tempBaseAssetOptions = useMemo(() => { if (trainingTemplateData.feMeta.baseAssets === 'all') { return } return valueOptions(assetSelectOptions, trainingTemplateData.feMeta.baseAssets as string[]) }, [trainingTemplateData.feMeta.baseAssets, assetSelectOptions]) // :: selectValue :: assetsForArima const assetsForArimaValueOptions = useMemo(() => { return valueOptions(assetSelectOptions, trainingTemplateData.feMeta.arimaSettings.assets) }, [trainingTemplateData.feMeta.arimaSettings.assets, assetSelectOptions]) // :: selectValue :: assetsForFFT const assetClassesForFFTValueOptions = useMemo(() => { return valueOptions(assetClassSelectOptions, trainingTemplateData.feMeta.fftSettings.assetClasses) }, [trainingTemplateData.feMeta.fftSettings.assetClasses, assetClassSelectOptions]) // :: selectValue :: fftNumSteps const fftNumStepsValueOptions = useMemo(() => { return valueOptionsGen(trainingTemplateData.feMeta.fftSettings.num_steps.map((x) => x.toString())) }, [trainingTemplateData.feMeta.fftSettings.num_steps]) const onSubmit = useCallback( async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault() if (trainingTemplateData == null) { throw new Error('TrainingTemplateData is null') } try { if (editMode) { updateItem(trainingTemplateData, true) } else { await createItem(trainingTemplateData) } history.push(`/${appvars.URL.TRAINING_TEMPLATE}/${trainingTemplateData.Id}`) } catch (err) { setFormError(`Error while ${editMode ? 'updating' : 'saving new'} training template object: ${err}`) } }, [trainingTemplateData, history, updateItem, createItem, setFormError, editMode], ) if (trainingTemplateData == null) { return null } const FEContent = ( <> <Container headingVariant='h2' title='Base Assets' actionGroup={ <Toggle label={baseAssetsAllChecked ? 'All' : 'Selective'} checked={baseAssetsAllChecked} onChange={(checked: boolean) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.baseAssets = checked ? 'all' : assets.map((a) => a.Id) }) }} /> } infoKey='fe.baseAssets' > <ColumnLayout> <Stack> <FormField label='Base Assets' stretch controlId='multiselect_BaseAssets' infoKey='fe.baseAssets'> <Multiselect disabled={baseAssetsAllChecked} checkboxes={true} options={assetSelectOptions} onChange={(selected) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { const selectedAssetIds = selected.map((x) => x.value as string) draft.feMeta.baseAssets = selectedAssetIds }) }} placeholder='Selected base assets' controlId='multiselect_BaseAssets' value={baseAssetsAllChecked ? undefined : tempBaseAssetOptions} /> </FormField> </Stack> </ColumnLayout> </Container> <Container headingVariant='h2' title='Technical Analysis features' actionGroup={ <Toggle label={trainingTemplateData.feMeta.taSettings.enabled ? 'Enabled' : 'Disabled'} checked={trainingTemplateData.feMeta.taSettings.enabled} onChange={(checked: boolean) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.taSettings.enabled = checked }) }} /> } infoKey='fe.ta.header' > <ColumnLayout> <Stack> <ColumnLayout> <Stack> <FormField label='Bollinger Band Window' controlId='field_bb_window' infoKey='fe.ta.bbWindow'> <Input type='number' controlId='field_bb_window' value={trainingTemplateData.feMeta.taSettings.bollingerBand.window} required={true} placeholder='BB window' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.taSettings.bollingerBand.window = parseInt(e, 10) }) }} /> </FormField> </Stack> <Stack> <FormField label='Bollinger Band Window deviance' controlId='field_bb_window_dev' infoKey='fe.ta.bbWindowDev' > <Input type='number' controlId='field_bb_window_dev' value={trainingTemplateData.feMeta.taSettings.bollingerBand.window_dev} required={true} placeholder='BB window dev' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.taSettings.bollingerBand.window_dev = parseInt(e, 10) }) }} /> </FormField> </Stack> </ColumnLayout> <Stack> <FormField label='Assets for TA' stretch controlId='multiselect_AssetsForTA' infoKey='fe.ta.assets'> <Multiselect checkboxes={true} options={assetSelectOptions} onChange={(selected) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.taSettings.assets = selected.map((x) => x.value as string) }) }} placeholder='Selected assets for TA' controlId='multiselect_AssetsForTA' value={assetsForTAValueOptions} /> </FormField> </Stack> </Stack> <Stack> <ColumnLayout> <Stack> <FormField label='RSI Window' controlId='field_rsi_window' infoKey='fe.ta.rsiWindow'> <Input type='number' controlId='field_rsi_window' value={trainingTemplateData.feMeta.taSettings.rsi.window} required={true} placeholder='RSI window' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.taSettings.rsi.window = parseInt(e, 10) }) }} /> </FormField> </Stack> <Stack> <FormField label='SMA Window' controlId='field_sma_window' infoKey='fe.ta.smaWindow'> <Input type='number' controlId='field_sma_window' value={trainingTemplateData.feMeta.taSettings.sma.window} required={true} placeholder='SMA window' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.taSettings.sma.window = parseInt(e, 10) }) }} /> </FormField> </Stack> </ColumnLayout> </Stack> </ColumnLayout> </Container> <Container headingVariant='h2' title='ARIMA features' actionGroup={ <Toggle label={trainingTemplateData.feMeta.arimaSettings.enabled ? 'Enabled' : 'Disabled'} checked={trainingTemplateData.feMeta.arimaSettings.enabled} onChange={(checked: boolean) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.arimaSettings.enabled = checked }) }} /> } infoKey='fe.arima.header' > <ColumnLayout> <Stack> <FormField label='Train set size' controlId='field_arima_trainsize' infoKey='fe.arima.trainSetSize'> <Input type='number' controlId='field_arima_trainsize' value={trainingTemplateData.feMeta.arimaSettings.trainSetSize} required={true} placeholder='Train set size' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.arimaSettings.trainSetSize = parseFloat(e) }) }} /> </FormField> </Stack> <Stack> <FormField label='Assets for ARIMA' controlId='multiselect_AssetsForArima' infoKey='fe.arima.assets'> <Multiselect checkboxes={true} options={assetSelectOptions} onChange={(selected) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.arimaSettings.assets = selected.map((x) => x.value as string) }) }} placeholder='Selected assets for ARIMA' controlId='multiselect_AssetsForArima' value={assetsForArimaValueOptions} /> </FormField> </Stack> </ColumnLayout> </Container> <Container headingVariant='h2' title='FFT features' actionGroup={ <Toggle label={trainingTemplateData.feMeta.fftSettings.enabled ? 'Enabled' : 'Disabled'} checked={trainingTemplateData.feMeta.fftSettings.enabled} onChange={(checked: boolean) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.fftSettings.enabled = checked }) }} /> } infoKey='fe.fft.header' > <ColumnLayout> <Stack> <FormField label='Steps number' controlId='field_fft_num_steps' infoKey='fe.fft.numSteps'> <Multiselect freeSolo={true} onChange={(selected) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { const steps = selected.map((x) => parseInt(x.value as string, 10)) steps.sort((a, b) => a - b) draft.feMeta.fftSettings.num_steps = steps }) }} placeholder='Selected step numbers for FFT' controlId='field_fft_num_steps' value={fftNumStepsValueOptions} /> </FormField> </Stack> </ColumnLayout> <ColumnLayout> <Stack> <FormField label='Assets classes for FFT' controlId='multiselect_AssetClassesForFFT' infoKey='fe.fft.assets' > <Multiselect checkboxes={true} options={assetClassSelectOptions} onChange={(selected) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.fftSettings.assetClasses = selected.map((x) => x.value as string) }) }} placeholder='Selected asset classes for FFT' controlId='multiselect_AssetClassesForFFT' value={assetClassesForFFTValueOptions} /> </FormField> </Stack> </ColumnLayout> </Container> <Container headingVariant='h2' title='Auto Encoder' actionGroup={ <Toggle label={trainingTemplateData.feMeta.autoEncoderSettings.enabled ? 'Enabled' : 'Disabled'} checked={trainingTemplateData.feMeta.autoEncoderSettings.enabled} onChange={(checked: boolean) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.autoEncoderSettings.enabled = checked }) }} /> } infoKey='fe.ae.header' > <ColumnLayout> <Stack> <FormField label='Optimizer' controlId='field_ae_optimizer' infoKey='fe.ae.optimizer'> <Input type='text' controlId='field_ae_optimizer' value={trainingTemplateData.feMeta.autoEncoderSettings.optimizer} required={true} placeholder='Auto encoder optimizer' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.autoEncoderSettings.optimizer = e }) }} /> </FormField> </Stack> <Stack> <FormField label='Loss' controlId='field_ae_loss' infoKey='fe.ae.loss'> <Input type='text' controlId='field_ae_loss' value={trainingTemplateData.feMeta.autoEncoderSettings.loss} required={true} placeholder='Auto encoder loss' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.autoEncoderSettings.loss = e }) }} /> </FormField> </Stack> <Stack> <FormField label='Fit epoch' controlId='field_ae_fitEpoch' infoKey='fe.ae.fitEpoch'> <Input type='number' controlId='field_ae_fitEpoch' value={trainingTemplateData.feMeta.autoEncoderSettings.fitEpoch} required={true} placeholder='Fit epoch' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.autoEncoderSettings.fitEpoch = parseInt(e, 10) }) }} /> </FormField> </Stack> <Stack> <FormField label='Fit batchsize' controlId='field_ae_batchsize' infoKey='fe.ae.fitBatchSize'> <Input type='number' controlId='field_ae_batchsize' value={trainingTemplateData.feMeta.autoEncoderSettings.fitBatchSize} required={true} placeholder='Fit batchsize' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.taSettings.sma.window = parseInt(e, 10) }) }} /> </FormField> </Stack> <Stack> <FormField label='Fit shuffle' controlId='field_ae_suffle' infoKey='fe.ae.fitShuffle'> <Checkbox controlId='field_ae_shuffle' checked={trainingTemplateData.feMeta.autoEncoderSettings.fitShuffle} onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.feMeta.autoEncoderSettings.fitShuffle = e.target.checked }) }} /> </FormField> </Stack> </ColumnLayout> </Container> </> ) const modelTrainingContent = ( <> <FormSection header='General'> <ColumnLayout> <Stack> <FormField label='Frequency' controlId='field_deepar_freq' infoKey='deepar.general.freq'> <Input type='text' controlId='field_deepar_freq' value={trainingTemplateData.deepARMeta.freq} required={true} disabled={true} /> </FormField> <FormField label='Start date of training' controlId='field_deepar_startDataset' infoKey='deepar.general.start' > <DatePicker controlId='field_deepar_startDataset' value={dayjsutc(trainingTemplateData.deepARMeta.startDataset).toDate()} onChange={(e) => { // TODO: handle e == null if (e == null) { return } updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.deepARMeta.startDataset = dayjsutc( `${e.getFullYear()}-${e.getMonth() + 1}-${e.getDate()}`, ).valueOf() }) }} /> </FormField> </Stack> <Stack> <FormField label='Prediction Length' controlId='field_deepar_predictionLen' infoKey='deepar.general.predictionLen' > <Input type='number' controlId='field_deepar_predictionLen' value={trainingTemplateData.deepARMeta.predictionLength} required={true} placeholder='Prediction Length' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.deepARMeta.predictionLength = parseInt(e, 10) }) }} /> </FormField> <FormField label='End training' controlId='field_deepar_endTraining' infoKey='deepar.general.end'> <DatePicker controlId='field_deepar_endTraining' value={dayjsutc(trainingTemplateData.deepARMeta.endTraining).toDate()} onChange={(e) => { if (e == null) { return } updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.deepARMeta.endTraining = dayjsutc( `${e.getFullYear()}-${e.getMonth() + 1}-${e.getDate()}`, ).valueOf() }) }} /> </FormField> </Stack> <Stack> <FormField label='Context Length' controlId='field_deepar_contextLen' infoKey='deepar.general.contextLen'> <Input type='number' controlId='field_deepar_contextLen' value={trainingTemplateData.deepARMeta.contextLength} required={true} placeholder='Context Length' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.deepARMeta.contextLength = parseInt(e, 10) }) }} /> </FormField> <FormField label='Test windows' controlId='field_deepar_testWindows' infoKey='deepar.general.testWindows'> <Input type='number' controlId='field_deepar_testWindows' value={trainingTemplateData.deepARMeta.testWindows} required={true} placeholder='Test windows' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.deepARMeta.testWindows = parseInt(e, 10) }) }} /> </FormField> </Stack> </ColumnLayout> </FormSection> <FormSection header='Hyperparameters'> <ColumnLayout> <Stack> <FormField label='Epochs' controlId='field_deepar_hyper_epochs' infoKey='deepar.hyper.epochs'> <Input type='number' controlId='field_deepar_hyper_epochs' value={trainingTemplateData.deepARMeta.hyperParams.epochs} required={true} placeholder='Epochs' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.deepARMeta.hyperParams.epochs = parseInt(e, 10) }) }} /> </FormField> </Stack> <Stack> <FormField label='Early stopping patience' controlId='field_deepar_hyper_esp' infoKey='deepar.hyper.esp'> <Input type='number' controlId='field_deepar_hyper_esp' value={trainingTemplateData.deepARMeta.hyperParams.early_stopping_patience} required={true} placeholder='Early stopping patience' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.deepARMeta.hyperParams.early_stopping_patience = parseInt(e, 10) }) }} /> </FormField> </Stack> <Stack> <FormField label='Mini batch size' controlId='field_deepar_hyper_mbs' infoKey='deepar.hyper.miniBatchSize'> <Input type='number' controlId='field_deepar_hyper_mbs' value={trainingTemplateData.deepARMeta.hyperParams.mini_batch_size} required={true} placeholder='Mini batch size' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.deepARMeta.hyperParams.mini_batch_size = parseInt(e, 10) }) }} /> </FormField> </Stack> <Stack> <FormField label='Learning rate' controlId='field_deepar_hyper_lr' infoKey='deepar.hyper.learningRate'> <Input type='number' controlId='field_deepar_hyper_lr' value={trainingTemplateData.deepARMeta.hyperParams.learning_rate} required={true} placeholder='Learning rate' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.deepARMeta.hyperParams.learning_rate = parseInt(e, 10) }) }} /> </FormField> </Stack> </ColumnLayout> </FormSection> </> ) const tabs = [ { label: 'Feature Engineering', id: 'feature-engineering', content: FEContent, }, { label: 'Model Training Parameters', id: 'model-training', content: modelTrainingContent, }, ] return ( <Form header={editMode ? 'Edit training template' : 'New training template'} onSubmit={onSubmit} errorText={formError} actions={ <Inline> <Button variant='link' onClick={() => { history.push( editMode ? `/${appvars.URL.TRAINING_TEMPLATE}/${trainingTemplateData.Id}` : `/${appvars.URL.TRAINING_TEMPLATE}`, ) }} > Cancel </Button> <Button type='submit' variant='primary'> Save </Button> </Inline> } > <FormSection header='Training template details'> <ColumnLayout> <Stack> <FormField label='Id' controlId='field_Id' infoKey='fe.general.id'> <Input type='text' controlId='text_Id' value={trainingTemplateData.Id} required={true} disabled={true} /> </FormField> </Stack> <Stack> <FormField label='Created' controlId='field_Created' infoKey='fe.general.created'> <Input type='text' controlId='text_Created' value={dayjsutc(trainingTemplateData.createdAt).format(appvars.DATETIMEFORMAT)} required={true} disabled={true} /> </FormField> </Stack> <Stack> <FormField label='Name' controlId='field_Name' infoKey='fe.general.name'> <Input type='text' controlId='text_Name' value={trainingTemplateData.name} required={true} placeholder='Training template name' onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.name = e }) }} /> </FormField> </Stack> <Stack> <FormField label='Asset to predict' controlId='field_PredictedAsset' infoKey='fe.general.predictedAsset'> <Select placeholder='Choose asset to predict' options={assetSelectOptions} selectedOption={predictedAssetValueOption} onChange={(e) => { updateTrainingTemplateData((draft: Draft<TrainingTemplateData>) => { draft.predictedAsset = e.target.value as string }) }} /> </FormField> </Stack> </ColumnLayout> </FormSection> <FormSection header=''> <Tabs tabs={tabs} variant='container' /> </FormSection> </Form> ) }