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}") # 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): class Trade(BaseModel):
symbol: str symbol: str
timestamp: datetime timestamp: datetime
@ -56,15 +56,21 @@ class RunRequest(BaseModel):
id: UUID id: UUID
account: Account account: Account
mode: Mode mode: Mode
note: Optional[str] = None
debug: bool = False debug: bool = False
bt_from: datetime = None bt_from: datetime = None
bt_to: datetime = None bt_to: datetime = None
cash: int = 100000 cash: int = 100000
class RunnerView(BaseModel): class RunnerView(BaseModel):
id: UUID id: UUID
run_started: Optional[datetime] = None run_started: Optional[datetime] = None
run_mode: Mode run_mode: Mode
run_name: Optional[str] = None
run_note: Optional[str] = None
run_account: Account run_account: Account
run_stopped: Optional[datetime] = None run_stopped: Optional[datetime] = None
run_paused: Optional[datetime] = None run_paused: Optional[datetime] = None
@ -75,6 +81,8 @@ class Runner(BaseModel):
run_started: Optional[datetime] = None run_started: Optional[datetime] = None
run_mode: Mode run_mode: Mode
run_account: Account run_account: Account
run_name: Optional[str] = None
run_note: Optional[str] = None
run_stopped: Optional[datetime] = None run_stopped: Optional[datetime] = None
run_paused: Optional[datetime] = None run_paused: Optional[datetime] = None
run_thread: Optional[object] = None run_thread: Optional[object] = None
@ -108,6 +116,37 @@ class TradeUpdate(BaseModel):
cash: Optional[float] cash: Optional[float]
pos_avg_price: 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): # class Trade(BaseModel):
# order: Order # order: Order
# value: float # value: float

View File

