Initial commit - BTC Trading Dashboard
- FastAPI backend with PostgreSQL database connection - Frontend dashboard with lightweight-charts - Technical indicators (SMA, EMA, RSI, MACD, Bollinger Bands, etc.) - Trading strategy simulation and backtesting - Database connection to NAS at 20.20.20.20:5433 - Development server setup and documentation
This commit is contained in:
38
src/api/dashboard/static/js/indicators/atr.js
Normal file
38
src/api/dashboard/static/js/indicators/atr.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class ATRIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const period = this.params.period || 14;
|
||||
const results = new Array(candles.length).fill(null);
|
||||
const tr = new Array(candles.length).fill(0);
|
||||
|
||||
for (let i = 1; i < candles.length; i++) {
|
||||
const h_l = candles[i].high - candles[i].low;
|
||||
const h_pc = Math.abs(candles[i].high - candles[i-1].close);
|
||||
const l_pc = Math.abs(candles[i].low - candles[i-1].close);
|
||||
tr[i] = Math.max(h_l, h_pc, l_pc);
|
||||
}
|
||||
|
||||
let atr = 0;
|
||||
let sum = 0;
|
||||
for (let i = 1; i <= period; i++) sum += tr[i];
|
||||
atr = sum / period;
|
||||
results[period] = atr;
|
||||
|
||||
for (let i = period + 1; i < candles.length; i++) {
|
||||
atr = (atr * (period - 1) + tr[i]) / period;
|
||||
results[i] = atr;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'ATR',
|
||||
description: 'Average True Range - measures market volatility',
|
||||
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
|
||||
plots: [{ id: 'value', color: '#795548', title: 'ATR' }],
|
||||
displayMode: 'pane'
|
||||
};
|
||||
}
|
||||
}
|
||||
18
src/api/dashboard/static/js/indicators/base.js
Normal file
18
src/api/dashboard/static/js/indicators/base.js
Normal file
@ -0,0 +1,18 @@
|
||||
export class BaseIndicator {
|
||||
constructor(config) {
|
||||
this.name = config.name;
|
||||
this.type = config.type;
|
||||
this.params = config.params || {};
|
||||
this.timeframe = config.timeframe || '1m';
|
||||
}
|
||||
calculate(candles) { throw new Error("Not implemented"); }
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: this.name,
|
||||
inputs: [],
|
||||
plots: [],
|
||||
displayMode: 'overlay'
|
||||
};
|
||||
}
|
||||
}
|
||||
43
src/api/dashboard/static/js/indicators/bb.js
Normal file
43
src/api/dashboard/static/js/indicators/bb.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class BollingerBandsIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const period = this.params.period || 20;
|
||||
const stdDevMult = this.params.stdDev || 2;
|
||||
const results = new Array(candles.length).fill(null);
|
||||
|
||||
for (let i = period - 1; i < candles.length; i++) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < period; j++) sum += candles[i-j].close;
|
||||
const sma = sum / period;
|
||||
|
||||
let diffSum = 0;
|
||||
for (let j = 0; j < period; j++) diffSum += Math.pow(candles[i-j].close - sma, 2);
|
||||
const stdDev = Math.sqrt(diffSum / period);
|
||||
|
||||
results[i] = {
|
||||
middle: sma,
|
||||
upper: sma + (stdDevMult * stdDev),
|
||||
lower: sma - (stdDevMult * stdDev)
|
||||
};
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Bollinger Bands',
|
||||
description: 'Volatility bands around a moving average',
|
||||
inputs: [
|
||||
{ name: 'period', label: 'Period', type: 'number', default: 20, min: 1, max: 100 },
|
||||
{ name: 'stdDev', label: 'Std Dev', type: 'number', default: 2, min: 0.5, max: 5, step: 0.5 }
|
||||
],
|
||||
plots: [
|
||||
{ id: 'upper', color: '#4caf50', title: 'Upper' },
|
||||
{ id: 'middle', color: '#4caf50', title: 'Middle', lineStyle: 2 },
|
||||
{ id: 'lower', color: '#4caf50', title: 'Lower' }
|
||||
],
|
||||
displayMode: 'overlay'
|
||||
};
|
||||
}
|
||||
}
|
||||
18
src/api/dashboard/static/js/indicators/ema.js
Normal file
18
src/api/dashboard/static/js/indicators/ema.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { MA } from './ma.js';
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class EMAIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const period = this.params.period || 44;
|
||||
return MA.ema(candles, period, 'close');
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'EMA',
|
||||
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }],
|
||||
plots: [{ id: 'value', color: '#ff9800', title: 'EMA' }],
|
||||
displayMode: 'overlay'
|
||||
};
|
||||
}
|
||||
}
|
||||
41
src/api/dashboard/static/js/indicators/hts.js
Normal file
41
src/api/dashboard/static/js/indicators/hts.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { MA } from './ma.js';
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class HTSIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const shortPeriod = this.params.short || 33;
|
||||
const longPeriod = this.params.long || 144;
|
||||
const maType = this.params.maType || 'RMA';
|
||||
|
||||
const shortHigh = MA.get(maType, candles, shortPeriod, 'high');
|
||||
const shortLow = MA.get(maType, candles, shortPeriod, 'low');
|
||||
const longHigh = MA.get(maType, candles, longPeriod, 'high');
|
||||
const longLow = MA.get(maType, candles, longPeriod, 'low');
|
||||
|
||||
return candles.map((_, i) => ({
|
||||
fastHigh: shortHigh[i],
|
||||
fastLow: shortLow[i],
|
||||
slowHigh: longHigh[i],
|
||||
slowLow: longLow[i]
|
||||
}));
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'HTS Trend System',
|
||||
description: 'High/Low Trend System with Fast and Slow MAs',
|
||||
inputs: [
|
||||
{ name: 'short', label: 'Fast Period', type: 'number', default: 33, min: 1, max: 500 },
|
||||
{ name: 'long', label: 'Slow Period', type: 'number', default: 144, min: 1, max: 500 },
|
||||
{ name: 'maType', label: 'MA Type', type: 'select', options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'], default: 'RMA' }
|
||||
],
|
||||
plots: [
|
||||
{ id: 'fastHigh', color: '#00bcd4', title: 'Fast High', width: 1 },
|
||||
{ id: 'fastLow', color: '#00bcd4', title: 'Fast Low', width: 1 },
|
||||
{ id: 'slowHigh', color: '#f44336', title: 'Slow High', width: 2 },
|
||||
{ id: 'slowLow', color: '#f44336', title: 'Slow Low', width: 2 }
|
||||
],
|
||||
displayMode: 'overlay'
|
||||
};
|
||||
}
|
||||
}
|
||||
43
src/api/dashboard/static/js/indicators/index.js
Normal file
43
src/api/dashboard/static/js/indicators/index.js
Normal file
@ -0,0 +1,43 @@
|
||||
export { MA } from './ma.js';
|
||||
export { BaseIndicator } from './base.js';
|
||||
export { HTSIndicator } from './hts.js';
|
||||
export { MAIndicator } from './ma_indicator.js';
|
||||
export { RSIIndicator } from './rsi.js';
|
||||
export { BollingerBandsIndicator } from './bb.js';
|
||||
export { MACDIndicator } from './macd.js';
|
||||
export { StochasticIndicator } from './stoch.js';
|
||||
export { ATRIndicator } from './atr.js';
|
||||
|
||||
import { HTSIndicator } from './hts.js';
|
||||
import { MAIndicator } from './ma_indicator.js';
|
||||
import { RSIIndicator } from './rsi.js';
|
||||
import { BollingerBandsIndicator } from './bb.js';
|
||||
import { MACDIndicator } from './macd.js';
|
||||
import { StochasticIndicator } from './stoch.js';
|
||||
import { ATRIndicator } from './atr.js';
|
||||
|
||||
export const IndicatorRegistry = {
|
||||
hts: HTSIndicator,
|
||||
ma: MAIndicator,
|
||||
rsi: RSIIndicator,
|
||||
bb: BollingerBandsIndicator,
|
||||
macd: MACDIndicator,
|
||||
stoch: StochasticIndicator,
|
||||
atr: ATRIndicator
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamically build the available indicators list from the registry.
|
||||
* Each indicator class provides its own name and description via getMetadata().
|
||||
*/
|
||||
export function getAvailableIndicators() {
|
||||
return Object.entries(IndicatorRegistry).map(([type, IndicatorClass]) => {
|
||||
const instance = new IndicatorClass({ type, params: {}, name: '' });
|
||||
const meta = instance.getMetadata();
|
||||
return {
|
||||
type,
|
||||
name: meta.name || type.toUpperCase(),
|
||||
description: meta.description || ''
|
||||
};
|
||||
});
|
||||
}
|
||||
93
src/api/dashboard/static/js/indicators/ma.js
Normal file
93
src/api/dashboard/static/js/indicators/ma.js
Normal file
@ -0,0 +1,93 @@
|
||||
export class MA {
|
||||
static get(type, candles, period, source = 'close') {
|
||||
switch (type.toUpperCase()) {
|
||||
case 'SMA': return MA.sma(candles, period, source);
|
||||
case 'EMA': return MA.ema(candles, period, source);
|
||||
case 'RMA': return MA.rma(candles, period, source);
|
||||
case 'WMA': return MA.wma(candles, period, source);
|
||||
case 'VWMA': return MA.vwma(candles, period, source);
|
||||
default: return MA.sma(candles, period, source);
|
||||
}
|
||||
}
|
||||
|
||||
static sma(candles, period, source = 'close') {
|
||||
const results = new Array(candles.length).fill(null);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < candles.length; i++) {
|
||||
sum += candles[i][source];
|
||||
if (i >= period) sum -= candles[i - period][source];
|
||||
if (i >= period - 1) results[i] = sum / period;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
static ema(candles, period, source = 'close') {
|
||||
const multiplier = 2 / (period + 1);
|
||||
const results = new Array(candles.length).fill(null);
|
||||
let ema = 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < candles.length; i++) {
|
||||
if (i < period) {
|
||||
sum += candles[i][source];
|
||||
if (i === period - 1) {
|
||||
ema = sum / period;
|
||||
results[i] = ema;
|
||||
}
|
||||
} else {
|
||||
ema = (candles[i][source] - ema) * multiplier + ema;
|
||||
results[i] = ema;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
static rma(candles, period, source = 'close') {
|
||||
const multiplier = 1 / period;
|
||||
const results = new Array(candles.length).fill(null);
|
||||
let rma = 0;
|
||||
let sum = 0;
|
||||
|
||||
for (let i = 0; i < candles.length; i++) {
|
||||
if (i < period) {
|
||||
sum += candles[i][source];
|
||||
if (i === period - 1) {
|
||||
rma = sum / period;
|
||||
results[i] = rma;
|
||||
}
|
||||
} else {
|
||||
rma = (candles[i][source] - rma) * multiplier + rma;
|
||||
results[i] = rma;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
static wma(candles, period, source = 'close') {
|
||||
const results = new Array(candles.length).fill(null);
|
||||
const weightSum = (period * (period + 1)) / 2;
|
||||
|
||||
for (let i = period - 1; i < candles.length; i++) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
sum += candles[i - j][source] * (period - j);
|
||||
}
|
||||
results[i] = sum / weightSum;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
static vwma(candles, period, source = 'close') {
|
||||
const results = new Array(candles.length).fill(null);
|
||||
|
||||
for (let i = period - 1; i < candles.length; i++) {
|
||||
let sumPV = 0;
|
||||
let sumV = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
sumPV += candles[i - j][source] * candles[i - j].volume;
|
||||
sumV += candles[i - j].volume;
|
||||
}
|
||||
results[i] = sumV !== 0 ? sumPV / sumV : null;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
23
src/api/dashboard/static/js/indicators/ma_indicator.js
Normal file
23
src/api/dashboard/static/js/indicators/ma_indicator.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { MA } from './ma.js';
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class MAIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const period = this.params.period || 44;
|
||||
const maType = this.params.maType || 'SMA';
|
||||
return MA.get(maType, candles, period, 'close');
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'MA',
|
||||
description: 'Moving Average (SMA/EMA/RMA/WMA/VWMA)',
|
||||
inputs: [
|
||||
{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 },
|
||||
{ name: 'maType', label: 'MA Type', type: 'select', options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'], default: 'SMA' }
|
||||
],
|
||||
plots: [{ id: 'value', color: '#2962ff', title: 'MA' }],
|
||||
displayMode: 'overlay'
|
||||
};
|
||||
}
|
||||
}
|
||||
60
src/api/dashboard/static/js/indicators/macd.js
Normal file
60
src/api/dashboard/static/js/indicators/macd.js
Normal file
@ -0,0 +1,60 @@
|
||||
import { MA } from './ma.js';
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class MACDIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const fast = this.params.fast || 12;
|
||||
const slow = this.params.slow || 26;
|
||||
const signal = this.params.signal || 9;
|
||||
|
||||
const fastEma = MA.ema(candles, fast, 'close');
|
||||
const slowEma = MA.ema(candles, slow, 'close');
|
||||
|
||||
const macdLine = fastEma.map((f, i) => (f !== null && slowEma[i] !== null) ? f - slowEma[i] : null);
|
||||
|
||||
const signalLine = new Array(candles.length).fill(null);
|
||||
const multiplier = 2 / (signal + 1);
|
||||
let ema = 0;
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
|
||||
for (let i = 0; i < macdLine.length; i++) {
|
||||
if (macdLine[i] === null) continue;
|
||||
count++;
|
||||
if (count < signal) {
|
||||
sum += macdLine[i];
|
||||
} else if (count === signal) {
|
||||
sum += macdLine[i];
|
||||
ema = sum / signal;
|
||||
signalLine[i] = ema;
|
||||
} else {
|
||||
ema = (macdLine[i] - ema) * multiplier + ema;
|
||||
signalLine[i] = ema;
|
||||
}
|
||||
}
|
||||
|
||||
return macdLine.map((m, i) => ({
|
||||
macd: m,
|
||||
signal: signalLine[i],
|
||||
histogram: (m !== null && signalLine[i] !== null) ? m - signalLine[i] : null
|
||||
}));
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'MACD',
|
||||
description: 'Moving Average Convergence Divergence - trend & momentum',
|
||||
inputs: [
|
||||
{ name: 'fast', label: 'Fast Period', type: 'number', default: 12 },
|
||||
{ name: 'slow', label: 'Slow Period', type: 'number', default: 26 },
|
||||
{ name: 'signal', label: 'Signal Period', type: 'number', default: 9 }
|
||||
],
|
||||
plots: [
|
||||
{ id: 'macd', color: '#2196f3', title: 'MACD' },
|
||||
{ id: 'signal', color: '#ff5722', title: 'Signal' },
|
||||
{ id: 'histogram', color: '#607d8b', title: 'Histogram', type: 'histogram' }
|
||||
],
|
||||
displayMode: 'pane'
|
||||
};
|
||||
}
|
||||
}
|
||||
69
src/api/dashboard/static/js/indicators/rsi.js
Normal file
69
src/api/dashboard/static/js/indicators/rsi.js
Normal file
@ -0,0 +1,69 @@
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class RSIIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const period = this.params.period || 14;
|
||||
|
||||
// 1. Calculate RSI using RMA (Wilder's Smoothing)
|
||||
let rsiValues = new Array(candles.length).fill(null);
|
||||
let upSum = 0;
|
||||
let downSum = 0;
|
||||
const rmaAlpha = 1 / period;
|
||||
|
||||
for (let i = 1; i < candles.length; i++) {
|
||||
const diff = candles[i].close - candles[i-1].close;
|
||||
const up = diff > 0 ? diff : 0;
|
||||
const down = diff < 0 ? -diff : 0;
|
||||
|
||||
if (i < period) {
|
||||
upSum += up;
|
||||
downSum += down;
|
||||
} else if (i === period) {
|
||||
upSum += up;
|
||||
downSum += down;
|
||||
const avgUp = upSum / period;
|
||||
const avgDown = downSum / period;
|
||||
rsiValues[i] = avgDown === 0 ? 100 : (avgUp === 0 ? 0 : 100 - (100 / (1 + avgUp / avgDown)));
|
||||
upSum = avgUp; // Store for next RMA step
|
||||
downSum = avgDown;
|
||||
} else {
|
||||
upSum = (up - upSum) * rmaAlpha + upSum;
|
||||
downSum = (down - downSum) * rmaAlpha + downSum;
|
||||
rsiValues[i] = downSum === 0 ? 100 : (upSum === 0 ? 0 : 100 - (100 / (1 + upSum / downSum)));
|
||||
}
|
||||
}
|
||||
|
||||
// Combine results
|
||||
return rsiValues.map((rsi, i) => {
|
||||
return {
|
||||
paneBg: 80, // Background lightening trick
|
||||
rsi: rsi,
|
||||
upperBand: 70,
|
||||
lowerBand: 30
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
const plots = [
|
||||
// RSI Line
|
||||
{ id: 'rsi', color: '#7E57C2', title: '', width: 1, lastValueVisible: true },
|
||||
|
||||
// Bands
|
||||
{ id: 'upperBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false },
|
||||
{ id: 'lowerBand', color: '#787B86', title: '', style: 'dashed', width: 1, lastValueVisible: false }
|
||||
];
|
||||
|
||||
return {
|
||||
name: 'RSI',
|
||||
description: 'Relative Strength Index',
|
||||
inputs: [
|
||||
{ name: 'period', label: 'RSI Length', type: 'number', default: 14, min: 1, max: 100 }
|
||||
],
|
||||
plots: plots,
|
||||
displayMode: 'pane',
|
||||
paneMin: 0,
|
||||
paneMax: 100
|
||||
};
|
||||
}
|
||||
}
|
||||
18
src/api/dashboard/static/js/indicators/sma.js
Normal file
18
src/api/dashboard/static/js/indicators/sma.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { MA } from './ma.js';
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class SMAIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const period = this.params.period || 44;
|
||||
return MA.sma(candles, period, 'close');
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'SMA',
|
||||
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }],
|
||||
plots: [{ id: 'value', color: '#2962ff', title: 'SMA' }],
|
||||
displayMode: 'overlay'
|
||||
};
|
||||
}
|
||||
}
|
||||
48
src/api/dashboard/static/js/indicators/stoch.js
Normal file
48
src/api/dashboard/static/js/indicators/stoch.js
Normal file
@ -0,0 +1,48 @@
|
||||
import { BaseIndicator } from './base.js';
|
||||
|
||||
export class StochasticIndicator extends BaseIndicator {
|
||||
calculate(candles) {
|
||||
const kPeriod = this.params.kPeriod || 14;
|
||||
const dPeriod = this.params.dPeriod || 3;
|
||||
const results = new Array(candles.length).fill(null);
|
||||
|
||||
const kValues = new Array(candles.length).fill(null);
|
||||
|
||||
for (let i = kPeriod - 1; i < candles.length; i++) {
|
||||
let lowest = Infinity;
|
||||
let highest = -Infinity;
|
||||
for (let j = 0; j < kPeriod; j++) {
|
||||
lowest = Math.min(lowest, candles[i-j].low);
|
||||
highest = Math.max(highest, candles[i-j].high);
|
||||
}
|
||||
const diff = highest - lowest;
|
||||
kValues[i] = diff === 0 ? 50 : ((candles[i].close - lowest) / diff) * 100;
|
||||
}
|
||||
|
||||
for (let i = kPeriod + dPeriod - 2; i < candles.length; i++) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < dPeriod; j++) sum += kValues[i-j];
|
||||
results[i] = { k: kValues[i], d: sum / dPeriod };
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
getMetadata() {
|
||||
return {
|
||||
name: 'Stochastic',
|
||||
description: 'Stochastic Oscillator - compares close to high-low range',
|
||||
inputs: [
|
||||
{ name: 'kPeriod', label: 'K Period', type: 'number', default: 14 },
|
||||
{ name: 'dPeriod', label: 'D Period', type: 'number', default: 3 }
|
||||
],
|
||||
plots: [
|
||||
{ id: 'k', color: '#3f51b5', title: '%K' },
|
||||
{ id: 'd', color: '#ff9800', title: '%D' }
|
||||
],
|
||||
displayMode: 'pane',
|
||||
paneMin: 0,
|
||||
paneMax: 100
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user