Add indicators to backtest and bot (#14)

* Add indicators to backtest and bot

* Remove
This commit is contained in:
Oda
2025-02-28 00:53:25 +07:00
committed by GitHub
parent e0a8347953
commit c715da8a17
30 changed files with 787 additions and 109 deletions

View File

@@ -1,5 +1,5 @@
import useTheme from '../../../hooks/useTheme'
const themes = ['black', 'coffee', 'cyberpunk', 'lofi', 'retro']
const themes = ['black', 'coffee', 'cyberpunk', 'lofi', 'retro', 'kaigen']
const ThemeSelector = (): JSX.Element => {
const { setTheme } = useTheme()

View File

@@ -139,6 +139,7 @@ const BacktestCards: React.FC<IBacktestCards> = ({ list, setBacktests }) => {
positions={backtest.positions}
walletBalances={backtest.walletBalances}
signals={backtest.signals}
strategiesValues={backtest.strategiesValues}
width={720}
height={512}
></TradeChart>

View File

@@ -1,11 +1,12 @@
import { TradeChart, CardPositionItem } from '..'
import type { IBotRowDetails } from '../../../global/interface'
import { IBotRowDetails } from '../../../global/type'
import { CardPosition } from '../../mollecules'
const BacktestRowDetails: React.FC<IBotRowDetails> = ({
candles,
positions,
walletBalances,
strategiesValues,
}) => {
return (
<>
@@ -30,11 +31,12 @@ const BacktestRowDetails: React.FC<IBotRowDetails> = ({
<div>
<figure>
<TradeChart
width={1000}
height={500}
width={1400}
height={1400}
candles={candles}
positions={positions}
walletBalances={walletBalances}
strategiesValues={strategiesValues}
signals={[]}
></TradeChart>
</figure>

View File

@@ -247,6 +247,7 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching }) => {
candles={row.original.candles}
positions={row.original.positions}
walletBalances={row.original.walletBalances}
strategiesValues={row.original.strategiesValues}
></BacktestRowDetails>
</>
),

View File

@@ -1,4 +1,5 @@
import type {
BaselineSeriesOptions,
CandlestickData,
IChartApi,
ISeriesApi,
@@ -18,6 +19,8 @@ import type {
KeyValuePairOfDateTimeAndDecimal,
Position,
Signal,
StrategiesResultBase,
StrategyType,
} from '../../../../generated/ManagingApi'
import {
PositionStatus,
@@ -25,11 +28,27 @@ import {
} from '../../../../generated/ManagingApi'
import useTheme from '../../../../hooks/useTheme'
// var customTheme = {
// background: '#0B0B0B',
// neutral: '#151515',
// primary: '#54B5F9',
// secondary: '#C492B1',
// third: '#B0DB43',
// fourth: '#F2D398',
// fifth: '#99EDCC',
// red: '#FF5340',
// green: '#08C25F',
// orange: '#EB6F22',
// }
type ITradeChartProps = {
candles: Candle[]
positions: Position[]
signals: Signal[]
walletBalances?: KeyValuePairOfDateTimeAndDecimal[] | null
strategiesValues?: { [key in keyof typeof StrategyType]?: StrategiesResultBase; } | null;
stream?: Candle | null
width: number
height: number
@@ -40,6 +59,7 @@ const TradeChart = ({
positions,
signals,
walletBalances,
strategiesValues,
stream,
width,
height,
@@ -68,6 +88,12 @@ const TradeChart = ({
}
}
const baselineOptions: BaselineSeriesOptions = {
bottomLineColor: theme.secondary,
topLineColor: theme.primary,
lineWidth: 1,
} as BaselineSeriesOptions
function buildMarker(
shape: SeriesMarkerShape,
color: string,
@@ -142,7 +168,7 @@ const TradeChart = ({
useEffect(() => {
if (chartRef.current) {
const lineColor = theme.secondary
const lineColor = theme['base-100']
chart.current = createChart(chartRef.current, {
crosshair: {
mode: CrosshairMode.Normal,
@@ -159,8 +185,8 @@ const TradeChart = ({
},
height: height,
layout: {
background: {color: '#121212'},
textColor: theme.secondary,
background: {color: theme['base-300']},
textColor: theme.accent,
},
localization: {
dateFormat: 'yyyy-MM-dd',
@@ -198,11 +224,11 @@ const TradeChart = ({
if (!chart.current) return
series1.current = chart.current.addCandlestickSeries({
borderDownColor: theme.secondary,
borderDownColor: theme.accent,
borderUpColor: theme.primary,
downColor: theme.secondary,
downColor: theme.accent,
upColor: theme.primary,
wickDownColor: theme.secondary,
wickDownColor: theme.accent,
wickUpColor: theme.primary,
})
@@ -230,7 +256,6 @@ const TradeChart = ({
},
})
const markers: SeriesMarker<Time>[] = []
if (signals) {
@@ -271,20 +296,268 @@ const TradeChart = ({
}
}
// Price panel
if (strategiesValues?.EmaTrend != null || strategiesValues?.EmaCross != null)
{
const emaSeries = chart.current.addLineSeries({
color: theme.secondary,
lineWidth: 1,
priceLineVisible: true,
priceLineWidth: 1,
priceFormat: {
minMove: 0.0001,
precision: 4,
type: 'price',
},
title: 'EMA',
})
const ema = strategiesValues.EmaTrend?.ema ?? strategiesValues.EmaCross?.ema
const emaData = ema?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.ema,
}
})
if (emaData != null)
{
// @ts-ignore
emaSeries.setData(emaData)
}
}
if (strategiesValues?.SuperTrend != null) {
const superTrendSeries = chart.current.addLineSeries({
color: theme.info,
lineWidth: 1,
priceLineVisible: false,
priceLineWidth: 1,
priceLineColor: theme.info,
title: 'SuperTrend',
pane: 0,
})
const superTrend = strategiesValues.SuperTrend.superTrend?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.superTrend,
}
})
// @ts-ignore
superTrendSeries.setData(superTrend)
}
if (markers.length > 0) {
series1.current.setMarkers(markers)
}
// Indicator panel
var paneCount = 1
if (strategiesValues?.RsiDivergence != null || strategiesValues?.RsiDivergenceConfirm != null)
{
const rsiSeries = chart.current.addLineSeries({
pane: paneCount,
title: 'RSI',
})
const rsi = strategiesValues.RsiDivergence?.rsi ?? strategiesValues.RsiDivergenceConfirm?.rsi
const rsiData = rsi?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.rsi,
}
})
// @ts-ignore
rsiSeries.setData(rsiData)
rsiSeries.applyOptions({
...baselineOptions,
priceFormat: {
minMove: 0.01,
precision: 4,
type: 'price',
},
})
paneCount++
}
if (strategiesValues?.Stc != null) {
const stcSeries = chart.current.addBaselineSeries({
pane: paneCount,
baseValue: {price: 50, type: 'price'},
title: 'STC',
})
stcSeries.createPriceLine(buildLine(theme.error, 25, 'low'))
stcSeries.createPriceLine(buildLine(theme.info, 75, 'high'))
const stcData = strategiesValues?.Stc.stc?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.stc,
}
})
// @ts-ignore
stcSeries.setData(stcData)
stcSeries.applyOptions({
...baselineOptions,
priceFormat: {
minMove: 1,
precision: 1,
type: 'price',
},
})
paneCount++
}
if (strategiesValues?.MacdCross != null) {
const histogramSeries = chart.current.addHistogramSeries({
color: theme.accent,
title: 'MACD',
pane: paneCount,
priceFormat: {
precision: 6,
type: 'volume',
}
})
const macd = strategiesValues.MacdCross.macd?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.histogram,
}
})
var priceOptions = {
scaleMargins:{
top: 0.7,
bottom: 0.02,
}
}
histogramSeries.priceScale().applyOptions(priceOptions)
// @ts-ignore
histogramSeries.setData(macd)
const macdSeries = chart.current.addLineSeries({
color: theme.primary,
lineWidth: 1,
priceLineVisible: false,
priceLineWidth: 1,
title: 'EMA',
pane: paneCount,
priceFormat: {
precision: 6,
type: 'price',
},
})
const macdData = strategiesValues.MacdCross.macd?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.macd,
}
})
macdSeries.priceScale().applyOptions(priceOptions)
// @ts-ignore
macdSeries.setData(macdData)
const signalSeries = chart.current.addLineSeries({
color: theme.info,
lineWidth: 1,
priceLineVisible: false,
priceLineWidth: 1,
lineStyle: LineStyle.Dotted,
title: 'Signal',
pane: paneCount,
priceFormat: {
precision: 6,
type: 'price',
},
})
const signalData = strategiesValues.MacdCross.macd?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.signal,
}
})
signalSeries.priceScale().applyOptions(priceOptions)
// @ts-ignore
signalSeries.setData(signalData)
paneCount++
}
if (strategiesValues?.StochRsiTrend){
const stochRsiSeries = chart.current.addLineSeries({
...baselineOptions,
priceLineVisible: false,
title: 'Stoch RSI',
pane: paneCount,
})
const stochRsi = strategiesValues.StochRsiTrend.stochRsi?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.stochRsi,
}
})
// @ts-ignore
stochRsiSeries.setData(stochRsi)
paneCount++
}
if (strategiesValues?.StDev != null) {
const stDevSeries = chart.current.addLineSeries({
color: theme.primary,
lineWidth: 1,
priceLineVisible: false,
priceLineWidth: 1,
title: 'StDev',
pane: paneCount,
})
const stDev = strategiesValues.StDev.stdDev?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.stdDev,
}
})
// @ts-ignore
stDevSeries.setData(stDev)
paneCount++
const zScoreSeries = chart.current.addBaselineSeries({
...baselineOptions,
baseValue: {price: 0, type: 'price'},
title: 'ZScore',
pane: paneCount,
})
const zScore = strategiesValues.StDev.stdDev?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.zScore,
}
})
// @ts-ignore
zScoreSeries.setData(zScore)
paneCount++
}
if (walletBalances != null) {
const walletSeries = chart.current.addBaselineSeries({
baseValue: {price: walletBalances[0].value, type: 'price'},
bottomFillColor1: 'rgba( 239, 83, 80, 0.05)',
bottomFillColor2: 'rgba( 239, 83, 80, 0.28)',
bottomLineColor: 'rgba( 239, 83, 80, 1)',
pane: 1,
topFillColor1: 'rgba( 38, 166, 154, 0.28)',
topFillColor2: 'rgba( 38, 166, 154, 0.05)',
topLineColor: 'rgba( 38, 166, 154, 1)',
pane: paneCount,
title: '$',
})
const walletData = walletBalances.map((w) => {
@@ -296,12 +569,15 @@ const TradeChart = ({
// @ts-ignore
walletSeries.setData(walletData)
walletSeries.applyOptions({
...baselineOptions,
priceFormat: {
minMove: 0.0001,
precision: 4,
type: 'price',
},
})
paneCount++
}
}

View File

@@ -1158,6 +1158,54 @@ export class ScenarioClient extends AuthorizedApiBase {
return Promise.resolve<FileResponse>(null as any);
}
scenario_UpdateScenario(name: string | null | undefined, loopbackPeriod: number | null | undefined, strategies: string[]): Promise<FileResponse> {
let url_ = this.baseUrl + "/Scenario?";
if (name !== undefined && name !== null)
url_ += "name=" + encodeURIComponent("" + name) + "&";
if (loopbackPeriod !== undefined && loopbackPeriod !== null)
url_ += "loopbackPeriod=" + encodeURIComponent("" + loopbackPeriod) + "&";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(strategies);
let options_: RequestInit = {
body: content_,
method: "PUT",
headers: {
"Content-Type": "application/json",
"Accept": "application/octet-stream"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processScenario_UpdateScenario(_response);
});
}
protected processScenario_UpdateScenario(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);
}
scenario_GetStrategies(): Promise<Strategy[]> {
let url_ = this.baseUrl + "/Scenario/strategy";
url_ = url_.replace(/[?&]$/, "");
@@ -1291,6 +1339,68 @@ export class ScenarioClient extends AuthorizedApiBase {
}
return Promise.resolve<FileResponse>(null as any);
}
scenario_UpdateStrategy(strategyType: StrategyType | undefined, name: string | null | undefined, period: number | null | undefined, fastPeriods: number | null | undefined, slowPeriods: number | null | undefined, signalPeriods: number | null | undefined, multiplier: number | null | undefined, stochPeriods: number | null | undefined, smoothPeriods: number | null | undefined, cyclePeriods: number | null | undefined): Promise<FileResponse> {
let url_ = this.baseUrl + "/Scenario/strategy?";
if (strategyType === null)
throw new Error("The parameter 'strategyType' cannot be null.");
else if (strategyType !== undefined)
url_ += "strategyType=" + encodeURIComponent("" + strategyType) + "&";
if (name !== undefined && name !== null)
url_ += "name=" + encodeURIComponent("" + name) + "&";
if (period !== undefined && period !== null)
url_ += "period=" + encodeURIComponent("" + period) + "&";
if (fastPeriods !== undefined && fastPeriods !== null)
url_ += "fastPeriods=" + encodeURIComponent("" + fastPeriods) + "&";
if (slowPeriods !== undefined && slowPeriods !== null)
url_ += "slowPeriods=" + encodeURIComponent("" + slowPeriods) + "&";
if (signalPeriods !== undefined && signalPeriods !== null)
url_ += "signalPeriods=" + encodeURIComponent("" + signalPeriods) + "&";
if (multiplier !== undefined && multiplier !== null)
url_ += "multiplier=" + encodeURIComponent("" + multiplier) + "&";
if (stochPeriods !== undefined && stochPeriods !== null)
url_ += "stochPeriods=" + encodeURIComponent("" + stochPeriods) + "&";
if (smoothPeriods !== undefined && smoothPeriods !== null)
url_ += "smoothPeriods=" + encodeURIComponent("" + smoothPeriods) + "&";
if (cyclePeriods !== undefined && cyclePeriods !== null)
url_ += "cyclePeriods=" + encodeURIComponent("" + cyclePeriods) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "PUT",
headers: {
"Accept": "application/octet-stream"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processScenario_UpdateStrategy(_response);
});
}
protected processScenario_UpdateStrategy(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);
}
}
export class SettingsClient extends AuthorizedApiBase {
@@ -1893,6 +2003,7 @@ export interface Backtest {
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
optimizedMoneyManagement: MoneyManagement;
moneyManagement: MoneyManagement;
strategiesValues?: { [key in keyof typeof StrategyType]?: StrategiesResultBase; } | null;
}
export enum Ticker {
@@ -2152,6 +2263,94 @@ export interface KeyValuePairOfDateTimeAndDecimal {
value?: number;
}
export interface StrategiesResultBase {
ema?: EmaResult[] | null;
macd?: MacdResult[] | null;
rsi?: RsiResult[] | null;
stoch?: StochResult[] | null;
stochRsi?: StochRsiResult[] | null;
bollingerBands?: BollingerBandsResult[] | null;
chandelierShort?: ChandelierResult[] | null;
stc?: StcResult[] | null;
stdDev?: StdDevResult[] | null;
superTrend?: SuperTrendResult[] | null;
chandelierLong?: ChandelierResult[] | null;
}
export interface ResultBase {
date?: Date;
}
export interface EmaResult extends ResultBase {
ema?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface MacdResult extends ResultBase {
macd?: number | null;
signal?: number | null;
histogram?: number | null;
fastEma?: number | null;
slowEma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface RsiResult extends ResultBase {
rsi?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
/** Stochastic indicator results includes aliases for those who prefer the simpler K,D,J outputs. See documentation for more information. */
export interface StochResult extends ResultBase {
oscillator?: number | null;
signal?: number | null;
percentJ?: number | null;
k?: number | null;
d?: number | null;
j?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StochRsiResult extends ResultBase {
stochRsi?: number | null;
signal?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface BollingerBandsResult extends ResultBase {
sma?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
percentB?: number | null;
zScore?: number | null;
width?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface ChandelierResult extends ResultBase {
chandelierExit?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StcResult extends ResultBase {
stc?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StdDevResult extends ResultBase {
stdDev?: number | null;
mean?: number | null;
zScore?: number | null;
stdDevSma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface SuperTrendResult extends ResultBase {
superTrend?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
}
export interface StartBotRequest {
botType: BotType;
botName: string;

View File

@@ -17,6 +17,8 @@ import type {
RiskLevel,
Scenario,
Signal,
StrategiesResultBase,
StrategyType,
Ticker,
Timeframe,
TradeDirection,
@@ -144,6 +146,7 @@ export type IBotRowDetails = {
candles: Candle[]
positions: Position[]
walletBalances?: KeyValuePairOfDateTimeAndDecimal[] | null
strategiesValues?: { [key in keyof typeof StrategyType]?: StrategiesResultBase; } | null;
}
export type IBacktestFormInput = {

View File

@@ -36,17 +36,39 @@ const themes: ThemesInterface = {
'--rounded-btn': '0',
'--tab-radius': '0',
accent: '#343232',
'base-100': '#000000',
'base-100': '#0B0B0B',
'base-200': '#0D0D0D',
'base-300': '#1A1919',
error: '#ff0000',
info: '#0000ff',
neutral: '#272626',
'base-300': '#0B0B0B',
error: '#FF5340',
info: '#B0DB43',
neutral: '#151515',
'neutral-focus': '#343232',
primary: '#343232',
secondary: '#343232',
success: '#008000',
warning: '#ffff00',
primary: '#54B5F9',
secondary: '#a3d9fe',
success: '#08C25F',
warning: '#EB6F22',
},
'[data-theme=kaigen]': {
'--animation-btn': '0',
'--animation-input': '0',
'--btn-focus-scale': '1',
'--btn-text-case': 'lowercase',
'--rounded-badge': '0',
'--rounded-box': '0',
'--rounded-btn': '0',
'--tab-radius': '0',
accent: '#343232',
'base-100': '#0B0B0B',
'base-200': '#0D0D0D',
'base-300': '#0B0B0B',
error: '#FF5340',
info: '#B0DB43',
neutral: '#151515',
'neutral-focus': '#343232',
primary: '#54B5F9',
secondary: '#a3d9fe',
success: '#08C25F',
warning: '#EB6F22',
},
'[data-theme=coffee]': {
@@ -61,18 +83,18 @@ const themes: ThemesInterface = {
success: '#9DB787',
warning: '#FFD25F',
},
'[data-theme=cyberpunk]': {
'--rounded-badge': '0',
'--rounded-box': '0',
'--rounded-btn': '0',
'--tab-radius': '0',
accent: '#c07eec',
'base-100': '#ffee00',
neutral: '#423f00',
'neutral-content': '#ffee00',
primary: '#ff7598',
secondary: '#75d1f0',
},
// '[data-theme=cyberpunk]': {
// '--rounded-badge': '0',
// '--rounded-box': '0',
// '--rounded-btn': '0',
// '--tab-radius': '0',
// accent: '#c07eec',
// 'base-100': '#ffee00',
// neutral: '#423f00',
// 'neutral-content': '#ffee00',
// primary: '#ff7598',
// secondary: '#75d1f0',
// },
'[data-theme=lofi]': {
'--animation-btn': '0',
'--animation-input': '0',

View File

@@ -1,7 +1,7 @@
module.exports = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
daisyui: {
themes: true,
themes: ['black', 'coffee', 'cyberpunk', 'lofi', 'retro', 'kaigen'],
},
plugins: [require('@tailwindcss/typography'), require('daisyui')],
theme: {