diff --git a/db.json b/db.json new file mode 100644 index 0000000..e69de29 diff --git a/testy/tinydbTest.py b/testy/tinydbTest.py new file mode 100644 index 0000000..62a62c4 --- /dev/null +++ b/testy/tinydbTest.py @@ -0,0 +1,152 @@ +from tinydb import TinyDB, Query, where +from tinydb.operations import set +from tinydb.storages import JSONStorage +from tinydb_serialization import SerializationMiddleware, Serializer +from tinydb_serialization.serializers import DateTimeSerializer +from v2realbot.common.model import Trade +from v2realbot.utils.utils import parse_toml_string, zoneNY, json_serial +from v2realbot.config import DATA_DIR +from datetime import datetime +from uuid import UUID, uuid4 +from enum import Enum +from v2realbot.common.model import Order, TradeUpdate as btTradeUpdate +from alpaca.trading.models import TradeUpdate +from alpaca.trading.enums import TradeEvent, OrderType, OrderSide, OrderType, OrderStatus +from rich import print +import json + +#storage_with_injected_serialization = JSONStorage() + +serialization = SerializationMiddleware(JSONStorage) + +#builtin DateTimeSerializer + +#customer DateTime2TimestampSerializer +class DateTime2TimestampSerializer(Serializer): + OBJ_CLASS = datetime # The class this serializer handles + + def encode(self, obj): + return str(obj.timestamp()) + + def decode(self, s): + return s + #return datetime.fromtimestamp(s) + +class EnumSerializer(Serializer): + OBJ_CLASS = Enum # The class this serializer handles + + def encode(self, obj): + return str(obj) + + def decode(self, s): + return s + +class UUIDSerializer(Serializer): + OBJ_CLASS = UUID # The class this serializer handles + + def encode(self, obj): + return str(obj) + + def decode(self, s): + return s + +class TradeUpdateSerializer(Serializer): + OBJ_CLASS = TradeUpdate # The class this serializer handles + + def encode(self, obj): + return obj.__dict__ + + def decode(self, s): + return str(s) + +class btTradeUpdateSerializer(Serializer): + OBJ_CLASS = btTradeUpdate # The class this serializer handles + + def encode(self, obj): + return obj.__dict__ + + def decode(self, s): + return str(s) + +class OrderSerializer(Serializer): + OBJ_CLASS = Order # The class this serializer handles + + def encode(self, obj): + return obj.__dict__ + + def decode(self, s): + return s + +orderList =[Order(id=uuid4(), + submitted_at = datetime(2023, 3, 17, 9, 30, 0, 0, tzinfo=zoneNY), + symbol = "BAC", + qty = 1, + status = OrderStatus.ACCEPTED, + order_type = OrderType.LIMIT, + side = OrderSide.BUY, + limit_price=22.4), + Order(id=uuid4(), + submitted_at = datetime(2023, 3, 17, 9, 30, 0, 0, tzinfo=zoneNY), + symbol = "BAC", + qty = 1, + status = OrderStatus.ACCEPTED, + order_type = OrderType.LIMIT, + side = OrderSide.BUY, + limit_price=22.4)] + +# serialization.register_serializer(DateTime2TimestampSerializer(), 'TinyDate') +# serialization.register_serializer(UUIDSerializer(), 'TinyUUID') +# serialization.register_serializer(TradeUpdateSerializer(), 'TinyTradeUpdate') +# serialization.register_serializer(btTradeUpdateSerializer(), 'TinybtTradeUpdate') +# serialization.register_serializer(OrderSerializer(), 'TinyOrder') + +a = Order(id=uuid4(), + submitted_at = datetime(2023, 3, 17, 9, 30, 0, 0, tzinfo=zoneNY), + symbol = "BAC", + qty = 1, + status = OrderStatus.ACCEPTED, + order_type = OrderType.LIMIT, + side = OrderSide.BUY, + limit_price=22.4) + +db_file = DATA_DIR + "/db.json" +db = TinyDB(db_file, default=json_serial) +db.truncate() +insert = {'datum': datetime.now(), 'side': OrderSide.BUY, 'name': 'david','id': uuid4(), 'order': orderList} + +#insert record +db.insert(a.__dict__) + +#get records by id +#res = db.search(where('id') == "0a9da064-708c-4645-8a07-e749d93a213d") +#or get one: +res = db.get(where('id') == "0a9da064-708c-4645-8a07-e749d93a213d") + + +#update one document +#db.update(set('name', "nove jmeno1"), where('id') == "0a9da064-708c-4645-8a07-e749d93a213d") + + +#get all documents +res = db.all() +print("vsechny zaznamy, res)", res) + +#fetch one docuemnt +# >>> db.get(User.name == 'John') +# {'name': 'John', 'age': 22} +# >>> db.get(User.name == 'Bobby') +# None + + +#delete record by id +#res = db.remove(where('id') == "0a9da064-708c-4645-8a07-e749d93a213d") +#print("removed", res) + +#res = db.search(qorder.order.id == "af447235-c01a-4c88-9f85-f3c267d2e2e1") +#res = db.search(qorder.orderList.side == "") +#print(res) + + +#print(db.all()) + + diff --git a/v2realbot/__pycache__/config.cpython-310.pyc b/v2realbot/__pycache__/config.cpython-310.pyc index 31817bd..586477c 100644 Binary files a/v2realbot/__pycache__/config.cpython-310.pyc and b/v2realbot/__pycache__/config.cpython-310.pyc differ diff --git a/v2realbot/common/__pycache__/model.cpython-310.pyc b/v2realbot/common/__pycache__/model.cpython-310.pyc index a47035e..23bcca9 100644 Binary files a/v2realbot/common/__pycache__/model.cpython-310.pyc and b/v2realbot/common/__pycache__/model.cpython-310.pyc differ diff --git a/v2realbot/common/model.py b/v2realbot/common/model.py index cac50ae..1ca88a0 100644 --- a/v2realbot/common/model.py +++ b/v2realbot/common/model.py @@ -25,7 +25,7 @@ from alpaca.data.enums import Exchange # raise HTTPException(status_code=404, detail=f"Could not find user with id: {id}") -#for GUI +#for GUI to fetch historical trades on given symbol class Trade(BaseModel): symbol: str timestamp: datetime @@ -56,15 +56,21 @@ class RunRequest(BaseModel): id: UUID account: Account mode: Mode + note: Optional[str] = None debug: bool = False bt_from: datetime = None bt_to: datetime = None cash: int = 100000 + + + class RunnerView(BaseModel): id: UUID run_started: Optional[datetime] = None run_mode: Mode + run_name: Optional[str] = None + run_note: Optional[str] = None run_account: Account run_stopped: Optional[datetime] = None run_paused: Optional[datetime] = None @@ -75,6 +81,8 @@ class Runner(BaseModel): run_started: Optional[datetime] = None run_mode: Mode run_account: Account + run_name: Optional[str] = None + run_note: Optional[str] = None run_stopped: Optional[datetime] = None run_paused: Optional[datetime] = None run_thread: Optional[object] = None @@ -108,6 +116,37 @@ class TradeUpdate(BaseModel): cash: Optional[float] pos_avg_price: Optional[float] +#Contains archive of running strategies (runner) - master +class RunArchive(BaseModel): + #unique id of algorun + id: UUID + #id of running strategy (stratin/runner) + strat_id: UUID + name: str + note: Optional[str] = None + started: datetime + stopped: Optional[datetime] = None + mode: Mode + account: Account + bt_from: Optional[datetime] = None + bt_to: Optional[datetime] = None + stratvars: Optional[dict] = None + profit: float = 0 + trade_count: int = 0 + end_positions: int = 0 + end_positions_avgp: float = 0 + open_orders: int = 0 + +#Contains archive of running strategies (runner) - detail data +class RunArchiveDetail(BaseModel): + id: UUID + name: str + bars: dict + #trades: Optional[dict] + indicators: dict + statinds: dict + trades: List[TradeUpdate] + # class Trade(BaseModel): # order: Order # value: float diff --git a/v2realbot/controller/services.py b/v2realbot/controller/services.py index 22dd025..098ecf2 100644 --- a/v2realbot/controller/services.py +++ b/v2realbot/controller/services.py @@ -5,13 +5,24 @@ from alpaca.data.historical import StockHistoricalDataClient from alpaca.data.requests import StockTradesRequest from alpaca.data.enums import DataFeed from v2realbot.enums.enums import RecordType, StartBarAlign, Mode, Account -from v2realbot.common.model import StrategyInstance, Runner, RunRequest -from v2realbot.utils.utils import AttributeDict, zoneNY, dict_replace_value, Store, parse_toml_string +from v2realbot.common.model import StrategyInstance, Runner, RunRequest, RunArchive, RunArchiveDetail +from v2realbot.utils.utils import AttributeDict, zoneNY, dict_replace_value, Store, parse_toml_string, json_serial from datetime import datetime from threading import Thread, current_thread, Event, enumerate -from v2realbot.config import STRATVARS_UNCHANGEABLES, ACCOUNT1_LIVE_API_KEY, ACCOUNT1_LIVE_SECRET_KEY +from v2realbot.config import STRATVARS_UNCHANGEABLES, ACCOUNT1_LIVE_API_KEY, ACCOUNT1_LIVE_SECRET_KEY, DATA_DIR import importlib from queue import Queue +from tinydb import TinyDB, Query, where +from tinydb.operations import set +import json + +arch_header_file = DATA_DIR + "/arch_header.json" +arch_detail_file = DATA_DIR + "/arch_detail.json" +#db layer to store runner archive +db_arch_h = TinyDB(arch_header_file, default=json_serial) +db_arch_d = TinyDB(arch_detail_file, default=json_serial) + +#db layer to store stratins, TBD zmigrovat do TinyDB db = Store() def get_all_threads(): @@ -259,6 +270,8 @@ def capsule(target: object, db: object): i.run_stop_ev = None #ukladame radek do historie (pozdeji refactor) save_history(id=i.id, st=target, runner=i, reason=reason) + #store in archive header and archive detail + archive_runner(runner=i, strat=target) #mazeme runner po skonceni instance db.runners.remove(i) @@ -342,6 +355,8 @@ def run_stratin(id: UUID, runReq: RunRequest): runner = Runner(id = i.id, run_started = datetime.now(zoneNY), run_pause_ev = pe, + run_name = name, + run_note = runReq.note, run_stop_ev = se, run_thread = vlakno, run_account = runReq.account, @@ -368,4 +383,79 @@ def get_trade_history(symbol: str, timestamp_from: float, timestamp_to:float): #print(all_trades[symbol]) return 0, all_trades[symbol] except Exception as e: - return (-2, f"problem {e}") \ No newline at end of file + return (-2, f"problem {e}") + +#archives runner and details +def archive_runner(runner: Runner, strat: StrategyInstance): + print("inside archive_runner") + try: + if strat.bt is not None: + bp_from = strat.bt.bp_from + bp_to = strat.bt.bp_to + else: + bp_from = None + bp_to = None + id = uuid4() + runArchive: RunArchive = RunArchive(id = id, + strat_id = runner.id, + name=runner.run_name, + note=runner.run_note, + started=runner.run_started, + stopped=runner.run_stopped, + mode=runner.run_mode, + account=runner.run_account, + bt_from=bp_from, + bt_to = bp_to, + stratvars = strat.state.vars, + profit=round(strat.state.profit,2), + trade_count=len(strat.state.tradeList), + end_positions=strat.state.positions, + end_positions_avgp=round(strat.state.avgp,3), + open_orders=9999 + ) + + runArchiveDetail: RunArchiveDetail = RunArchiveDetail(id = id, + name=runner.run_name, + bars=strat.state.bars, + indicators=strat.state.indicators, + statinds=strat.state.statinds, + trades=strat.state.tradeList) + resh = db_arch_h.insert(runArchive.__dict__) + resd = db_arch_d.insert(runArchiveDetail.__dict__) + print("archive runner finished") + return 0, str(resh) + " " + str(resd) + except Exception as e: + print(str(e)) + return -2, str(e) + +def get_all_archived_runners(): + res = db_arch_h.all() + return 0, res + +#delete runner in archive and archive detail +def delete_archived_runners_byID(id: UUID): + try: + resh = db_arch_h.remove(where('id') == id) + resd = db_arch_d.remove(where('id') == id) + if len(resh) == 0 or len(resd) == 0: + return -1, "not found "+str(resh) + " " + str(resd) + return 0, str(resh) + " " + str(resd) + except Exception as e: + return -2, str(e) + +def get_all_archived_runners_detail(): + res = db_arch_d.all() + return 0, res + +def get_archived_runner_details_byID(id: UUID): + res = db_arch_d.get(where('id') == id) + if res==None: + return -2, "not found" + else: + return 0, res + + +# change_archived_runner +# delete_archived_runner_details + + diff --git a/v2realbot/enums/__pycache__/enums.cpython-310.pyc b/v2realbot/enums/__pycache__/enums.cpython-310.pyc index 017f62b..7abbfb4 100644 Binary files a/v2realbot/enums/__pycache__/enums.cpython-310.pyc and b/v2realbot/enums/__pycache__/enums.cpython-310.pyc differ diff --git a/v2realbot/enums/enums.py b/v2realbot/enums/enums.py index 047c167..4e649af 100644 --- a/v2realbot/enums/enums.py +++ b/v2realbot/enums/enums.py @@ -13,7 +13,7 @@ class Order: self.limit_price = limit_price -class FillCondition(Enum): +class FillCondition(str, Enum): """ Execution settings: fast = pro vyplneni limi orderu musi byt cena stejne @@ -22,13 +22,13 @@ class FillCondition(Enum): """ FAST = "fast" SLOW = "slow" -class Account(Enum): +class Account(str, Enum): """ Accounts - keys to config """ ACCOUNT1 = "ACCOUNT1" ACCOUNT2 = "ACCOUNT2" -class RecordType(Enum): +class RecordType(str, Enum): """ Represents output of aggregator """ @@ -37,7 +37,7 @@ class RecordType(Enum): CBAR = "continuosbar" TRADE = "trade" -class Mode(Enum): +class Mode(str, Enum): """ LIVE or BT """ @@ -47,7 +47,7 @@ class Mode(Enum): BT = "backtest" -class StartBarAlign(Enum): +class StartBarAlign(str, Enum): """ Represents first bar start time alignement according to timeframe ROUND = bar starts at 0,5,10 (for 5s timeframe) diff --git a/v2realbot/main.py b/v2realbot/main.py index 10b3b92..23537b2 100644 --- a/v2realbot/main.py +++ b/v2realbot/main.py @@ -12,7 +12,7 @@ from fastapi.security import APIKeyHeader import uvicorn from uuid import UUID import v2realbot.controller.services as cs -from v2realbot.common.model import StrategyInstance, RunnerView, RunRequest, Trade +from v2realbot.common.model import StrategyInstance, RunnerView, RunRequest, Trade, RunArchive, RunArchiveDetail from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, HTTPException, status, WebSocketException, Cookie, Query from fastapi.responses import HTMLResponse, FileResponse from fastapi.staticfiles import StaticFiles @@ -262,6 +262,44 @@ def get_trade_history(symbol: str, timestamp_from: float, timestamp_to:float) -> else: raise HTTPException(status_code=404, detail=f"No trades found {res}") +#ARCHIVE RUNNERS SECTION + +#get all archived runners header +@app.get("/archived_runners/", dependencies=[Depends(api_key_auth)]) +def _get_all_archived_runners() -> list[RunArchive]: + res, set =cs.get_all_archived_runners() + if res == 0: + return set + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found") + +#delete archive runner from header and detail +@app.delete("/archived_runners/{runner_id}", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK) +def _delete_archived_runners_byID(runner_id): + res, id = cs.delete_archived_runners_byID(id=runner_id) + if res == 0: return id + elif res < 0: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error: {res}:{id}") + +#get all archived runners detail +@app.get("/archived_runners_detail/", dependencies=[Depends(api_key_auth)]) +def _get_all_archived_runners_detail() -> list[RunArchiveDetail]: + res, set =cs.get_all_archived_runners_detail() + if res == 0: + return set + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found") + +#get archived runners detail by id +@app.get("/archived_runners_detail/{runner_id}", dependencies=[Depends(api_key_auth)]) +def _get_archived_runner_details_byID(runner_id) -> RunArchiveDetail: + res, set = cs.get_archived_runner_details_byID(runner_id) + if res == 0: + return set + else: + raise HTTPException(status_code=404, detail=f"No runner with id: {runner_id} a {set}") + + #join cekej na dokonceni vsech for i in cs.db.runners: i.run_thread.join() diff --git a/v2realbot/static/index.html b/v2realbot/static/index.html index 5889221..c8278ae 100644 --- a/v2realbot/static/index.html +++ b/v2realbot/static/index.html @@ -17,25 +17,28 @@
-

