diff --git a/app.py b/app.py index 94da6e5..fa9839c 100644 --- a/app.py +++ b/app.py @@ -5,11 +5,13 @@ from fastapi.staticfiles import StaticFiles from pymongo import MongoClient from datetime import datetime import uvicorn + from urllib.parse import quote_plus from dotenv import load_dotenv import os import yaml + with open("coin.yaml", "r") as file: data = yaml.safe_load(file) interest_coins = data["interest"] @@ -17,7 +19,8 @@ interest_coins = data["interest"] load_dotenv() app = FastAPI() - +app.mount("/static", StaticFiles(directory="static"), name="static") +data_points = [] # Setup templates directory templates = Jinja2Templates(directory="templates") @@ -45,6 +48,25 @@ async def view_coin_graph(request: Request, coin: str): 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}") async def get_data(coin): collection = db[coin] @@ -68,5 +90,117 @@ async def get_data(coin): 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__": uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True) diff --git a/mexc-socket.py b/mexc-socket.py index 171f776..1087de6 100644 --- a/mexc-socket.py +++ b/mexc-socket.py @@ -98,6 +98,14 @@ def handle_message(message): time = message["t"] msg = message["d"]["k"] 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") mdb.insert_coin_price(col.lower(), msg) diff --git a/static/js/chartjs-chart-financial.js b/static/js/chartjs-chart-financial.js new file mode 100644 index 0000000..af5a1e6 --- /dev/null +++ b/static/js/chartjs-chart-financial.js @@ -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); + +})); diff --git a/templates/candle_coin.html b/templates/candle_coin.html index 7b7a317..85cadda 100644 --- a/templates/candle_coin.html +++ b/templates/candle_coin.html @@ -1,183 +1,207 @@ + + - XRP Candlestick Chart - - - - + Realtime Candlestick Chart + + + + + + + -
-

XRP Candlestick Chart

-
- -
-
-
-
Open
-
-
-
-
-
High
-
-
-
-
-
Low
-
-
-
-
-
Close
-
-
-
-
+
+
- - + + \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..6043fd7 --- /dev/null +++ b/test.py @@ -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)