migrationt to websocket
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 1m15s
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 1m15s
This commit is contained in:
136
app.py
136
app.py
@ -5,11 +5,13 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from pymongo import MongoClient
|
from pymongo import MongoClient
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
with open("coin.yaml", "r") as file:
|
with open("coin.yaml", "r") as file:
|
||||||
data = yaml.safe_load(file)
|
data = yaml.safe_load(file)
|
||||||
interest_coins = data["interest"]
|
interest_coins = data["interest"]
|
||||||
@ -17,7 +19,8 @@ interest_coins = data["interest"]
|
|||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
data_points = []
|
||||||
|
|
||||||
# Setup templates directory
|
# Setup templates directory
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
@ -45,6 +48,25 @@ async def view_coin_graph(request: Request, coin: str):
|
|||||||
return {"coin": "not found"}
|
return {"coin": "not found"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/coin/{coin}/candle1min")
|
||||||
|
async def view_coin_candle_graph(request: Request, coin: str):
|
||||||
|
if coin in interest_coins:
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request=request, name="candle_coin.html", context={"coin": coin}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return {"coin": "not found"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/update-data")
|
||||||
|
async def update_data(data: dict):
|
||||||
|
data_points.append(data)
|
||||||
|
# Keep only last 300 points to ensure we have enough data to generate 100 candles
|
||||||
|
if len(data_points) > 300:
|
||||||
|
data_points.pop(0)
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/data/{coin}")
|
@app.get("/data/{coin}")
|
||||||
async def get_data(coin):
|
async def get_data(coin):
|
||||||
collection = db[coin]
|
collection = db[coin]
|
||||||
@ -68,5 +90,117 @@ async def get_data(coin):
|
|||||||
return {"times": times, "prices": prices}
|
return {"times": times, "prices": prices}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/data/{coin}/get-candle/all")
|
||||||
|
async def get_candle_data_all(coin):
|
||||||
|
if coin in interest_coins:
|
||||||
|
target = coin + "USDT" + "_kline"
|
||||||
|
collection = db[target.lower()]
|
||||||
|
latest_doc = collection.find_one(sort=[("T", -1)])
|
||||||
|
latest_end_time = latest_doc["T"]
|
||||||
|
time_threshold = latest_end_time - (
|
||||||
|
3000
|
||||||
|
) # 60 seconds before the latest endTime
|
||||||
|
pipeline = [
|
||||||
|
# Filter based on endTime
|
||||||
|
{"$match": {"T": {"$gte": time_threshold}}},
|
||||||
|
# Group by start time and end time
|
||||||
|
{
|
||||||
|
"$group": {
|
||||||
|
"_id": {"startTime": "$t", "endTime": "$T"},
|
||||||
|
"open": {"$first": "$o"},
|
||||||
|
"high": {"$max": "$h"},
|
||||||
|
"low": {"$min": "$l"},
|
||||||
|
"close": {"$last": "$c"},
|
||||||
|
"volume": {"$sum": "$v"},
|
||||||
|
"weighted_price": {"$sum": {"$multiply": ["$c", "$v"]}},
|
||||||
|
"count": {"$sum": 1},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
# Sort by start time
|
||||||
|
{"$sort": {"_id.startTime": 1}},
|
||||||
|
# Reshape for output
|
||||||
|
{
|
||||||
|
"$project": {
|
||||||
|
"_id": 0,
|
||||||
|
"time": {"$multiply": ["$_id.startTime", 1000]},
|
||||||
|
"open": 1,
|
||||||
|
"high": 1,
|
||||||
|
"low": 1,
|
||||||
|
"close": 1,
|
||||||
|
"volume": 1,
|
||||||
|
"vwap": {
|
||||||
|
"$cond": {
|
||||||
|
"if": {"$eq": ["$volume", 0]},
|
||||||
|
"then": 0,
|
||||||
|
"else": {"$divide": ["$weighted_price", "$volume"]},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trades": "$count",
|
||||||
|
"endTime": "$_id.endTime",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
candles = list(collection.aggregate(pipeline))
|
||||||
|
return candles
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/data/{coin}/get-candle")
|
||||||
|
async def get_candle_data(coin):
|
||||||
|
if coin in interest_coins:
|
||||||
|
target = coin + "USDT" + "_kline"
|
||||||
|
collection = db[target.lower()]
|
||||||
|
latest_doc = collection.find_one(sort=[("T", -1)])
|
||||||
|
latest_end_time = latest_doc["T"]
|
||||||
|
time_threshold = latest_end_time - (20) # 60 seconds before the latest endTime
|
||||||
|
pipeline = [
|
||||||
|
# Filter based on endTime
|
||||||
|
{"$match": {"T": {"$gte": time_threshold}}},
|
||||||
|
# Group by start time and end time
|
||||||
|
{
|
||||||
|
"$group": {
|
||||||
|
"_id": {"startTime": "$t", "endTime": "$T"},
|
||||||
|
"open": {"$first": "$o"},
|
||||||
|
"high": {"$max": "$h"},
|
||||||
|
"low": {"$min": "$l"},
|
||||||
|
"close": {"$last": "$c"},
|
||||||
|
"volume": {"$sum": "$v"},
|
||||||
|
"weighted_price": {"$sum": {"$multiply": ["$c", "$v"]}},
|
||||||
|
"count": {"$sum": 1},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
# Sort by start time
|
||||||
|
{"$sort": {"_id.startTime": 1}},
|
||||||
|
# Reshape for output
|
||||||
|
{
|
||||||
|
"$project": {
|
||||||
|
"_id": 0,
|
||||||
|
"time": {"$multiply": ["$_id.startTime", 1000]},
|
||||||
|
"open": 1,
|
||||||
|
"high": 1,
|
||||||
|
"low": 1,
|
||||||
|
"close": 1,
|
||||||
|
"volume": 1,
|
||||||
|
"vwap": {
|
||||||
|
"$cond": {
|
||||||
|
"if": {"$eq": ["$volume", 0]},
|
||||||
|
"then": 0,
|
||||||
|
"else": {"$divide": ["$weighted_price", "$volume"]},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trades": "$count",
|
||||||
|
"endTime": "$_id.endTime",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
candles = list(collection.aggregate(pipeline))
|
||||||
|
return candles[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)
|
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)
|
||||||
|
@ -98,6 +98,14 @@ def handle_message(message):
|
|||||||
time = message["t"]
|
time = message["t"]
|
||||||
msg = message["d"]["k"]
|
msg = message["d"]["k"]
|
||||||
msg["stime"] = time
|
msg["stime"] = time
|
||||||
|
msg["c"] = float(msg["c"])
|
||||||
|
msg["h"] = float(msg["h"])
|
||||||
|
msg["l"] = float(msg["l"])
|
||||||
|
msg["o"] = float(msg["o"])
|
||||||
|
msg["v"] = float(msg["v"])
|
||||||
|
msg["T"] = int(msg["T"])
|
||||||
|
msg["a"] = float(msg["a"])
|
||||||
|
msg["t"] = int(msg["t"])
|
||||||
msg["time"] = datetime.now(timezone).strftime("%Y/%m/%d, %H:%M:%S")
|
msg["time"] = datetime.now(timezone).strftime("%Y/%m/%d, %H:%M:%S")
|
||||||
mdb.insert_coin_price(col.lower(), msg)
|
mdb.insert_coin_price(col.lower(), msg)
|
||||||
|
|
||||||
|
423
static/js/chartjs-chart-financial.js
Normal file
423
static/js/chartjs-chart-financial.js
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
/*!
|
||||||
|
* @license
|
||||||
|
* chartjs-chart-financial
|
||||||
|
* http://chartjs.org/
|
||||||
|
* Version: 0.2.0
|
||||||
|
*
|
||||||
|
* Copyright 2024 Chart.js Contributors
|
||||||
|
* Released under the MIT license
|
||||||
|
* https://github.com/chartjs/chartjs-chart-financial/blob/master/LICENSE.md
|
||||||
|
*/
|
||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('chart.js'), require('chart.js/helpers')) :
|
||||||
|
typeof define === 'function' && define.amd ? define(['chart.js', 'chart.js/helpers'], factory) :
|
||||||
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Chart, global.Chart.helpers));
|
||||||
|
})(this, (function (chart_js, helpers) { 'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is based off controller.bar.js from the upstream Chart.js library
|
||||||
|
*/
|
||||||
|
class FinancialController extends chart_js.BarController {
|
||||||
|
|
||||||
|
static overrides = {
|
||||||
|
label: '',
|
||||||
|
|
||||||
|
parsing: false,
|
||||||
|
|
||||||
|
hover: {
|
||||||
|
mode: 'label'
|
||||||
|
},
|
||||||
|
animations: {
|
||||||
|
numbers: {
|
||||||
|
type: 'number',
|
||||||
|
properties: ['x', 'y', 'base', 'width', 'open', 'high', 'low', 'close']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'timeseries',
|
||||||
|
offset: true,
|
||||||
|
ticks: {
|
||||||
|
major: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
source: 'data',
|
||||||
|
maxRotation: 0,
|
||||||
|
autoSkip: true,
|
||||||
|
autoSkipPadding: 75,
|
||||||
|
sampleSize: 100
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: 'linear'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'index',
|
||||||
|
callbacks: {
|
||||||
|
label(ctx) {
|
||||||
|
const point = ctx.parsed;
|
||||||
|
|
||||||
|
if (!helpers.isNullOrUndef(point.y)) {
|
||||||
|
return chart_js.defaults.plugins.tooltip.callbacks.label(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {o, h, l, c} = point;
|
||||||
|
|
||||||
|
return `O: ${o} H: ${h} L: ${l} C: ${c}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getLabelAndValue(index) {
|
||||||
|
const me = this;
|
||||||
|
const parsed = me.getParsed(index);
|
||||||
|
const axis = me._cachedMeta.iScale.axis;
|
||||||
|
|
||||||
|
const {o, h, l, c} = parsed;
|
||||||
|
const value = `O: ${o} H: ${h} L: ${l} C: ${c}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: `${me._cachedMeta.iScale.getLabelForValue(parsed[axis])}`,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserBounds(scale) {
|
||||||
|
const {min, max, minDefined, maxDefined} = scale.getUserBounds();
|
||||||
|
return {
|
||||||
|
min: minDefined ? min : Number.NEGATIVE_INFINITY,
|
||||||
|
max: maxDefined ? max : Number.POSITIVE_INFINITY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement this ourselves since it doesn't handle high and low values
|
||||||
|
* https://github.com/chartjs/Chart.js/issues/7328
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
getMinMax(scale) {
|
||||||
|
const meta = this._cachedMeta;
|
||||||
|
const _parsed = meta._parsed;
|
||||||
|
const axis = meta.iScale.axis;
|
||||||
|
const otherScale = this._getOtherScale(scale);
|
||||||
|
const {min: otherMin, max: otherMax} = this.getUserBounds(otherScale);
|
||||||
|
|
||||||
|
if (_parsed.length < 2) {
|
||||||
|
return {min: 0, max: 1};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scale === meta.iScale) {
|
||||||
|
return {min: _parsed[0][axis], max: _parsed[_parsed.length - 1][axis]};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newParsedData = _parsed.filter(({x}) => x >= otherMin && x < otherMax);
|
||||||
|
|
||||||
|
let min = Number.POSITIVE_INFINITY;
|
||||||
|
let max = Number.NEGATIVE_INFINITY;
|
||||||
|
for (let i = 0; i < newParsedData.length; i++) {
|
||||||
|
const data = newParsedData[i];
|
||||||
|
min = Math.min(min, data.l);
|
||||||
|
max = Math.max(max, data.h);
|
||||||
|
}
|
||||||
|
return {min, max};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
calculateElementProperties(index, ruler, reset, options) {
|
||||||
|
const me = this;
|
||||||
|
const vscale = me._cachedMeta.vScale;
|
||||||
|
const base = vscale.getBasePixel();
|
||||||
|
const ipixels = me._calculateBarIndexPixels(index, ruler, options);
|
||||||
|
const data = me.chart.data.datasets[me.index].data[index];
|
||||||
|
const open = vscale.getPixelForValue(data.o);
|
||||||
|
const high = vscale.getPixelForValue(data.h);
|
||||||
|
const low = vscale.getPixelForValue(data.l);
|
||||||
|
const close = vscale.getPixelForValue(data.c);
|
||||||
|
|
||||||
|
return {
|
||||||
|
base: reset ? base : low,
|
||||||
|
x: ipixels.center,
|
||||||
|
y: (low + high) / 2,
|
||||||
|
width: ipixels.size,
|
||||||
|
open,
|
||||||
|
high,
|
||||||
|
low,
|
||||||
|
close
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
const me = this;
|
||||||
|
const chart = me.chart;
|
||||||
|
const rects = me._cachedMeta.data;
|
||||||
|
helpers.clipArea(chart.ctx, chart.chartArea);
|
||||||
|
for (let i = 0; i < rects.length; ++i) {
|
||||||
|
rects[i].draw(me._ctx);
|
||||||
|
}
|
||||||
|
helpers.unclipArea(chart.ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to get the bounds of the bar regardless of the orientation
|
||||||
|
* @param {Rectangle} bar the bar
|
||||||
|
* @param {boolean} [useFinalPosition]
|
||||||
|
* @return {object} bounds of the bar
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function getBarBounds(bar, useFinalPosition) {
|
||||||
|
const {x, y, base, width, height} = bar.getProps(['x', 'low', 'high', 'width', 'height'], useFinalPosition);
|
||||||
|
|
||||||
|
let left, right, top, bottom, half;
|
||||||
|
|
||||||
|
if (bar.horizontal) {
|
||||||
|
half = height / 2;
|
||||||
|
left = Math.min(x, base);
|
||||||
|
right = Math.max(x, base);
|
||||||
|
top = y - half;
|
||||||
|
bottom = y + half;
|
||||||
|
} else {
|
||||||
|
half = width / 2;
|
||||||
|
left = x - half;
|
||||||
|
right = x + half;
|
||||||
|
top = Math.min(y, base); // use min because 0 pixel at top of screen
|
||||||
|
bottom = Math.max(y, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {left, top, right, bottom};
|
||||||
|
}
|
||||||
|
|
||||||
|
function inRange(bar, x, y, useFinalPosition) {
|
||||||
|
const skipX = x === null;
|
||||||
|
const skipY = y === null;
|
||||||
|
const bounds = !bar || (skipX && skipY) ? false : getBarBounds(bar, useFinalPosition);
|
||||||
|
|
||||||
|
return bounds
|
||||||
|
&& (skipX || x >= bounds.left && x <= bounds.right)
|
||||||
|
&& (skipY || y >= bounds.top && y <= bounds.bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FinancialElement extends chart_js.BarElement {
|
||||||
|
|
||||||
|
static defaults = {
|
||||||
|
backgroundColors: {
|
||||||
|
up: 'rgba(75, 192, 192, 0.5)',
|
||||||
|
down: 'rgba(255, 99, 132, 0.5)',
|
||||||
|
unchanged: 'rgba(201, 203, 207, 0.5)',
|
||||||
|
},
|
||||||
|
borderColors: {
|
||||||
|
up: 'rgb(75, 192, 192)',
|
||||||
|
down: 'rgb(255, 99, 132)',
|
||||||
|
unchanged: 'rgb(201, 203, 207)',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
height() {
|
||||||
|
return this.base - this.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
inRange(mouseX, mouseY, useFinalPosition) {
|
||||||
|
return inRange(this, mouseX, mouseY, useFinalPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
inXRange(mouseX, useFinalPosition) {
|
||||||
|
return inRange(this, mouseX, null, useFinalPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
inYRange(mouseY, useFinalPosition) {
|
||||||
|
return inRange(this, null, mouseY, useFinalPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRange(axis) {
|
||||||
|
return axis === 'x' ? this.width / 2 : this.height / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCenterPoint(useFinalPosition) {
|
||||||
|
const {x, low, high} = this.getProps(['x', 'low', 'high'], useFinalPosition);
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y: (high + low) / 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltipPosition(useFinalPosition) {
|
||||||
|
const {x, open, close} = this.getProps(['x', 'open', 'close'], useFinalPosition);
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y: (open + close) / 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CandlestickElement extends FinancialElement {
|
||||||
|
static id = 'candlestick';
|
||||||
|
|
||||||
|
static defaults = {
|
||||||
|
...FinancialElement.defaults,
|
||||||
|
borderWidth: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
draw(ctx) {
|
||||||
|
const me = this;
|
||||||
|
|
||||||
|
const {x, open, high, low, close} = me;
|
||||||
|
|
||||||
|
let borderColors = me.options.borderColors;
|
||||||
|
if (typeof borderColors === 'string') {
|
||||||
|
borderColors = {
|
||||||
|
up: borderColors,
|
||||||
|
down: borderColors,
|
||||||
|
unchanged: borderColors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let borderColor;
|
||||||
|
if (close < open) {
|
||||||
|
borderColor = helpers.valueOrDefault(borderColors ? borderColors.up : undefined, chart_js.defaults.elements.candlestick.borderColors.up);
|
||||||
|
ctx.fillStyle = helpers.valueOrDefault(me.options.backgroundColors ? me.options.backgroundColors.up : undefined, chart_js.defaults.elements.candlestick.backgroundColors.up);
|
||||||
|
} else if (close > open) {
|
||||||
|
borderColor = helpers.valueOrDefault(borderColors ? borderColors.down : undefined, chart_js.defaults.elements.candlestick.borderColors.down);
|
||||||
|
ctx.fillStyle = helpers.valueOrDefault(me.options.backgroundColors ? me.options.backgroundColors.down : undefined, chart_js.defaults.elements.candlestick.backgroundColors.down);
|
||||||
|
} else {
|
||||||
|
borderColor = helpers.valueOrDefault(borderColors ? borderColors.unchanged : undefined, chart_js.defaults.elements.candlestick.borderColors.unchanged);
|
||||||
|
ctx.fillStyle = helpers.valueOrDefault(me.backgroundColors ? me.backgroundColors.unchanged : undefined, chart_js.defaults.elements.candlestick.backgroundColors.unchanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.lineWidth = helpers.valueOrDefault(me.options.borderWidth, chart_js.defaults.elements.candlestick.borderWidth);
|
||||||
|
ctx.strokeStyle = borderColor;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, high);
|
||||||
|
ctx.lineTo(x, Math.min(open, close));
|
||||||
|
ctx.moveTo(x, low);
|
||||||
|
ctx.lineTo(x, Math.max(open, close));
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.fillRect(x - me.width / 2, close, me.width, open - close);
|
||||||
|
ctx.strokeRect(x - me.width / 2, close, me.width, open - close);
|
||||||
|
ctx.closePath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CandlestickController extends FinancialController {
|
||||||
|
|
||||||
|
static id = 'candlestick';
|
||||||
|
|
||||||
|
static defaults = {
|
||||||
|
...FinancialController.defaults,
|
||||||
|
dataElementType: CandlestickElement.id
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultRoutes = chart_js.BarController.defaultRoutes;
|
||||||
|
|
||||||
|
updateElements(elements, start, count, mode) {
|
||||||
|
const reset = mode === 'reset';
|
||||||
|
const ruler = this._getRuler();
|
||||||
|
const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode);
|
||||||
|
|
||||||
|
for (let i = start; i < start + count; i++) {
|
||||||
|
const options = sharedOptions || this.resolveDataElementOptions(i, mode);
|
||||||
|
|
||||||
|
const baseProperties = this.calculateElementProperties(i, ruler, reset, options);
|
||||||
|
|
||||||
|
if (includeOptions) {
|
||||||
|
baseProperties.options = options;
|
||||||
|
}
|
||||||
|
this.updateElement(elements[i], i, baseProperties, mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaults = chart_js.Chart.defaults;
|
||||||
|
|
||||||
|
class OhlcElement extends FinancialElement {
|
||||||
|
static id = 'ohlc';
|
||||||
|
|
||||||
|
static defaults = {
|
||||||
|
...FinancialElement.defaults,
|
||||||
|
lineWidth: 2,
|
||||||
|
armLength: null,
|
||||||
|
armLengthRatio: 0.8
|
||||||
|
};
|
||||||
|
|
||||||
|
draw(ctx) {
|
||||||
|
const me = this;
|
||||||
|
|
||||||
|
const {x, open, high, low, close} = me;
|
||||||
|
|
||||||
|
const armLengthRatio = helpers.valueOrDefault(me.armLengthRatio, defaults.elements.ohlc.armLengthRatio);
|
||||||
|
let armLength = helpers.valueOrDefault(me.armLength, defaults.elements.ohlc.armLength);
|
||||||
|
if (armLength === null) {
|
||||||
|
// The width of an ohlc is affected by barPercentage and categoryPercentage
|
||||||
|
// This behavior is caused by extending controller.financial, which extends controller.bar
|
||||||
|
// barPercentage and categoryPercentage are now set to 1.0 (see controller.ohlc)
|
||||||
|
// and armLengthRatio is multipled by 0.5,
|
||||||
|
// so that when armLengthRatio=1.0, the arms from neighbour ohcl touch,
|
||||||
|
// and when armLengthRatio=0.0, ohcl are just vertical lines.
|
||||||
|
armLength = me.width * armLengthRatio * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (close < open) {
|
||||||
|
ctx.strokeStyle = helpers.valueOrDefault(me.options.borderColors ? me.options.borderColors.up : undefined, defaults.elements.ohlc.borderColors.up);
|
||||||
|
} else if (close > open) {
|
||||||
|
ctx.strokeStyle = helpers.valueOrDefault(me.options.borderColors ? me.options.borderColors.down : undefined, defaults.elements.ohlc.borderColors.down);
|
||||||
|
} else {
|
||||||
|
ctx.strokeStyle = helpers.valueOrDefault(me.options.borderColors ? me.options.borderColors.unchanged : undefined, defaults.elements.ohlc.borderColors.unchanged);
|
||||||
|
}
|
||||||
|
ctx.lineWidth = helpers.valueOrDefault(me.lineWidth, defaults.elements.ohlc.lineWidth);
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, high);
|
||||||
|
ctx.lineTo(x, low);
|
||||||
|
ctx.moveTo(x - armLength, open);
|
||||||
|
ctx.lineTo(x, open);
|
||||||
|
ctx.moveTo(x + armLength, close);
|
||||||
|
ctx.lineTo(x, close);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OhlcController extends FinancialController {
|
||||||
|
static id = 'ohlc';
|
||||||
|
|
||||||
|
static defaults = {
|
||||||
|
...FinancialController.defaults,
|
||||||
|
dataElementType: OhlcElement.id,
|
||||||
|
datasets: {
|
||||||
|
barPercentage: 1.0,
|
||||||
|
categoryPercentage: 1.0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateElements(elements, start, count, mode) {
|
||||||
|
const reset = mode === 'reset';
|
||||||
|
const ruler = this._getRuler();
|
||||||
|
const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode);
|
||||||
|
|
||||||
|
for (let i = start; i < start + count; i++) {
|
||||||
|
const options = sharedOptions || this.resolveDataElementOptions(i, mode);
|
||||||
|
|
||||||
|
const baseProperties = this.calculateElementProperties(i, ruler, reset, options);
|
||||||
|
|
||||||
|
if (includeOptions) {
|
||||||
|
baseProperties.options = options;
|
||||||
|
}
|
||||||
|
this.updateElement(elements[i], i, baseProperties, mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
chart_js.Chart.register(CandlestickController, OhlcController, CandlestickElement, OhlcElement);
|
||||||
|
|
||||||
|
}));
|
@ -1,183 +1,207 @@
|
|||||||
|
<!-- templates/index.html -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>XRP Candlestick Chart</title>
|
<title>Realtime Candlestick Chart</title>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/luxon@3.4.4"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/luxon/2.0.2/luxon.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-adapter-luxon/1.1.0/chartjs-adapter-luxon.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.3.1"></script>
|
||||||
<style>
|
<script src="{{ url_for('static', path='js/chartjs-chart-financial.js') }}"></script>
|
||||||
body {
|
<link href="https://cdn.jsdelivr.net/npm/flowbite@2.5.2/dist/flowbite.min.css" rel="stylesheet" />
|
||||||
font-family: Arial, sans-serif;
|
<script src="https://cdn.jsdelivr.net/npm/flowbite@2.5.2/dist/flowbite.min.js"></script>
|
||||||
margin: 20px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background-color: white;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #333;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.chart-container {
|
|
||||||
position: relative;
|
|
||||||
height: 60vh;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.info-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.info-item {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.info-label {
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
.info-value {
|
|
||||||
font-size: 1.2em;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="flex w-full mt-8">
|
||||||
<h1>XRP Candlestick Chart</h1>
|
<canvas id="chart"></canvas>
|
||||||
<div class="chart-container">
|
|
||||||
<canvas id="candlestickChart"></canvas>
|
|
||||||
</div>
|
|
||||||
<div class="info-container">
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Open</div>
|
|
||||||
<div class="info-value" id="openPrice">-</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">High</div>
|
|
||||||
<div class="info-value" id="highPrice">-</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Low</div>
|
|
||||||
<div class="info-value" id="lowPrice">-</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Close</div>
|
|
||||||
<div class="info-value" id="closePrice">-</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let chart;
|
var barCount = 100;
|
||||||
|
var barData = [];
|
||||||
|
var lineData = [];
|
||||||
|
|
||||||
async function fetchCandlestickData() {
|
var ctx = document.getElementById('chart').getContext('2d');
|
||||||
|
ctx.canvas.width = 1000;
|
||||||
|
ctx.canvas.height = 250;
|
||||||
|
|
||||||
|
var chart = new Chart(ctx, {
|
||||||
|
type: 'candlestick',
|
||||||
|
data: {
|
||||||
|
datasets: [{
|
||||||
|
label: '{{coin}}',
|
||||||
|
data: barData
|
||||||
|
}, {
|
||||||
|
label: 'Close price',
|
||||||
|
type: 'line',
|
||||||
|
data: lineData,
|
||||||
|
hidden: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
unit: 'minute',
|
||||||
|
displayFormats: {
|
||||||
|
minute: 'HH:mm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
adapters: {
|
||||||
|
date: {
|
||||||
|
zone: 'UTC'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
position: 'right'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchHistoricalData() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/data/{{coin}}/candlestick');
|
const response = await fetch('/data/{{coin}}/get-candle/all');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching candlestick data:', error);
|
console.error('Error fetching historical data:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateInfoDisplay(candlestick) {
|
async function fetchLatestData() {
|
||||||
document.getElementById('openPrice').textContent = `$${candlestick.o.toFixed(4)}`;
|
try {
|
||||||
document.getElementById('highPrice').textContent = `$${candlestick.h.toFixed(4)}`;
|
const response = await fetch('/data/{{coin}}/get-candle');
|
||||||
document.getElementById('lowPrice').textContent = `$${candlestick.l.toFixed(4)}`;
|
const data = await response.json();
|
||||||
document.getElementById('closePrice').textContent = `$${candlestick.c.toFixed(4)}`;
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching latest data:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeChart() {
|
||||||
|
// Fetch historical data first
|
||||||
|
const historicalData = await fetchHistoricalData();
|
||||||
|
if (!historicalData) return;
|
||||||
|
|
||||||
|
// Process historical data
|
||||||
|
historicalData.forEach(data => {
|
||||||
|
const candleData = {
|
||||||
|
x: data.time,
|
||||||
|
o: data.open,
|
||||||
|
h: data.high,
|
||||||
|
l: data.low,
|
||||||
|
c: data.close,
|
||||||
|
v: data.volume,
|
||||||
|
vwap: data.vwap,
|
||||||
|
trades: data.trades
|
||||||
|
};
|
||||||
|
|
||||||
|
barData.push(candleData);
|
||||||
|
lineData.push({
|
||||||
|
x: data.time,
|
||||||
|
y: data.close
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
chart.update();
|
||||||
|
|
||||||
|
// Start real-time updates
|
||||||
|
setInterval(updateChart, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateChart() {
|
async function updateChart() {
|
||||||
const data = await fetchCandlestickData();
|
const newData = await fetchLatestData();
|
||||||
if (!data) return;
|
if (!newData) return;
|
||||||
|
|
||||||
if (data.length > 0) {
|
if (barData.length >= barCount) {
|
||||||
updateInfoDisplay(data[data.length - 1]);
|
barData.shift();
|
||||||
|
lineData.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chart) {
|
const newBarData = {
|
||||||
chart.data.datasets[0].data = data;
|
x: newData.time,
|
||||||
chart.update();
|
o: newData.open,
|
||||||
|
h: newData.high,
|
||||||
|
l: newData.low,
|
||||||
|
c: newData.close,
|
||||||
|
v: newData.volume,
|
||||||
|
vwap: newData.vwap,
|
||||||
|
trades: newData.trades
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the last candle's timestamp matches the new data
|
||||||
|
const lastCandle = barData[barData.length - 1];
|
||||||
|
if (lastCandle && lastCandle.x === newData.time) {
|
||||||
|
// Update the existing candle
|
||||||
|
Object.assign(lastCandle, newBarData);
|
||||||
|
lineData[lineData.length - 1].y = newData.close;
|
||||||
} else {
|
} else {
|
||||||
const ctx = document.getElementById('candlestickChart').getContext('2d');
|
// Add new candle
|
||||||
chart = new Chart(ctx, {
|
barData.push(newBarData);
|
||||||
type: 'candlestick',
|
lineData.push({
|
||||||
data: {
|
x: newData.time,
|
||||||
datasets: [{
|
y: newData.close
|
||||||
label: 'XRP Price',
|
|
||||||
data: data,
|
|
||||||
color: {
|
|
||||||
up: 'rgb(75, 192, 75)',
|
|
||||||
down: 'rgb(192, 75, 75)',
|
|
||||||
unchanged: 'rgb(90, 90, 90)',
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: false
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'XRP Price Movements'
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
|
||||||
label: function(context) {
|
|
||||||
const d = context.raw;
|
|
||||||
return [
|
|
||||||
`Open: $${d.o.toFixed(4)}`,
|
|
||||||
`High: $${d.h.toFixed(4)}`,
|
|
||||||
`Low: $${d.l.toFixed(4)}`,
|
|
||||||
`Close: $${d.c.toFixed(4)}`
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
ticks: {
|
|
||||||
callback: function(value) {
|
|
||||||
return '$' + value.toFixed(4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
type: 'time',
|
|
||||||
time: {
|
|
||||||
unit: 'hour',
|
|
||||||
tooltipFormat: 'yyyy-MM-dd HH:mm'
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
maxTicksLimit: 10,
|
|
||||||
maxRotation: 45,
|
|
||||||
minRotation: 45
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chart.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update every 5 seconds
|
// Initialize the chart when the page loads
|
||||||
updateChart();
|
initializeChart();
|
||||||
setInterval(updateChart, 5000);
|
|
||||||
|
var update = function () {
|
||||||
|
var dataset = chart.config.data.datasets[0];
|
||||||
|
|
||||||
|
// candlestick vs ohlc
|
||||||
|
var type = document.getElementById('type').value;
|
||||||
|
chart.config.type = type;
|
||||||
|
|
||||||
|
// linear vs log
|
||||||
|
var scaleType = document.getElementById('scale-type').value;
|
||||||
|
chart.config.options.scales.y.type = scaleType;
|
||||||
|
|
||||||
|
// color
|
||||||
|
var colorScheme = document.getElementById('color-scheme').value;
|
||||||
|
if (colorScheme === 'neon') {
|
||||||
|
chart.config.data.datasets[0].backgroundColors = {
|
||||||
|
up: '#01ff01',
|
||||||
|
down: '#fe0000',
|
||||||
|
unchanged: '#999',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
delete chart.config.data.datasets[0].backgroundColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// border
|
||||||
|
var border = document.getElementById('border').value;
|
||||||
|
if (border === 'false') {
|
||||||
|
dataset.borderColors = 'rgba(0, 0, 0, 0)';
|
||||||
|
} else {
|
||||||
|
delete dataset.borderColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mixed charts
|
||||||
|
var mixed = document.getElementById('mixed').value;
|
||||||
|
if (mixed === 'true') {
|
||||||
|
chart.config.data.datasets[1].hidden = false;
|
||||||
|
} else {
|
||||||
|
chart.config.data.datasets[1].hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
[...document.getElementsByTagName('select')].forEach(element => element.addEventListener('change', update));
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
64
test.py
Normal file
64
test.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
from urllib.parse import quote_plus
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from pymongo import MongoClient
|
||||||
|
from mexc_sdk import Spot
|
||||||
|
import yaml
|
||||||
|
import os
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
with open("coin.yaml", "r") as file:
|
||||||
|
data = yaml.safe_load(file)
|
||||||
|
interest_coins = data["interest"]
|
||||||
|
|
||||||
|
MONGO_URI = f'mongodb://{quote_plus(os.getenv("DB_USER"))}:{quote_plus(os.getenv("DB_PWD"))}@{os.getenv("DB_HOST")}:{os.getenv("DB_PORT")}'
|
||||||
|
client = MongoClient(MONGO_URI)
|
||||||
|
db = client[os.getenv("DB_NAME")] # Replace with your database name
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
target = interest_coins[0] + "USDT" + "_kline"
|
||||||
|
collection = db[target.lower()]
|
||||||
|
latest_doc = collection.find_one(sort=[("T", -1)])
|
||||||
|
latest_end_time = latest_doc["T"]
|
||||||
|
time_threshold = latest_end_time - (
|
||||||
|
60 * 100
|
||||||
|
) # 100 minutes before the latest endTime
|
||||||
|
pipeline = [
|
||||||
|
# Filter based on endTime
|
||||||
|
{"$match": {"T": {"$gte": time_threshold}}},
|
||||||
|
# Group by start time and end time
|
||||||
|
{
|
||||||
|
"$group": {
|
||||||
|
"_id": {"startTime": "$t", "endTime": "$T"},
|
||||||
|
"open": {"$first": "$o"},
|
||||||
|
"high": {"$max": "$h"},
|
||||||
|
"low": {"$min": "$l"},
|
||||||
|
"close": {"$last": "$c"},
|
||||||
|
"volume": {"$sum": "$v"},
|
||||||
|
"weighted_price": {"$sum": {"$multiply": ["$c", "$v"]}},
|
||||||
|
"count": {"$sum": 1},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
# Sort by start time
|
||||||
|
{"$sort": {"_id.startTime": 1}},
|
||||||
|
# Reshape for output
|
||||||
|
{
|
||||||
|
"$project": {
|
||||||
|
"_id": 0,
|
||||||
|
"time": {"$multiply": ["$_id.startTime", 1000]},
|
||||||
|
"open": 1,
|
||||||
|
"high": 1,
|
||||||
|
"low": 1,
|
||||||
|
"close": 1,
|
||||||
|
"volume": 1,
|
||||||
|
"vwap": {"$divide": ["$weighted_price", "$volume"]},
|
||||||
|
"trades": "$count",
|
||||||
|
"endTime": "$_id.endTime",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
candles = list(collection.aggregate(pipeline))
|
||||||
|
print(candles)
|
Reference in New Issue
Block a user