gui refactor backtest chart

This commit is contained in:
David Brazda
2023-04-28 21:13:05 +02:00
parent 4edcb31a7b
commit 7a3567a8a6
17 changed files with 991 additions and 129 deletions

0
db.json Normal file
View File

152
testy/tinydbTest.py Normal file
View File

@ -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 == "<OrderSide.BUY: 'buy'>")
#print(res)
#print(db.all())

View File

@ -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

View File

@ -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}")
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

View File

@ -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)

View File

@ -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()

View File

@ -17,25 +17,28 @@
<body>
<div id="main" class="mainConteiner flex-container">
<div id="chartContainer" class="flex-items">
<h4>Status: <span id="status">Not connected</span></h4>
<div id="formular">
<form action="">
<input type="text" id="runnerId" autocomplete="off" placeholder="StrategyID" value=""/>
<button onclick="connect(event)" id="bt-conn" class="btn btn-success">Connect</button>
<button onclick="disconnect(event)" id="bt-disc" style="display: None" class="btn btn-success">Disconnect</button>
<!-- <label>Message: --> <input type="text" id="messageText" autocomplete="off" placeholder="WS out message"/>
<button onclick="sendMessage(event)" id="bt.send" class="btn btn-success">Send</button>
</form>
</div>
<div id="chart" style="display: None; float: left; "></div>
<div class="legend" id="legend"></div>
<div id="msgContainer">
<div class="msgContainerInner">
<div id="lines">
<label data-toggle="collapse" data-target="#chartContainerInner">Realtime chart</label>
<h5>Status: <span id="status">Not connected</span></h5>
<div id="chartContainerInner" class="collapsed collapse in">
<div id="formular">
<form action="">
<input type="text" id="runnerId" autocomplete="off" placeholder="StrategyID" value=""/>
<button onclick="connect(event)" id="bt-conn" class="btn btn-success">Connect</button>
<button onclick="disconnect(event)" id="bt-disc" style="display: None" class="btn btn-success">Disconnect</button>
<!-- <label>Message: --> <input type="text" id="messageText" autocomplete="off" placeholder="WS out message"/>
<button onclick="sendMessage(event)" id="bt.send" class="btn btn-success">Send</button>
</form>
</div>
<div id="chart" style="display: None; float: left; "></div>
<div class="legend" id="legend"></div>
<div id="msgContainer">
<div class="msgContainerInner">
<div id="lines">
</div>
</div>
</div>
</div>
</div>
</div>
<div id="hist-trades" class="flex-items">
<div id="form-trades">
<label data-toggle="collapse" data-target="#trades-data">Trade history</label>
@ -50,15 +53,16 @@
</div>
</div>
<div id="runner-table" class="flex-items">
<div id="controls">
<label>API-KEY: <input type="password" id="api-key" autocomplete="off"/></label>
<button onclick="store_api_key(event)" id="bt-store" class="btn btn-success">Store</button>
<button id="button_pause" class="btn btn-success">Pause/Unpause</button>
<button id="button_stop" class="btn btn-success">Stop</button>
<button id="button_stopall" class="btn btn-success">Stop All</button>
<button id="button_refresh" class="btn btn-success">Refresh</button>
</div>
<div id="runner-table">
<label data-toggle="collapse" data-target="#runner-table-inner">Running</label>
<div id="runner-table-inner" class="collapsed">
<div id="controls">
<label>API-KEY: <input type="password" id="api-key" autocomplete="off"/></label>
<button onclick="store_api_key(event)" id="bt-store" class="btn btn-success">Store</button>
<button id="button_pause" class="btn btn-success">Pause/Unpause</button>
<button id="button_stop" class="btn btn-success">Stop</button>
<button id="button_stopall" class="btn btn-success">Stop All</button>
<button id="button_refresh" class="btn btn-success">Refresh</button>
</div>
<table id="runnerTable" class="display" style="width:100%">
<thead>
<tr>
@ -95,36 +99,97 @@
</div>
</div>
</div>
<div id="archive-table" class="flex-items">
<label data-toggle="collapse" data-target="#archive-table-inner">Run Archive</label>
<div id="archive-table-inner" class="collapsed">
<div id="archive-chart">
<div id="chartArchive" style="position: relative;">BT chart</div>
<div class="legend" id="legendArchive"></div>
</div>
<div id="controls">
<button id="button_show_arch" class="btn btn-success">Show</button>
<button id="button_delete_arch" class="btn btn-success">Delete</button>
<!-- <button id="button_stopall" class="btn btn-success">Stop All</button>
<button id="button_refresh" class="btn btn-success">Refresh</button> -->
</div>
<table id="archiveTable" class="display" style="width:100%">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Note</th>
<th>started</th>
<th>stopped</th>
<th>mode</th>
<th>account</th>
<th>bt_from</th>
<th>bt_to</th>
<th>stratvars</th>
<th>profit</th>
<th>tradecnt</th>
<th>end_pos</th>
<th>end_pos_avgp</th>
<th>open</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div id="delModalArchive" class="modal fade">
<div class="modal-dialog">
<form method="post" id="delFormArchive">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h4 class="modal-title_del"><i class="fa fa-plus"></i> Delete Archive</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="delidarchive" class="control-label">Id</label>
<input type="text" class="form-control" id="delidarchive" name="delidarchive" placeholder="id">
</div>
</div>
<div class="modal-footer">
<input type="submit" name="delete" id="deletearchive" class="btn btn-info" value="Delete" />
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div id="stratin-table" class="flex-items">
<div id="stratin-table">
<button id="button_add" class="btn btn-success">Add</button>
<button id="button_add_json" class="btn btn-success">Add JSON</button>
<button id="button_edit" class="btn btn-success">Edit</button>
<button id="button_dup" class="btn btn-success">Duplicate</button>
<button id="button_copy" class="btn btn-success">Copy JSON</button>
<button id="button_delete" class="btn btn-success">Delete</button>
<button id="button_run" class="btn btn-success">Run Strategy</button>
<table id="stratinTable" class="display" style="width:100%">
<thead>
<tr>
<th>Id</th>
<th>Id2</th>
<th>Name</th>
<th>Symbol</th>
<th>class</th>
<th>script</th>
<th>OR</th>
<th>CR</th>
<th>Stratvars</th>
<th>add_data</th>
<th>note</th>
<th>history</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
<label data-toggle="collapse" data-target="#stratin-table-inner">Strategies</label>
<div id="stratin-table-inner" class="collapsed">
<button id="button_add" class="btn btn-success">Add</button>
<button id="button_add_json" class="btn btn-success">Add JSON</button>
<button id="button_edit" class="btn btn-success">Edit</button>
<button id="button_dup" class="btn btn-success">Duplicate</button>
<button id="button_copy" class="btn btn-success">Copy JSON</button>
<button id="button_delete" class="btn btn-success">Delete</button>
<button id="button_run" class="btn btn-success">Run Strategy</button>
<table id="stratinTable" class="display" style="width:100%">
<thead>
<tr>
<th>Id</th>
<th>Id2</th>
<th>Name</th>
<th>Symbol</th>
<th>class</th>
<th>script</th>
<th>OR</th>
<th>CR</th>
<th>Stratvars</th>
<th>add_data</th>
<th>note</th>
<th>history</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div id="recordModal" class="modal fade">
<div class="modal-dialog">
@ -292,7 +357,9 @@
</div>
</div>
</div>
<div id="bottomContainer" class="flex-items" style="height: 800px">
<BR>
</div>
</div>
<script type="text/javascript" src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
<script src="/static/js/mywebsocket.js"></script>

View File

@ -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 = '-';
}
});

View File

@ -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 = `<div>${param.time}</div>`
}
var price = data.value
// !== undefined ? data.value : data.close;
toolTip.innerHTML += `<pre>${JSON.stringify(tradeDetails.get(param.time),null,2)}</pre><div>${price.toFixed(3)}</div>`;
//inspirace
// toolTip.innerHTML = `<div style="color: ${'#2962FF'}">Apple Inc.</div><div style="font-size: 24px; margin: 4px 0px; color: ${'black'}">
// ${Math.round(100 * price) / 100}
// </div><div style="color: ${'black'}">
// ${dateStr}
// </div>`;
// 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);
})

View File

@ -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"

View File

@ -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

View File

@ -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):