@ -5,13 +5,24 @@ from alpaca.data.historical import StockHistoricalDataClient
from alpaca.data.requests import StockTradesRequest from alpaca.data.requests import StockTradesRequest
from alpaca.data.enums import DataFeed from alpaca.data.enums import DataFeed
from v2realbot.enums.enums import RecordType, StartBarAlign, Mode, Account from v2realbot.enums.enums import RecordType, StartBarAlign, Mode, Account
from v2realbot.common.model import StrategyInstance, Runner, RunRequest from v2realbot.common.model import StrategyInstance, Runner, RunRequest, RunArchive, RunArchiveDetail
from v2realbot.utils.utils import AttributeDict, zoneNY, dict_replace_value, Store, parse_toml_string from v2realbot.utils.utils import AttributeDict, zoneNY, dict_replace_value, Store, parse_toml_string, json_serial
from datetime import datetime from datetime import datetime
from threading import Thread, current_thread, Event, enumerate 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 import importlib
from queue import Queue 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() db = Store()
def get_all_threads(): def get_all_threads():
@ -259,6 +270,8 @@ def capsule(target: object, db: object):
i.run_stop_ev = None i.run_stop_ev = None
#ukladame radek do historie (pozdeji refactor) #ukladame radek do historie (pozdeji refactor)
save_history(id=i.id, st=target, runner=i, reason=reason) 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 #mazeme runner po skonceni instance
db.runners.remove(i) db.runners.remove(i)
@ -342,6 +355,8 @@ def run_stratin(id: UUID, runReq: RunRequest):
runner = Runner(id = i.id, runner = Runner(id = i.id,
run_started = datetime.now(zoneNY), run_started = datetime.now(zoneNY),
run_pause_ev = pe, run_pause_ev = pe,
run_name = name,
run_note = runReq.note,
run_stop_ev = se, run_stop_ev = se,
run_thread = vlakno, run_thread = vlakno,
run_account = runReq.account, run_account = runReq.account,
@ -368,4 +383,79 @@ def get_trade_history(symbol: str, timestamp_from: float, timestamp_to:float):
#print(all_trades[symbol]) #print(all_trades[symbol])
return 0, all_trades[symbol] return 0, all_trades[symbol]
except Exception as e: 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 self.limit_price = limit_price
class FillCondition(Enum): class FillCondition(str, Enum):
""" """
Execution settings: Execution settings:
fast = pro vyplneni limi orderu musi byt cena stejne fast = pro vyplneni limi orderu musi byt cena stejne
@ -22,13 +22,13 @@ class FillCondition(Enum):
""" """
FAST = "fast" FAST = "fast"
SLOW = "slow" SLOW = "slow"
class Account(Enum): class Account(str, Enum):
""" """
Accounts - keys to config Accounts - keys to config
""" """
ACCOUNT1 = "ACCOUNT1" ACCOUNT1 = "ACCOUNT1"
ACCOUNT2 = "ACCOUNT2" ACCOUNT2 = "ACCOUNT2"
class RecordType(Enum): class RecordType(str, Enum):
""" """
Represents output of aggregator Represents output of aggregator
""" """
@ -37,7 +37,7 @@ class RecordType(Enum):
CBAR = "continuosbar" CBAR = "continuosbar"
TRADE = "trade" TRADE = "trade"
class Mode(Enum): class Mode(str, Enum):
""" """
LIVE or BT LIVE or BT
""" """
@ -47,7 +47,7 @@ class Mode(Enum):
BT = "backtest" BT = "backtest"
class StartBarAlign(Enum): class StartBarAlign(str, Enum):
""" """
Represents first bar start time alignement according to timeframe Represents first bar start time alignement according to timeframe
ROUND = bar starts at 0,5,10 (for 5s timeframe) ROUND = bar starts at 0,5,10 (for 5s timeframe)

View File

@ -12,7 +12,7 @@ from fastapi.security import APIKeyHeader
import uvicorn import uvicorn
from uuid import UUID from uuid import UUID
import v2realbot.controller.services as cs 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 import FastAPI, WebSocket, WebSocketDisconnect, Depends, HTTPException, status, WebSocketException, Cookie, Query
from fastapi.responses import HTMLResponse, FileResponse from fastapi.responses import HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@ -262,6 +262,44 @@ def get_trade_history(symbol: str, timestamp_from: float, timestamp_to:float) ->
else: else:
raise HTTPException(status_code=404, detail=f"No trades found {res}") 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 #join cekej na dokonceni vsech
for i in cs.db.runners: for i in cs.db.runners:
i.run_thread.join() i.run_thread.join()

View File

@ -17,25 +17,28 @@
<body> <body>
<div id="main" class="mainConteiner flex-container"> <div id="main" class="mainConteiner flex-container">
<div id="chartContainer" class="flex-items"> <div id="chartContainer" class="flex-items">
<h4>Status: <span id="status">Not connected</span></h4> <label data-toggle="collapse" data-target="#chartContainerInner">Realtime chart</label>
<div id="formular"> <h5>Status: <span id="status">Not connected</span></h5>
<form action=""> <div id="chartContainerInner" class="collapsed collapse in">
<input type="text" id="runnerId" autocomplete="off" placeholder="StrategyID" value=""/> <div id="formular">
<button onclick="connect(event)" id="bt-conn" class="btn btn-success">Connect</button> <form action="">
<button onclick="disconnect(event)" id="bt-disc" style="display: None" class="btn btn-success">Disconnect</button> <input type="text" id="runnerId" autocomplete="off" placeholder="StrategyID" value=""/>
<!-- <label>Message: --> <input type="text" id="messageText" autocomplete="off" placeholder="WS out message"/> <button onclick="connect(event)" id="bt-conn" class="btn btn-success">Connect</button>
<button onclick="sendMessage(event)" id="bt.send" class="btn btn-success">Send</button> <button onclick="disconnect(event)" id="bt-disc" style="display: None" class="btn btn-success">Disconnect</button>
</form> <!-- <label>Message: --> <input type="text" id="messageText" autocomplete="off" placeholder="WS out message"/>
</div> <button onclick="sendMessage(event)" id="bt.send" class="btn btn-success">Send</button>
<div id="chart" style="display: None; float: left; "></div> </form>
<div class="legend" id="legend"></div> </div>
<div id="msgContainer"> <div id="chart" style="display: None; float: left; "></div>
<div class="msgContainerInner"> <div class="legend" id="legend"></div>
<div id="lines"> <div id="msgContainer">
<div class="msgContainerInner">
<div id="lines">
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div id="hist-trades" class="flex-items"> <div id="hist-trades" class="flex-items">
<div id="form-trades"> <div id="form-trades">
<label data-toggle="collapse" data-target="#trades-data">Trade history</label> <label data-toggle="collapse" data-target="#trades-data">Trade history</label>
@ -50,15 +53,16 @@
</div> </div>
</div> </div>
<div id="runner-table" class="flex-items"> <div id="runner-table" class="flex-items">
<div id="controls"> <label data-toggle="collapse" data-target="#runner-table-inner">Running</label>
<label>API-KEY: <input type="password" id="api-key" autocomplete="off"/></label> <div id="runner-table-inner" class="collapsed">
<button onclick="store_api_key(event)" id="bt-store" class="btn btn-success">Store</button> <div id="controls">
<button id="button_pause" class="btn btn-success">Pause/Unpause</button> <label>API-KEY: <input type="password" id="api-key" autocomplete="off"/></label>
<button id="button_stop" class="btn btn-success">Stop</button> <button onclick="store_api_key(event)" id="bt-store" class="btn btn-success">Store</button>
<button id="button_stopall" class="btn btn-success">Stop All</button> <button id="button_pause" class="btn btn-success">Pause/Unpause</button>
<button id="button_refresh" class="btn btn-success">Refresh</button> <button id="button_stop" class="btn btn-success">Stop</button>
</div> <button id="button_stopall" class="btn btn-success">Stop All</button>
<div id="runner-table"> <button id="button_refresh" class="btn btn-success">Refresh</button>
</div>
<table id="runnerTable" class="display" style="width:100%"> <table id="runnerTable" class="display" style="width:100%">
<thead> <thead>
<tr> <tr>
@ -95,36 +99,97 @@
</div> </div>
</div> </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" class="flex-items">
<div id="stratin-table"> <label data-toggle="collapse" data-target="#stratin-table-inner">Strategies</label>
<button id="button_add" class="btn btn-success">Add</button> <div id="stratin-table-inner" class="collapsed">
<button id="button_add_json" class="btn btn-success">Add JSON</button> <button id="button_add" class="btn btn-success">Add</button>
<button id="button_edit" class="btn btn-success">Edit</button> <button id="button_add_json" class="btn btn-success">Add JSON</button>
<button id="button_dup" class="btn btn-success">Duplicate</button> <button id="button_edit" class="btn btn-success">Edit</button>
<button id="button_copy" class="btn btn-success">Copy JSON</button> <button id="button_dup" class="btn btn-success">Duplicate</button>
<button id="button_delete" class="btn btn-success">Delete</button> <button id="button_copy" class="btn btn-success">Copy JSON</button>
<button id="button_run" class="btn btn-success">Run Strategy</button> <button id="button_delete" class="btn btn-success">Delete</button>
<table id="stratinTable" class="display" style="width:100%"> <button id="button_run" class="btn btn-success">Run Strategy</button>
<thead> <table id="stratinTable" class="display" style="width:100%">
<tr> <thead>
<th>Id</th> <tr>
<th>Id2</th> <th>Id</th>
<th>Name</th> <th>Id2</th>
<th>Symbol</th> <th>Name</th>
<th>class</th> <th>Symbol</th>
<th>script</th> <th>class</th>
<th>OR</th> <th>script</th>
<th>CR</th> <th>OR</th>
<th>Stratvars</th> <th>CR</th>
<th>add_data</th> <th>Stratvars</th>
<th>note</th> <th>add_data</th>
<th>history</th> <th>note</th>
<th></th> <th>history</th>
<th></th> <th></th>
</tr> <th></th>
</thead> </tr>
<tbody></tbody> </thead>
</table> <tbody></tbody>
</table>
</div> </div>
<div id="recordModal" class="modal fade"> <div id="recordModal" class="modal fade">
<div class="modal-dialog"> <div class="modal-dialog">
@ -292,7 +357,9 @@
</div> </div>
</div> </div>
</div> </div>
<div id="bottomContainer" class="flex-items" style="height: 800px">
<BR>
</div>
</div> </div>
<script type="text/javascript" src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script> <script type="text/javascript" src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
<script src="/static/js/mywebsocket.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}} //it is called after population
const chart = LightweightCharts.createChart(document.getElementById('chart'), chartOptions); function populate_real_time_chart() {
chart.applyOptions({ timeScale: { visible: true, timeVisible: true, secondsVisible: true }, crosshair: { if (chart !== null) {
mode: LightweightCharts.CrosshairMode.Normal, labelVisible: true chart.remove()
}}) }
const candlestickSeries = chart.addCandlestickSeries({ lastValueVisible: true, priceLineWidth:2, priceLineColor: "red", priceFormat: { type: 'price', precision: 2, minMove: 0.01 }});
candlestickSeries.priceScale().applyOptions({ //const chartOptions = { layout: { textColor: 'black', background: { type: 'solid', color: 'white' } } };
scaleMargins: { const chartOptions = { width: 1045, height: 600, leftPriceScale: {visible: true}}
top: 0.1, // highest point of the series will be 10% away from the top chart = LightweightCharts.createChart(document.getElementById('chart'), chartOptions);
bottom: 0.4, // lowest point will be 40% away from the bottom 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: ''}); const volumeSeries = chart.addHistogramSeries({title: "Volume", color: '#26a69a', priceFormat: {type: 'volume'}, priceScaleId: ''});
volumeSeries.priceScale().applyOptions({ volumeSeries.priceScale().applyOptions({
// set the positioning of the volume series // set the positioning of the volume series
scaleMargins: { scaleMargins: {
top: 0.7, // highest point of the series will be 70% away from the top top: 0.7, // highest point of the series will be 70% away from the top
bottom: 0, bottom: 0,
}, },
}); });
const vwapSeries = chart.addLineSeries({ const vwapSeries = chart.addLineSeries({
// title: "vwap", // title: "vwap",
color: '#2962FF', color: '#2962FF',
lineWidth: 1, lineWidth: 1,
lastValueVisible: false lastValueVisible: false
}) })
//chart.timeScale().fitContent(); //chart.timeScale().fitContent();
//TBD dynamicky zobrazovat vsechny indikatory //TBD dynamicky zobrazovat vsechny indikatory
//document.getElementById('chart').style.display = 'inline-block'; //document.getElementById('chart').style.display = 'inline-block';
var legendlist = document.getElementById('legend'); var legendlist = document.getElementById('legend');
var firstRow = document.createElement('div'); var firstRow = document.createElement('div');
firstRow.innerText = '-'; firstRow.innerText = '-';
// firstRow.style.color = 'white'; // firstRow.style.color = 'white';
legendlist.appendChild(firstRow); 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) { function pad(n) {
var s = ('0' + n); var s = ('0' + n);
return s.substr(s.length - 2); 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") 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 // Iterate through each element in the
// first array and if some of them // first array and if some of them
@ -92,9 +111,448 @@ function is_running(id) {
// //console.log(obj); // //console.log(obj);
// //alert(JSON.stringify(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 () { $(document).ready(function () {
//reaload hlavni tabulky //reaload hlavni tabulky
@ -239,6 +697,7 @@ $(document).ready(function () {
$('#button_refresh').click(function () { $('#button_refresh').click(function () {
runnerRecords.ajax.reload(); runnerRecords.ajax.reload();
stratinRecords.ajax.reload(); stratinRecords.ajax.reload();
archiveRecords.ajax.reload();
}) })
//button copy //button copy
@ -258,7 +717,7 @@ $(document).ready(function () {
rec.add_data_conf = row.add_data_conf; rec.add_data_conf = row.add_data_conf;
rec.note = row.note; rec.note = row.note;
rec.history = ""; rec.history = "";
jsonString = JSON.stringify(rec); jsonString = JSON.stringify(rec, null, 2);
navigator.clipboard.writeText(jsonString); navigator.clipboard.writeText(jsonString);
$('#button_copy').attr('disabled', false); $('#button_copy').attr('disabled', false);
}) })

View File

@ -16,6 +16,7 @@ function connect(event) {
console.log("nejaky error" + err) console.log("nejaky error" + err)
} }
ws.onopen = function(event) { ws.onopen = function(event) {
populate_real_time_chart()
document.getElementById("status").textContent = "Connected to" + runnerId.value document.getElementById("status").textContent = "Connected to" + runnerId.value
document.getElementById("bt-disc").style.display = "initial" document.getElementById("bt-disc").style.display = "initial"
document.getElementById("bt-conn").style.display = "none" 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 v2realbot.interfaces.live_interface import LiveInterface
from alpaca.trading.enums import OrderSide from alpaca.trading.enums import OrderSide
from v2realbot.backtesting.backtester import Backtester 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 alpaca.trading.enums import TradeEvent, OrderStatus
from threading import Event, current_thread from threading import Event, current_thread
import json import json

View File

@ -9,7 +9,7 @@ import decimal
from v2realbot.enums.enums import RecordType, Mode, StartBarAlign from v2realbot.enums.enums import RecordType, Mode, StartBarAlign
import pickle import pickle
import os import os
from v2realbot.common.model import StrategyInstance, Runner from v2realbot.common.model import StrategyInstance, Runner, RunArchive, RunArchiveDetail
from typing import List from typing import List
import tomli import tomli
from v2realbot.config import DATA_DIR, QUIET_MODE from v2realbot.config import DATA_DIR, QUIET_MODE
@ -66,6 +66,11 @@ def json_serial(obj):
return obj.__dict__ return obj.__dict__
if type(obj) is btTradeUpdate: if type(obj) is btTradeUpdate:
return obj.__dict__ 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)) raise TypeError (str(obj)+"Type %s not serializable" % type(obj))
def parse_toml_string(tomlst: str): def parse_toml_string(tomlst: str):