Status: Not connected

-
-
- - - - - -
-
- -
-
-
-
+ +
Status: Not connected
+
-
@@ -50,15 +53,16 @@
-
- - - - - - -
-
+ + + +
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
IdId2NameSymbolclassscriptORCRStratvarsadd_datanotehistory
+ +
- +
+
+
diff --git a/v2realbot/static/js/mychart.js b/v2realbot/static/js/mychart.js index d0a64e5..104941b 100644 --- a/v2realbot/static/js/mychart.js +++ b/v2realbot/static/js/mychart.js @@ -1,70 +1,80 @@ -//const chartOptions = { layout: { textColor: 'black', background: { type: 'solid', color: 'white' } } }; -const chartOptions = { width: 1045, height: 600, leftPriceScale: {visible: true}} -const chart = LightweightCharts.createChart(document.getElementById('chart'), chartOptions); -chart.applyOptions({ timeScale: { visible: true, timeVisible: true, secondsVisible: true }, crosshair: { - mode: LightweightCharts.CrosshairMode.Normal, labelVisible: true -}}) -const candlestickSeries = chart.addCandlestickSeries({ lastValueVisible: true, priceLineWidth:2, priceLineColor: "red", priceFormat: { type: 'price', precision: 2, minMove: 0.01 }}); -candlestickSeries.priceScale().applyOptions({ - scaleMargins: { - top: 0.1, // highest point of the series will be 10% away from the top - bottom: 0.4, // lowest point will be 40% away from the bottom - }, -}); + +//it is called after population +function populate_real_time_chart() { + if (chart !== null) { + chart.remove() + } + + //const chartOptions = { layout: { textColor: 'black', background: { type: 'solid', color: 'white' } } }; + const chartOptions = { width: 1045, height: 600, leftPriceScale: {visible: true}} + chart = LightweightCharts.createChart(document.getElementById('chart'), chartOptions); + chart.applyOptions({ timeScale: { visible: true, timeVisible: true, secondsVisible: true }, crosshair: { + mode: LightweightCharts.CrosshairMode.Normal, labelVisible: true + }}) + const candlestickSeries = chart.addCandlestickSeries({ lastValueVisible: true, priceLineWidth:2, priceLineColor: "red", priceFormat: { type: 'price', precision: 2, minMove: 0.01 }}); + candlestickSeries.priceScale().applyOptions({ + scaleMargins: { + top: 0.1, // highest point of the series will be 10% away from the top + bottom: 0.4, // lowest point will be 40% away from the bottom + }, + }); -const volumeSeries = chart.addHistogramSeries({title: "Volume", color: '#26a69a', priceFormat: {type: 'volume'}, priceScaleId: ''}); -volumeSeries.priceScale().applyOptions({ - // set the positioning of the volume series - scaleMargins: { - top: 0.7, // highest point of the series will be 70% away from the top - bottom: 0, - }, -}); + const volumeSeries = chart.addHistogramSeries({title: "Volume", color: '#26a69a', priceFormat: {type: 'volume'}, priceScaleId: ''}); + volumeSeries.priceScale().applyOptions({ + // set the positioning of the volume series + scaleMargins: { + top: 0.7, // highest point of the series will be 70% away from the top + bottom: 0, + }, + }); -const vwapSeries = chart.addLineSeries({ -// title: "vwap", - color: '#2962FF', - lineWidth: 1, - lastValueVisible: false -}) + const vwapSeries = chart.addLineSeries({ + // title: "vwap", + color: '#2962FF', + lineWidth: 1, + lastValueVisible: false + }) -//chart.timeScale().fitContent(); + //chart.timeScale().fitContent(); -//TBD dynamicky zobrazovat vsechny indikatory -//document.getElementById('chart').style.display = 'inline-block'; -var legendlist = document.getElementById('legend'); -var firstRow = document.createElement('div'); -firstRow.innerText = '-'; -// firstRow.style.color = 'white'; -legendlist.appendChild(firstRow); + //TBD dynamicky zobrazovat vsechny indikatory + //document.getElementById('chart').style.display = 'inline-block'; + var legendlist = document.getElementById('legend'); + var firstRow = document.createElement('div'); + firstRow.innerText = '-'; + // firstRow.style.color = 'white'; + legendlist.appendChild(firstRow); + + + chart.subscribeClick(param => { + //display timestamp in trade-timestamp input field + $('#trade-timestamp').val(param.time) + }); + + chart.subscribeCrosshairMove((param) => { + if (param.time) { + const data = param.seriesData.get(vwapSeries); + const vwap = data.value !== undefined ? data.value : data.close; + const bars = param.seriesData.get(candlestickSeries); + const volumes = param.seriesData.get(volumeSeries); + firstRow.innerText = ""; + //iterate of custom indicators dictionary to get values of custom lines + // var customIndicator = {name: key, series: null} + indList.forEach(function (item) { + const ind = param.seriesData.get(item.series) + firstRow.innerText += item.name + " " + ind.value + " "; + }); + + firstRow.innerText += ' vwap' + ' ' + vwap.toFixed(2) + " O" + bars.open + " H" + bars.high + " L" + bars.low + " C" + bars.close + " V" + volumes.value + ""; + } + else { + firstRow.innerText = '-'; + } + }); +} function pad(n) { var s = ('0' + n); return s.substr(s.length - 2); } - -chart.subscribeCrosshairMove((param) => { - if (param.time) { - const data = param.seriesData.get(vwapSeries); - const vwap = data.value !== undefined ? data.value : data.close; - const bars = param.seriesData.get(candlestickSeries); - const volumes = param.seriesData.get(volumeSeries); - firstRow.innerText = ""; - //iterate of custom indicators dictionary to get values of custom lines - // var customIndicator = {name: key, series: null} - indList.forEach(function (item) { - const ind = param.seriesData.get(item.series) - firstRow.innerText += item.name + " " + ind.value + " "; - }); - - firstRow.innerText += ' vwap' + ' ' + vwap.toFixed(2) + " O" + bars.open + " H" + bars.high + " L" + bars.low + " C" + bars.close + " V" + volumes.value + ""; - - //display timestamp in trade-timestamp input field - $('#trade-timestamp').val(param.time) - - } - else { - firstRow.innerText = '-'; - } -}); diff --git a/v2realbot/static/js/mytables.js b/v2realbot/static/js/mytables.js index 3ee103c..8c326a3 100644 --- a/v2realbot/static/js/mytables.js +++ b/v2realbot/static/js/mytables.js @@ -1,5 +1,24 @@ API_KEY = localStorage.getItem("api-key") +var chart = null +//Date.prototype.toJSON = function(){ return Date.parse(this)/1000 } + +// safely handles circular references https://stackoverflow.com/questions/11616630/how-can-i-print-a-circular-structure-in-a-json-like-format +JSON.safeStringify = (obj, indent = 2) => { + let cache = []; + const retVal = JSON.stringify( + obj, + (key, value) => + typeof value === "object" && value !== null + ? cache.includes(value) + ? undefined // Duplicate reference found, discard key + : cache.push(value) && value // Store value in our collection + : value, + indent + ); + cache = null; + return retVal; + }; // Iterate through each element in the // first array and if some of them @@ -92,9 +111,448 @@ function is_running(id) { // //console.log(obj); // //alert(JSON.stringify(obj)) +var tradeDetails = new Map(); +//CHART ARCHIVED RUN - move to own file +//input array object bars = { high: [1,2,3], time: [1,2,3], close: [2,2,2]...} +//output array [{ time: 111, open: 11, high: 33, low: 333, close: 333},..] +function transform_data(data) { + transformed = [] + //get basic bars, volume and vvwap + var bars = [] + var volume = [] + var vwap = [] + data.bars.time.forEach((element, index, array) => { + sbars = {}; + svolume = {}; + svwap = {}; + + sbars["time"] = element; + sbars["close"] = data.bars.close[index] + sbars["open"] = data.bars.open[index] + sbars["high"] = data.bars.high[index] + sbars["low"] = data.bars.low[index] + + + svwap["time"] = element + svwap["value"] = data.bars.vwap[index] + + svolume["time"] = element + svolume["value"] = data.bars.volume[index] + + bars.push(sbars) + vwap.push(svwap) + volume.push(svolume) + }); + transformed["bars"] = bars + transformed["vwap"] = vwap + transformed["volume"] = volume + + //get markers - avgp line for all buys + var avgp_buy_line = [] + var avgp_markers = [] + var markers = [] + var markers_line = [] + data.trades.forEach((trade, index, array) => { + obj = {}; + a_markers = {} + timestamp = Date.parse(trade.order.filled_at)/1000 + if (trade.order.side == "buy") { + //line pro avgp markers + obj["time"] = timestamp; + obj["value"] = trade.pos_avg_price; + avgp_buy_line.push(obj) + + //avgp markers pro prumernou cenu aktualnich pozic + a_markers["time"] = timestamp + a_markers["position"] = "aboveBar" + a_markers["color"] = "#e8c76d" + a_markers["shape"] = "arrowDown" + a_markers["text"] = trade.position_qty + " " + parseFloat(trade.pos_avg_price).toFixed(3) + avgp_markers.push(a_markers) + } + + //buy sell markery + marker = {} + marker["time"] = timestamp; + // marker["position"] = (trade.order.side == "buy") ? "belowBar" : "aboveBar" + marker["position"] = (trade.order.side == "buy") ? "inBar" : "aboveBar" + marker["color"] = (trade.order.side == "buy") ? "blue" : "red" + //marker["shape"] = (trade.order.side == "buy") ? "arrowUp" : "arrowDown" + marker["shape"] = (trade.order.side == "buy") ? "circle" : "arrowDown" + marker["text"] = trade.qty + " " + trade.price + markers.push(marker) + + //prevedeme iso data na timestampy + trade.order.submitted_at = Date.parse(trade.order.submitted_at)/1000 + trade.order.filled_at = Date.parse(trade.order.filled_at)/1000 + trade.timestamp = Date.parse(trade.order.timestamp)/1000 + tradeDetails.set(timestamp, trade) + + //line pro buy/sell markery + mline = {} + mline["time"] = timestamp + mline["value"] = trade.price + markers_line.push(mline) + + // time: datesForMarkers[i].time, + // position: 'aboveBar', + // color: '#e91e63', + // shape: 'arrowDown', + // text: 'Sell @ ' + Math.floor(datesForMarkers[i].high + 2), + + + }); + transformed["avgp_buy_line"] = avgp_buy_line + transformed["markers"] = markers + transformed["markers_line"] = markers_line + transformed["avgp_markers"] = avgp_markers + //get additional indicators + //TBD + return transformed +} + +function chart_archived_run(data) { + if (chart !== null) { + chart.remove() + } + + //console.log("inside") + var transformed_data = transform_data(data) + //console.log(transformed_data) + //tbd transform indicators + //var markersData = transform_trades(data) + + // time: datesForMarkers[i].time, + // position: 'aboveBar', + // color: '#e91e63', + // shape: 'arrowDown', + // text: 'Sell @ ' + Math.floor(datesForMarkers[i].high + 2), + document.getElementById("chart").style.display = "block" + //initialize chart + var chartOptions = { width: 1300, height: 600, leftPriceScale: {visible: true}} + chart = LightweightCharts.createChart(document.getElementById('chart'), chartOptions); + chart.applyOptions({ timeScale: { visible: true, timeVisible: true, secondsVisible: true }, crosshair: { + mode: LightweightCharts.CrosshairMode.Normal, labelVisible: true + }}) + var archCandlestickSeries = chart.addCandlestickSeries({ lastValueVisible: true, priceLineWidth:2, priceLineColor: "red", priceFormat: { type: 'price', precision: 2, minMove: 0.01 }}); + archCandlestickSeries.priceScale().applyOptions({ + scaleMargins: { + top: 0.1, // highest point of the series will be 10% away from the top + bottom: 0.4, // lowest point will be 40% away from the bottom + }, + }); + + var archVwapSeries = chart.addLineSeries({ + // title: "vwap", + color: '#2962FF', + lineWidth: 1, + lastValueVisible: false + }); + + var archVolumeSeries = chart.addHistogramSeries({title: "Volume", color: '#26a69a', priceFormat: {type: 'volume'}, priceScaleId: ''}); + archVolumeSeries.priceScale().applyOptions({ + // set the positioning of the volume series + scaleMargins: { + top: 0.7, // highest point of the series will be 70% away from the top + bottom: 0, + }, + }); + + archVwapSeries.setData(transformed_data["vwap"]) + archCandlestickSeries.setData(transformed_data["bars"]) + archVolumeSeries.setData(transformed_data["volume"]) + + + var avgBuyLine = chart.addLineSeries({ + // title: "avgpbuyline", + color: '#e8c76d', + // color: 'transparent', + lineWidth: 1, + lastValueVisible: false + }); + + avgBuyLine.setData(transformed_data["avgp_buy_line"]); + + avgBuyLine.setMarkers(transformed_data["avgp_markers"]) + + var markersLine = chart.addLineSeries({ + // title: "avgpbuyline", + // color: '#d6d1c3', + color: 'transparent', + lineWidth: 1, + lastValueVisible: false + }); + + markersLine.setData(transformed_data["markers_line"]); + + //console.log("markers") + //console.log(transformed_data["markers"]) + + markersLine.setMarkers(transformed_data["markers"]) + //TBD dynamicky + //pokud je nazev atributu X_candles vytvorit candles + //pokud je objekt Y_line pak vytvorit lajnu + //pokud je objekt Z_markers pak vytvorit markers + //pokud je Z = X nebo Y, pak markers dat na danou lajnu (priklad vvwap_line, avgp_line, avgp_markers) + //udelat si nahodny vyber barev z listu + + //DO BUDOUCNA MARKERS + // chart.subscribeCrosshairMove(param => { + // console.log(param.hoveredObjectId); + // }); + + + //define tooltip + const container1 = document.getElementById('chart'); + + const toolTipWidth = 90; + const toolTipHeight = 90; + const toolTipMargin = 15; + + // Create and style the tooltip html element + const toolTip = document.createElement('div'); + //width: 90px; , height: 80px; + toolTip.style = `position: absolute; display: none; padding: 8px; box-sizing: border-box; font-size: 12px; text-align: left; z-index: 1000; top: 12px; left: 12px; pointer-events: none; border: 1px solid; border-radius: 2px;font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;`; + toolTip.style.background = 'white'; + toolTip.style.color = 'black'; + toolTip.style.borderColor = '#2962FF'; + container1.appendChild(toolTip); + + + //TODO onlick zkopirovat timestamp param.time + // chart.subscribeClick(param => { + // $('#trade-timestamp').val(param.time) + // //alert(JSON.safeStringify(param)) + // //console.log(param.hoveredObjectId); + // }); + + //chart.subscribeCrosshairMove(param => { + + chart.subscribeClick(param => { + $('#trade-timestamp').val(param.time) + if ( + param.point === undefined || + !param.time || + param.point.x < 0 || + param.point.x > container1.clientWidth || + param.point.y < 0 || + param.point.y > container1.clientHeight + ) { + toolTip.style.display = 'none'; + } else { + //vyber serie s jakou chci pracovat - muzu i dynamicky + //je to mapa https://tradingview.github.io/lightweight-charts/docs/api/interfaces/MouseEventParams + + //key = series (key.seriestype vraci Line/Candlestick atp.) https://tradingview.github.io/lightweight-charts/docs/api/interfaces/SeriesOptionsMap + + toolTip.style.display = 'none'; + toolTip.innerHTML = ""; + var data = param.seriesData.get(markersLine); + if (data !== undefined) { + //param.seriesData.forEach((value, key) => { + //console.log("key",key) + //console.log("value",value) + + //data = value + //DOCASNE VYPNUTO + toolTip.style.display = 'block'; + + //console.log(JSON.safeStringify(key)) + if (toolTip.innerHTML == "") { + toolTip.innerHTML = `
${param.time}
` + } + var price = data.value + // !== undefined ? data.value : data.close; + + + toolTip.innerHTML += `
${JSON.stringify(tradeDetails.get(param.time),null,2)}
${price.toFixed(3)}
`; + + //inspirace + // toolTip.innerHTML = `
Apple Inc.
+ // ${Math.round(100 * price) / 100} + //
+ // ${dateStr} + //
`; + + + // Position tooltip according to mouse cursor position + toolTip.style.left = param.point.x+120 + 'px'; + toolTip.style.top = param.point.y-100 + 'px'; + } + //}); + } + }); + + + + + chart.timeScale().fitContent(); + + //TBD other dynamically created indicators + +} + + + +//ARCHIVE TABLES +$(document).ready(function () { + archiveRecords.ajax.reload(); + + //disable buttons (enable on row selection) + $('#button_show_arch').attr('disabled','disabled'); + $('#button_delete_arch').attr('disabled','disabled'); + + + //selectable rows in archive table + $('#archiveTable tbody').on('click', 'tr', function () { + if ($(this).hasClass('selected')) { + $(this).removeClass('selected'); + $('#button_show_arch').attr('disabled','disabled'); + $('#button_delete_arch').attr('disabled','disabled'); + } else { + stratinRecords.$('tr.selected').removeClass('selected'); + $(this).addClass('selected'); + $('#button_show_arch').attr('disabled',false); + $('#button_delete_arch').attr('disabled',false); + } + }); + + //delete button + $('#button_delete_arch').click(function () { + row = archiveRecords.row('.selected').data(); + window.$('#delModalArchive').modal('show'); + $('#delidarchive').val(row.id); + }); + + + //show button + $('#button_show_arch').click(function () { + row = archiveRecords.row('.selected').data(); + $('#button_show_arch').attr('disabled',true); + $.ajax({ + url:"/archived_runners_detail/"+row.id, + beforeSend: function (xhr) { + xhr.setRequestHeader('X-API-Key', + API_KEY); }, + method:"GET", + contentType: "application/json", + dataType: "json", + success:function(data){ + $('#button_show_arch').attr('disabled',false); + //$('#chartArchive').append(JSON.stringify(data,null,2)); + console.log(JSON.stringify(data,null,2)); + chart_archived_run(data); + }, + error: function(xhr, status, error) { + var err = eval("(" + xhr.responseText + ")"); + window.alert(JSON.stringify(xhr)); + //console.log(JSON.stringify(xhr)); + $('#button_show_arch').attr('disabled',false); + } + }) + }); + + + + + + + + + + +}) + +//delete modal +$("#delModalArchive").on('submit','#delFormArchive', function(event){ + event.preventDefault(); + $('#deletearchive').attr('disabled','disabled'); + id = $('#delidarchive').val() + //var formData = $(this).serializeJSON(); + $.ajax({ + url:"/archived_runners/"+id, + beforeSend: function (xhr) { + xhr.setRequestHeader('X-API-Key', + API_KEY); }, + method:"DELETE", + contentType: "application/json", + dataType: "json", + success:function(data){ + $('#delFormArchive')[0].reset(); + window.$('#delModalArchive').modal('hide'); + $('#deletearchive').attr('disabled', false); + archiveRecords.ajax.reload(); + }, + error: function(xhr, status, error) { + var err = eval("(" + xhr.responseText + ")"); + window.alert(JSON.stringify(xhr)); + console.log(JSON.stringify(xhr)); + $('#deletearchive').attr('disabled', false); + } + }) +}); + + +//https://www.w3schools.com/jsref/jsref_tolocalestring.asp +function format_date(datum) { + //const options = { weekday: 'long', year: 'numeric', month: 'numeric', day: 'numeric', }; + const options = {dateStyle: "short", timeStyle: "short"} + const date = new Date(datum); + return date.toLocaleString('cs-CZ', options); +} + +//stratin table +var archiveRecords = + $('#archiveTable').DataTable( { + ajax: { + url: '/archived_runners/', + dataSrc: '', + beforeSend: function (xhr) { + xhr.setRequestHeader('X-API-Key', + API_KEY); }, + error: function(xhr, status, error) { + //var err = eval("(" + xhr.responseText + ")"); + //window.alert(JSON.stringify(xhr)); + console.log(JSON.stringify(xhr)); + } + }, + columns: [{ data: 'id' }, + {data: 'name'}, + {data: 'note'}, + {data: 'started'}, + {data: 'stopped'}, + {data: 'mode'}, + {data: 'account', visible: true}, + {data: 'bt_from', visible: true}, + {data: 'bt_to', visible: true}, + {data: 'stratvars', visible: true}, + {data: 'profit'}, + {data: 'trade_count', visible: true}, + {data: 'end_positions', visible: true}, + {data: 'end_positions_avgp', visible: true}, + {data: 'open_orders', visible: true} + ], + columnDefs: [{ + targets: [3,4,7,8], + render: function ( data, type, row ) { + return format_date(data) + }, + }], + order: [[4, 'desc']], + paging: true, + lengthChange: false, + // createdRow: function( row, data, dataIndex){ + // if (is_running(data.id) ){ + // alert("runner"); + // $(row).addClass('highlight'); + // } + //} + } ); + +//STRATIN and RUNNERS TABELS $(document).ready(function () { //reaload hlavni tabulky @@ -239,6 +697,7 @@ $(document).ready(function () { $('#button_refresh').click(function () { runnerRecords.ajax.reload(); stratinRecords.ajax.reload(); + archiveRecords.ajax.reload(); }) //button copy @@ -258,7 +717,7 @@ $(document).ready(function () { rec.add_data_conf = row.add_data_conf; rec.note = row.note; rec.history = ""; - jsonString = JSON.stringify(rec); + jsonString = JSON.stringify(rec, null, 2); navigator.clipboard.writeText(jsonString); $('#button_copy').attr('disabled', false); }) diff --git a/v2realbot/static/js/mywebsocket.js b/v2realbot/static/js/mywebsocket.js index 395349a..f26ab3c 100644 --- a/v2realbot/static/js/mywebsocket.js +++ b/v2realbot/static/js/mywebsocket.js @@ -16,6 +16,7 @@ function connect(event) { console.log("nejaky error" + err) } ws.onopen = function(event) { + populate_real_time_chart() document.getElementById("status").textContent = "Connected to" + runnerId.value document.getElementById("bt-disc").style.display = "initial" document.getElementById("bt-conn").style.display = "none" diff --git a/v2realbot/strategy/__pycache__/base.cpython-310.pyc b/v2realbot/strategy/__pycache__/base.cpython-310.pyc index 5ab3345..9dde1bb 100644 Binary files a/v2realbot/strategy/__pycache__/base.cpython-310.pyc and b/v2realbot/strategy/__pycache__/base.cpython-310.pyc differ diff --git a/v2realbot/strategy/base.py b/v2realbot/strategy/base.py index 6799f2e..04a400b 100644 --- a/v2realbot/strategy/base.py +++ b/v2realbot/strategy/base.py @@ -17,7 +17,8 @@ from v2realbot.interfaces.backtest_interface import BacktestInterface from v2realbot.interfaces.live_interface import LiveInterface from alpaca.trading.enums import OrderSide from v2realbot.backtesting.backtester import Backtester -from alpaca.trading.models import TradeUpdate +#from alpaca.trading.models import TradeUpdate +from v2realbot.common.model import TradeUpdate from alpaca.trading.enums import TradeEvent, OrderStatus from threading import Event, current_thread import json diff --git a/v2realbot/utils/__pycache__/utils.cpython-310.pyc b/v2realbot/utils/__pycache__/utils.cpython-310.pyc index 85233dc..ceec23a 100644 Binary files a/v2realbot/utils/__pycache__/utils.cpython-310.pyc and b/v2realbot/utils/__pycache__/utils.cpython-310.pyc differ diff --git a/v2realbot/utils/utils.py b/v2realbot/utils/utils.py index 7424b83..bd563eb 100644 --- a/v2realbot/utils/utils.py +++ b/v2realbot/utils/utils.py @@ -9,7 +9,7 @@ import decimal from v2realbot.enums.enums import RecordType, Mode, StartBarAlign import pickle import os -from v2realbot.common.model import StrategyInstance, Runner +from v2realbot.common.model import StrategyInstance, Runner, RunArchive, RunArchiveDetail from typing import List import tomli from v2realbot.config import DATA_DIR, QUIET_MODE @@ -66,6 +66,11 @@ def json_serial(obj): return obj.__dict__ if type(obj) is btTradeUpdate: return obj.__dict__ + if type(obj) is RunArchive: + return obj.__dict__ + if type(obj) is RunArchiveDetail: + return obj.__dict__ + raise TypeError (str(obj)+"Type %s not serializable" % type(obj)) def parse_toml_string(tomlst: str):