diff --git a/.cproject b/.cproject index f7d29db4..5c5ca115 100644 --- a/.cproject +++ b/.cproject @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/.gitignore b/.gitignore index 4d74ed97..61f97701 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,12 @@ CMakeCache.txt CMakeFiles cmake_install.cmake Makefile -bin +/bin /log /build/ -data -www/report* -local.conf \ No newline at end of file +/data +/www/report* +/local.conf +/secure_data +/run/ +/prod/ diff --git a/.settings/language.settings.xml b/.settings/language.settings.xml index 95a1c0f5..c6ae1834 100644 --- a/.settings/language.settings.xml +++ b/.settings/language.settings.xml @@ -1,28 +1,15 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/CMakeLists.txt b/CMakeLists.txt index b0fb8be8..6f423c93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,15 +4,17 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin/) include_directories(BEFORE src/imtjson/src src/server/src src ) add_compile_options(-std=c++17) -add_compile_options(-Wall -Werror -Wno-noexcept-type) +add_compile_options(-Wall -Wno-noexcept-type) add_subdirectory (src/imtjson/src/imtjson EXCLUDE_FROM_ALL) add_subdirectory (src/server/src/simpleServer EXCLUDE_FROM_ALL) add_subdirectory (src/main) +add_subdirectory (src/trainer) add_subdirectory (src/coinmate) add_subdirectory (src/poloniex) add_subdirectory (src/binance) add_subdirectory (src/deribit) +add_subdirectory (src/bitmex) install(DIRECTORY conf DESTINATION ".") diff --git a/README.md b/README.md index f8a65c5f..1bf09bae 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,3 @@ -**Developer note:** -``` - A new major version is in the development. You - can expect a lot of changes. Because of this, -I have a little time to handle support for -the current version in the master. -Stay tuned, this will be massive! -``` - - # mmbot Market Making trading bot for cryptomarkets @@ -24,4 +14,4 @@ Sorry, english documentation is not complete Česká dokumentace * [Obecné seznámení](doc/cs.md) * [Config](doc/config_cs.md) -* [Poznámka k obchodování na Deribit a Bitmex](doc/deribit_bitmex_cs.md) +* [Poznámka k obchodování na Deribit a Bitmex](doc/deribit_bitmex_cs.md) \ No newline at end of file diff --git a/build b/build new file mode 100755 index 00000000..377f8e08 --- /dev/null +++ b/build @@ -0,0 +1,8 @@ +#!/bin/bash + +mkdir -p ./data +mkdir -p ./secure_data +mkdir -p ./log +mkdir -p ./run +cmake -DCMAKE_BUILD_TYPE=RELWITHDEBINFO . +make all -j `nproc` diff --git a/conf/brokers.conf b/conf/brokers.conf index 5694b1d1..2d57fe78 100644 --- a/conf/brokers.conf +++ b/conf/brokers.conf @@ -6,21 +6,20 @@ # # = # -# The broker obviously needs a path to a configuration file. The configuration -# file contains all required url and **public and private keys as well**. -# -# The broker used by 'traders' (traders.conf) -# -# -# The working director for the broker is directory of this config. You can -# use absolute or relative path to the broker. Path to the config file is -# mostly relative to this config file. +# The broker usually needs path to configuration file in secure_data. If the configuration file +# doesn't exists, it is created soon on later when the user sets the api keys. Without keys, the +# only public interface is available # +# All directories are relative to this directory # [brokers] -coinmate=../bin/brokers/coinmate brokers/coinmate.conf -poloniex=../bin/brokers/poloniex brokers/poloniex.conf -binance=../bin/brokers/binance brokers/binance.conf -deribit=../bin/brokers/deribit brokers/deribit.conf +coinmate=../bin/brokers/coinmate ../secure_data/coinmate +poloniex=../bin/brokers/poloniex ../secure_data/poloniex +binance=../bin/brokers/binance ../secure_data/binance +deribit=../bin/brokers/deribit ../secure_data/deribit +bitmex=../bin/brokers/bitmex ../secure_data/bitmex +trainer1=../bin/brokers/trainer ../secure_data/trainer1 +trainer2=../bin/brokers/trainer ../secure_data/trainer2 + diff --git a/conf/brokers/binance.conf b/conf/brokers/binance.conf deleted file mode 100644 index 691afea7..00000000 --- a/conf/brokers/binance.conf +++ /dev/null @@ -1,5 +0,0 @@ -[api] -secret= -key= -url=https://api.binance.com - diff --git a/conf/brokers/coinmate.conf b/conf/brokers/coinmate.conf deleted file mode 100644 index 9471eed3..00000000 --- a/conf/brokers/coinmate.conf +++ /dev/null @@ -1,11 +0,0 @@ -# -# Configuration of coinmate broker -# - -[api] -private_key= -public_key= -clientid= -api_url=https://coinmate.io/api/ - - diff --git a/conf/brokers/deribit.conf b/conf/brokers/deribit.conf deleted file mode 100644 index 53737adc..00000000 --- a/conf/brokers/deribit.conf +++ /dev/null @@ -1,5 +0,0 @@ -[api] -key= -secret= -url=https://www.deribit.com/api/v2 -scope=session:apiconsole diff --git a/conf/brokers/poloniex.conf b/conf/brokers/poloniex.conf deleted file mode 100644 index 421c6d54..00000000 --- a/conf/brokers/poloniex.conf +++ /dev/null @@ -1,9 +0,0 @@ -[api] -secret= -key= -private_url=https://poloniex.com/tradingApi -public_url=https://poloniex.com/public - -[order_db] -path=../../data/_poloniex_orderdb - diff --git a/conf/ident b/conf/ident new file mode 100644 index 00000000..dca659cb --- /dev/null +++ b/conf/ident @@ -0,0 +1 @@ +test_user diff --git a/conf/mmbot.conf b/conf/mmbot.conf index 9b038ecb..b09457ab 100644 --- a/conf/mmbot.conf +++ b/conf/mmbot.conf @@ -1,122 +1,83 @@ -# General config rules -# -# The format of the configuration is similar to standard ini file -# -# [This is begin of section] -# key=value -# @directive +[service] + +# specifies an instance file +# the instance file is file created when service starts and it +# is used to communicate with the command-line interface. # -# Duplicate sections are merged -# Duplicate keys in the same sections replaces each other -# If you need a multiline value, use backslash at the end \ -# of line -# -# -# Directives can control how the configuration file is processed +# this file also used to detect, whether the bot is running, to +# support start/stop/restart and other features. You can run multiple +# instances where the each instance has different inst_file. # -# @include - includes other file. Its content is merged with -# the current. +# if you delete instance file while the bot is running, you will +# unable to control the service. You need to kill it manually +# using killall (or pidof mmbot and kill) # -# @template - merges specified section to current section. The specified -# section acts as template, its keys are imported -# to current section. This reduces duplicated declarations -# -# [aaa] -# a=1 -# b=2 -# c=3 -# -# [bbb] -# @template aaa - imports a,b,c into section [aaa] -# d=4 +inst_file=../run/inst.pid +# Allows to change effective user. This is useful, when the bot +# is started under root account. However it is not recommended at all. +# it is better to start the bot directly through the "su". +# user=mmbot -########################################### -# -# Paths: All paths can be relative to the configuration file where they appear. -# -########################################### -# -# Service settinggs -# -# inst_file - specify path to an instance file (pid file) -# - this file identifies the service while it runs -# - the file is created on start -# - required -# -# name - name of the service (appears on various places) +# socket file used to communicate with webserver + +socket=../run/mmbot.socket:666 + +# enable listening on specified port. It is disabled by default to enable multiple instalations # -# user - switch to user after start +#listen=localhost:11223 # -[service] -inst_file=../data/inst.pid +# path to the directory, where traders data are stored -# user=mmbot +storage_path=../data +# data are stored in binary format. You can disable binary format +# then data are stored as json. This makes large files, but readable +# for humans. Use this if you need to edit the data files. # -# Enables logging -# -# file - path to log file (it is created) -# level - minimal level: debug, info, progress, note, warning, error, fatal -# -# Note: option -d temporarily sets level to debug. Option -v redirects log to stdout +# Note that you can edit data files only if the bot is stopped + +#storage_binary=yes + +# specify admin login:password in base64 to log into webadmin in case that you lost access # +#admin=YWRtaW46c2VjcmV0 + + [log] + +# specify path and name for the log file + file=../log/logfile -level=info -# -# Trades common configuration -# -# storage_path - path to directory where data files are stored -# -# +# defines level of logging +# available levels are (from the most verbose to less) +# - debug - log everything +# - info - important informations, such subresults of calculations etc. +# - progress - used to log progress of various operations +# - note - important messages, that should not be harmfull +# - warning - important message, that may be harmfull or recoverable errors +# - error - errors mostly unrecoverable +# - fatal - fatal errors and crashes -[traders] -storage_path=../data +level=info -# -# Report configuration -# -# path - path where reports are placed -# interval - exports records not older than specified time in miliseconds -# -# http_bind - enables local webserver to see results. The value specifies -# address and port where the content is served -# -# (use http://) -# http_auth - enable basic autentification. Specify one or more login -# tokens separated by a space. To generate login token, use -# following command -# -# $echo -n "login:password" | base64 -# -# a2np - counts accumulated assets as profit. -# This causes that overlall normalized profit -# will partially copy development of the asset's price, because -# accumulated assets are part of the profit. -# If this option is turned off while the options -# `acum_factor_buy` and `acum_factor_sell` are set to 1 results -# accumulated profit to zero -# because all of the profit is used to accumulation. -# Default is off - [report] + +# specify path where the report is stored + path=../www +# specify defaul interval of reported data (configured from webadmin) + interval=864000000 -# http_bind=127.0.0.1:11223 -# -## echo -n "admin:secret" | base64 -# http_auth=YWRtaW46c2VjcmV0 -# +# daily_report_service=../../mmbot_couchdb_report/bin/mmbot_perf ../../mmbot_couchdb_report/conf/mmbot_perf.conf ident @include brokers.conf -@include traders.conf diff --git a/conf/traders.conf b/conf/traders.conf deleted file mode 100644 index 4edbe44c..00000000 --- a/conf/traders.conf +++ /dev/null @@ -1,70 +0,0 @@ -# -# Configuration of traders -# -# list - list of IDs of enabled traders. -# - each ID refers to name of section which contains configuration -# - IDs are separated by space -# -# -[traders] -list=ltcczk btcczk - - -# -# an example of trader (without template) -# -# broker - name of the broker (see brokers.conf) -# -# pair_symbol - specifies which pair is traded -# external_assets - amount of assets kept off the stockmarket. -# This value is also often used to increase volume -# of trading by specifiing amount of assets which -# in reality doesn't exists. -# -# dynmult_raise - controls multiplicator. After each trade, -# the multiplicator is raised by specified percent -# (200 means 200%) -# dynmult_fall - specifies, how much the multiplocator falls each -# minute without a trade. The multiplicator never -# goes below 1 -# title - name of this trader -# -# acum_factor_buy - sets how many of the normalized profit is used to -# buy extra assets while buying. This value is between 0 and 1. -# where 0 means no assets, 1 means all profit is used to buy -# assets. Default is 1. -# acum_factor_sell - sets how many of the normalized profit is used to -# leave extra assets while selling. This value is between 0 and 1. -# where 0 means no assets are left, 1 means that assets of -# calculated profit are substracted from the order size. -# Default is 0 - -[ltcczk] - -broker=coinmate -pair_symbol=LTC_CZK -external_assets=20 -dynmult_raise=200 -dynmult_fall=2 -title=LTC/CZK - -# -# An example of templat section -# -# -# - -[coinmate_template] -broker=coinmate -dynmult_raise=250 -dynmult_fall=1 - -# -# A trader uses template - -[btcczk] -@template coinmate_template -pair_symbol=BTC_CZK -external_assets=0.5 -title=BTC/CZK - diff --git a/conf/traders.conf.ex b/conf/traders.conf.ex deleted file mode 100644 index a73a7fb5..00000000 --- a/conf/traders.conf.ex +++ /dev/null @@ -1,98 +0,0 @@ -# -# example of configuration of real running instance -# - -[traders] -list=btcczk ltcczk grinbtc bchsvbtc usdeth_deribit usdbtc_deribit xrpczk - -[common] -dynmult_raise=200 -dynmult_fall=1 -spread_calc_max_trades=50 -spread_calc_min_trades=1 -dry_run=0 -acum_factor_buy=0.5 -acum_factor_sell=0.5 -detect_manual_trades=0 - -[coinmate] -@template common -start_time=1561100016000 -broker=coinmate - -[poloniex] -@template common -broker=poloniex - -[binance] -@template common -broker=binance - -[deribit] -@template common -broker=deribit -acum_factor_buy=0 -acum_factor_sell=0 -buy_step_mult=0.9 -sell_step_mult=0.9 -spread_calc_max_trades=50 -spread_calc_min_trades=12 -dynmult_raise=200 -dynmult_fall=1 -sliding_pos.change=5 - -[ltcczk] -@template coinmate -pair_symbol=LTC_CZK -external_assets=25 -title=LTC/CZK -sliding_pos.change=5 -sliding_pos.change.assets=15 - - -[btcczk] -@template coinmate -pair_symbol=BTC_CZK -external_assets=0.8 -title=BTC/CZK -sliding_pos.change=5 -sliding_pos.assets=0.2 - -[xrpczk] -@template coinmate -pair_symbol=XRP_CZK -external_assets=10000 -title=XRP/CZK -sliding_pos.change=10 - -[grinbtc] -@template poloniex -title=GRIN/BTC -pair_symbol=BTC_GRIN -external_assets=0 - -[bchsvbtc] -@template poloniex -title=BSV/BTC -pair_symbol=BTC_BCHSV -external_assets=0 -acum_factor_buy=0 -acum_factor_sell=0 - -[btcusdt] -@template binance -title=BTC/USDT -pair_symbol=BTCUSDT -external_assets=0.25 - -[usdbtc_deribit] -@template deribit -pair_symbol=BTC-PERPETUAL -external_assets=14000 -title=USD/BTC - -[usdeth_deribit] -@template deribit -pair_symbol=ETH-PERPETUAL -external_assets=10000 -title=USD/ETH diff --git a/doc/config_cs.md b/doc/config_cs.md index 5bdf4fb1..c0bac051 100644 --- a/doc/config_cs.md +++ b/doc/config_cs.md @@ -271,9 +271,7 @@ Nastavení neutrální pozice umožňuje řídit obchodování k maximalizaci zi **sliding_pos.hours** - Specifikuje s jakou rychlostí se posouvá cena neutrální pozice. Tato volba musí být v kombinaci s `neutral_pos`. Hodnota je hodinách. Je v celku obtížné ji dobře nastavit. Příliš malé hodnoty mohou ve výsledku generovat ztrátu. Příliš velké hodnoty zase nezajistí posun tak rychle, aby se obchodování nedostalo mimo rozsah. Výpočet nejvíc sedí na EMA v hodinovém grafu. Optimální hodnota cca 240 (deset dní) -**sliding_pos.weaken** - Specifikuje zeslabování obchodní síly se zvyšování pozice. Hodnota definuje maximální pozici (při téhle pozici bude obchodování víceméně minimální) Hodnota se zadává jako procento z `external_assets`+`neutral_pos`. Pomocí backtestů byla zjištěna optimální hodnota 10-11. Vyšší hodnoty dovolí vyšší pozici. Volbu je dobré kombinovat se sliding_pos.hours. Pokud se obchodování přesune rychle na jinou cenu, vlivem snižování obchodní síly nebude nabraná taková ztráta. Časem dojde ke srovnání neutrální ceny a aktuální ceny. Nicméně zeslabení obchodní síly snižuje potenciální zisk - -**expected_trend** - (nepovinné) Umožňuje určit očekávaný trend. Hodnota je většinou mezi -10 až 10. Hodnota -10 představuje silný downtrend. Hodnota 10 silný uptrend. Hodnota 0 je pohyb do boku. Doporučuje se v kombinaci se sliding_pos. Tato hodnota zasahuje do výpočtu tím, že posouvá equilibrium jednorázově v zadaném směru o zadaný počet procent vůči aktuální ceně. Tedy hodnota 10 při equilibrium 5000 vede na equilibrium 5500 a vůči němu se počítá velikost nákupního a prodejního pokynu. +**sliding_pos.weaken** - Specifikuje zeslabování obchodní síly se zvyšování pozice. Hodnota definuje maximální pozici (při téhle pozici bude obchodování víceméně minimální) Hodnota se zadává jako procento z `external_assets`+`neutral_pos`. Pomocí backtestů byla zjištěna optimální hodnota 10-11. Vyšší hodnoty dovolí vyšší pozici. Volbu je dobré kombinovat se sliding_pos.hours. Pokud se obchodování přesune rychle na jinou cenu, vlivem snižování obchodní síly nebude nabraná taková ztráta. Časem dojde ke srovnání neutrální ceny a aktuální ceny. Nicméně zeslabení obchodní síly snižuje potenciální zisk **accept_loss** - pokud je nenulová, definuje počet hodin od posledního trade, po kterou musí mít robot zablokované vydání pokynu v jednom směru, aby se aktivovala funkce `accept_loss`. Zároveň musí být splněno, že dynamické multiplikátory jsou rovné 1. Pokud je tedy toto splněno, robot posune equlibrium na cenu zablokovaného pokynu a tím akceptuje ztrátu vzniklou tím, že se pokyn nebude realizovat. Pokyn může být zablokován v důsledku `sliding_pos.max_pos`, ale v důsledku toho, že nejsou prostředky na burze. Je třeba si ovšem dát pozor, aby pokyn nebyl zablokován například v důsledku dlouhotrvající maintenance na burze (robot momentálně nerozpozná důvod zablokování). Proto je dobré nastavit hodnotu na řádově několik hodin, například 12 (nejdelší maintenance míval Bitfinex = 7 hodin) diff --git a/doc/cs.md b/doc/cs.md index ee85a5ea..7f3055a2 100644 --- a/doc/cs.md +++ b/doc/cs.md @@ -1,447 +1,204 @@ -# Novinky ve verzi Square +# MMBOT 2.0 -1. Upravený výpočet -2. Upravený výpočet spreadu -3. Možnost řídit faktor akumulace, nebo jej úplně vypnout -4. Redukce race conditions na burzách, zpomalení robota po exekuci -5. Řešení duplicitních orderů -6. Zamítnutí vystavení orderu, pokud původní byl exekuován -7. Nevystavování orderů příliš malých (redukce počtu hlášení z API burzy) -8. Kalkulátor - referenčí stav výpočtu -9. Detekce ručně provedených obchodů -10. Detekce změny balance na burze -11. Interní balance (možnost sledovat balanci jen pomocí vygenerovaných obchodů) -12. Příkaz `reset` snadno vyresetuje statistiky -13. Příkaz `achieve` pomůže změnit nastavení kalkulátoru, případně provede počáteční nákup +## Změny +V původním dokumentu zde byl popsán algoritmus robota a postup jak jej zprovoznit. Všechny +tyto texty jsou ve verzi 2.0 zastaralé. Verze 2.0 se instaluje jinak, provozuje jinak a výpočty +jsou též jiné +* Na výběr je několik strategií (každá přináší jinou sadu výpočtů) +* Veškeré nastavení se provádí přes webové rozhraní. Konfigurační soubor obsahuje minimum nastavení -# Nastavení a provozování robota -## Jak robot funguje? -Robot vydělává na rozdílech mezi nákupní a prodejní cenou obchodovaného assetu (třeba bitcoin). Vždy tedy nakupuje za nižší cenu, než je cena posledního obchodu, a prodává za cenu vyšší. Rozdíly v ceně mohou být malé a tedy i malé zisky, robot však provádí mnoho obchodů a tak se malé zisky mohou nasčítat ve větší zisky -**Nejedná se o HFT** Robot neobchoduje na MARKET. Naopak pouze dodává likviditu na burzu pomocí vhodného umístění LIMITních příkazů do orderbooku. Výkon robota a počet obchodů tak závisí na ostatních účastnících trhu, kteří tam nakupují a prodávají. Vhodným protějškem jsou také arbitrážní roboti, kteří jsou schopni zobchodovat limitní příkazy při reakci na pohyb na jiné burze. +## Jak robot funguje -## Na jakém trhu robot funguje nejlépe? +Obchodní robot MMBot 2.0 (dále jen Robot) provádí automatizované obchodování formou tzv. +market makingu. To je technika, kdy robot generuje zisk při obchodování ve spreadu, tedy v +situacích, kdy se cena nehýbe moc, nebo vytváří vlnky, kdy se cena po růstech vrací do původního +cenového pásma, atd. V situacích, kdy cena dlouhodobě trenduje robot nefunguje efektivně a tím hůře, +čím silnější trend je. Ovšem statisticky drtivá většina trendů se láme v korekci, ve kterých často +robot nabere zpět to co trendem ztratil a ještě něco navíc. Navíc, trendová období netrvají dlouho, zpravidla jde o krátké pumpy a dumpu následované několika dny klidu nebo vlnkování. -Ideálním trhem je trh, který jde dlouhodobě do boku avšak objevují se na něm vlnky a výkyvy. Čí vyšší jsou výkyvy nahoru a dolu, tím většího zisku může robot dosáhnout. Naopak trendující trhy, u kterých je trend strmější než denní volatilita v součtu mohou rapidně srážet výkon a přinášet ztráty - avšak krátkodobé trendy, které končí změnou trendu mohou naopak být vítané, protože to co robot zobchoduje jedním směrem pak pokryje se ziskem druhým směrem +## Kolik robot vydělává -## Jaké mám očekávat zisky? +Výše výdělku závisí na trhu i zvoleném nastavení. Neexistuje univerzální recept jak provozovat robota ziskově. Potenciál zisku pramení z market makingu v tom, že robot nenakupuje dráž než prodává (s výjimkou situací, kdy musí, protože se dostane do kraního stavu) -Robot není navržen tak, aby generoval vysoké zisky, protože primárně není úrčen pro obchod na páce. Avšak velmi záleží na nastavení. V základním nastavení lze očekávat slabé zisky kolem 10% ročně. V tomto nastavení však robot dokáže obchodovat na libovolně rozkolísaném trhu. Je však vhodné nastavit robota na větší objemy avšak dobře si spočítat, zda rozkolísání trhu nemůže robota poslat na jednu z kritických stran, kde přestává fungovat, tedy kdy robotovi dojdou buď assety nebo peníze. Pak je třeba situaci řešit buď doplněním assetů nebo peněz (nebo obchodováním na páku) +Z dlouhodobého provozu robota bylo dosaženo až 150% zisku na ETH/USD na Deribit za 4 měsíce provozu, dále cca 40% zisku na BTC/USD též na Deribitu. -Více o nastavení robota dále +Kromě toho, jedna ze strategií umožňuje 100% úspěšnost ovšem při zisku kolem 10% ročně založeného na principu toho, že po každém obchodu na účet přibudou prostředky, které již k činnosti robota nejsou potřeba a lze je odebrat. Tato strategie generuje trvalý profit bez nebezpečí, že by se robot dostal do situace, že by mu došly prostředky a nemohl obchodovat. Lze tedy robota nechat obchodovat po mnoho let a vydělané prostředky z něho postupně stahovat. Strategie se jmenuje Half-Half. -## Jaké je riziko? - -Dokud robot obchoduje pouze s prostředky bez páky, je riziko malé. Sám algoritmus dlouhodobě negeneruje ztrátu. Ztráta může přijít pouze tím, že trh, na kterém robot obchoduje jde dlouhodobě dolu a přestává generovat vlnky, nebo na něm klesají objemy. Takový trh signalizuje, že o něj obchodníci ztrácí zájem. Robot se tak může dostat do situace, kdy zůstane bagholderem, tedy bude držet assety, které nemají žádnou cenu. - -Neznamená to ale, že každý trh jdoucí dolu může být ztrátový. Pokud jde trh dolu pomaleji než zisky z vlnek, pak i takový trh může přinášet v celku zisk. - -## Co potřebuju k provozu robota? - -- Trvale běžící stroj, server nebo VPS -- Operační systém linux, ideálně Ubuntu 18, případně vrstevníci -- Připojení na internet -- účet na podporované burze -- vygenerované API klíče -- GCC a GPP 7.0 -- Git -- různé další knihovny: libssl, libcurlpp, libcurl, atd - -Více informací na install.md - -## Soubory robota - -``` - +-- bin - | + -- mmbot - | + -- brokers - | + -- coinmate - | + -- poloniex - | + -- - + -- conf - | + -- brokers - | | + -- coinmate.conf - | | + -- poloniex.conf - | + -- mmbot.conf - | + -- brokers.conf - | + -- traders.conf - + -- log - + -- data - + -- www - + -- index.html - + -- style.css - + -- code.js - + -- manifest.json -``` - - -## Konfigurace - -### Soubory ke konfiguraci - -Podrobnosti v nastavení hledejte v config_cs.md - -- **mmbot.conf** - obsahuje základni nastavení aplikace, které z pravidla není třeba měnit. -- **brokers.conf** - obsahuje nastavení broketů. Nastavení není třeba měnit, není třeba zavést dalšíhi brokera. Je vhodné znát jména broketů, které jsou v tomto configu deklarovány -- **traders.conf** - do tohoto konfiguračního souboru se vkládají definice jednotlivých obchodovaných párů a jejich nastavení -- **brokes/*.conf** - pro každého brokera je zde config, ve kterém je třeba doplnit API klíče na burzy. Stačí upravit jen ty, které se používají - -### Příprava na zavedení obchodovaného páru - -1. Pro každý pár je vhodné vyhradit 50% vkladu a za tento vklad nakoupit obchodované assety. Naptíklad, pokud obchoduju BTC/USD se vkladem 30000 USD při ceně 1 BTC za 10000USD, první krokem je nákup 1.5 BTC za 15000 USD. Tímto zůstane na účtu 15000 USD v BTC a 15000 USD v penězích. -2. Zjistěte označení assetu (třeba `BTC`), currency (třeba EUR) a páru (třeba `BTCEUR`). Tyto informace často nejsou k dispozici na UI burze, ale najdete je často v popisu API. Například na bitfinexu se pár označuje s písmenem t (`tBTCEUR`), na coinmate byt to bylo `BTC_EUR` - - -### Nastavení nového páru - -1. v configu **traders.conf** smažte ukázkový obsah a na začátek napište - -``` -[traders] -list= -``` - -Namísto si vložte vaši značku měnového páru. Například `btceur` +Robota v tuto chvíli není možné nastavit aby provozoval skalpovací strategie a/nebo obchodoval podle indikátorů. Jediné indikátory, které se používají jsou `SMA` (Simple Moving Average) a `STDEV` (standard deviation - obecně `Bollinger Bands`) a to pro výpočet šířky spreadu, přičemž SMA část lze zvolit jinou délku, než STDEV část (Standardní indikátor BB vyžaduje výpočet průměru za stejné období jako odchylky) + + +## Instalace robota -``` -[traders] -list=btceur -``` +* Robot se instaluje na počítač s operačním systémem Linux, odzkoušena je instalace na Ubuntu 18+. Nevylučuje to instalaci na jiný typ Linuxu, jen to není vyzkoušené +* Instalujte robota jako uživatel, **nikoliv jako root**. Provozujte robota též jako uživatel. Je vhodné pro robota vytvořit nového uživatele, aby byl oddělen od ostatních částí systému. Lze provozovat víc instancí robota, každého jako jiný uživatel +* Pro pohodlné ovládání budete potřebovat webserver, například **nginx**. Robota lze sice pustit a ovládat bez webserveru, ale neobsahuje žádné zabezpečení, takže tento způsob považuju za pouze dočasné řešení +### postup + +1. použijte `git clone https://github.com/ondra-novak/mmbot.git` (v současném stavu přidejte `-b 2.0`, ve větvi 2.0) +2. přejděte do adresáře `mmbot` +3. spusťte `./update` -2. doplnte sekci s vašim označení +Dosta pravděpodobně instalace selže, protože na vašem systému nejsou nainstalované důležité baliky. Získejte následující balíky ``` -[traders] -list=btceur - -[btceur] -broker= -pair_symbol= +cmake make g++ git libcurl4-openssl-dev libssl-dev libcurlpp-dev ``` -* **broker** - napište jméno brokera. Seznam brokerů najdete v **brookers** -* **pair_symbol** - vložte značku obchodovaného páru (např. `BTC_EUR`) - -**za poslední položkou musí být prázný řádek** - -## API klíče - -1. Získetjte API klíče na burze -2. Otevřte konfigurační soubor brokera -3. Vyplňte prázdné položky hodnotami získané z burzy - -## Více párů +Jakmile instalace projde, najdete aplikaci ve složce bin/mmbot -Pokud chcete obchodovat více párů, pak postupujte stejně jako u jednoho páru. Do položky `list` lze vložit více značek oddělené mezerou. +## První spuštění -Dobře si propočítejte finanční rezervu - - -## Záběh robota - -Před spuštěním robota s novým párem je dobré aby robot po nějakou dobu pouze sbíral data. Do konfiguračního souboru k danému páru lze napsat následující řádek +Ve výchozím stavu není webové rozhraní povoleno pro externí port. Pokud se připojujete na vzdálený server, kam robota instalujete, přidejte si do připojení tunel (ssh tunel). Příkazová řádka ssh může vypadat takto ``` -dry_run=1 +$ ssh -L10000:localhost:10000 ``` -tento řádek způsobí, že robot nebude posílat pokyny na burzu. Je vhodne pro nově přidaný pár po nějakou dobu (minimálně hodinu) nechat robota puštěného v tomto režimu. Robot během této doby sbírá data z trhu a ukládá je do souboru v adresáři `data`. Tyto data posléze používá k výpočtu nutných hodnot - -**Poznámka**: V tomto režimu pokyny končí v emulátoru. Robot navenek vykazuje činnost jako by reálně obchodoval. Zapsané obchody se na burze fyzicky nerealizují. Pokud je robot přepnut do ostrého režimu, všechny testovací obchody jsou vymazány a je provedena synchornizace obchodů s burzou. - - -**Poznámka:** tento krok lze vynechat, avšak je potřeba počítat s tím, že během prvních pár hodin nemusí robot obchodovat optimálně. Neměl by však generovat ztrátu. Může se stát, že robot nebude obchodovat vůbec (například bude držet vysoký spread), nebo naopak bude generovat velmi mnoho titěrných obchodů. I tak by každý tento obchod měl pokrývat poplatky na burze, čili samotné obchody by neměly generovat ztrátu - -Stejná situace nastane, pokud promažete adresář `data` - -## Spuštění robota - -### Spuštění robota na zkoušku - -Robota lze spustit tak, aby vypisoval co dělá, ale neprováděl žádnou obchodní činnost - -V adresáři kde je robot nainstalován: +Poté přejděte do složky robota (do té výchozí, nikoliv do bin) a napíšte ``` -$ bin/mmbot -vdt run +$ bin/mmbot -p 10000 start ``` -- `-vdt` je kombinace tří přepínačů - - `v` způsobí, že činnost se bude vypisovat do konzole - - `d` způsobí, že robot bude vypisovat veškerou činnost (debug) - - `t` způsobí, že žádný pokyn nedorazí na burzu (globalní dry_run) - -Činnost robota lze ukončit stiskem Ctrl+C +Pokud nenastala chyba, příkaz nic nevypíše. Pokud napíše, že port 10000 je obsazen, zkuste celý postup opakovat s jiným číslem (např o 1 vyšším). +Po spuštění robota bude k dispozici na vašem počítači webová stránka http://localhost:10000/ .Přes tuto stránku se dostanete do administrace kliknutím na ozubené kolečko +## Počáteční nastavení -### Ostré spuštění robota jako služba +1. Administrace je přístupná do okamžiku, než si založíte login. Není možné nic jiného nastavit, + dokud není založen aspoň jeden administrátor. Nastavení účtu proveďte v sekci `Access control` + +2. Ve stejné sekci (`Access control`) nastavte i API klíče k jednotlivým směnárnám - samozřejme +jen těm, které budete používat. Nastavení klíčů se uplatní po uložení přes tlačítko Save -``` -$ bin/mmbot start -``` +**Poznámka**: Nastavení kllčů lze upravovat jen do prvního znovunačtení stránky. Pak už je +nelze zpět vyvolat a jediná možná akce je smazání klíču. Teprve po smazání klíčů lze nastavit nové -V režimu služby se nic na konzoli nezobrazí. Pokud chcete zkontrolovat, že robot běží, napište do konzoli +## Přidání obchodovaných párů -``` -$ bin/mmbot status -``` +Tlačítkem + lze přidat obchodované páry. Obchodování se spustí po uložení, pokud pár má zaškrtnutý "Enable" a vyškrtnutý "Dry run". Tyto volba jsou v tomto stavu od začátku jako forma pojistky. -### Ukončení robota spuštěněho jako služba +## Zastavení robota ``` $ bin/mmbot stop ``` -**Poznámka:** Zastavení robota nezpůsobí smazání umístěných pokynů na burzách. Pokud je robot znovy spuštěn, pokračuje v činnosti tam kde přestal. - -### Restart robota +## Restart robota po nastavení ``` $ bin/mmbot restart ``` +Pokud je robot spuštěn bez parametru `-p`, není otevřen port pro přístup do nastavení a statistik. +Přístup je ale možný trvale přes webserver a unixový socket, který se doporučuje spíš než otevřený port -Restart robota je třeba provést **při každé změně konfigurace**. Bez restartu se změny konfigurace neuplatní. -## Správné nastavení robota +## Propojení s webserverem -[Podrobné nastavení](config_cs.md) - -Robot provádí nákup a prodej v relativně malém pásu kolem aktuální ceny. Vydělává tak tím, že poskytuje likviditu obchodníkům, kteří potřebují rychle nakoupit nebo prodat. Dlouhodobě se totiž ukazuje, že cena se často točí kolem nějaké hodnoty, dokud nenajde rovnováhu, Jakmile je rovnáha nalezena, může se záhy cena posunou jinam a zase hledá rovnováhu. Cílem obchodování malých rozdílu přitom je, aby ztráta pramenící z velké změny ceny byla vyrovnána obchodováním při pohybu do boku. - -Nastavení se liší podle sledovaného cíle. Robot může sloužit jako doplněk k HODLu, kdy není dopředu známo, odkud kam se cena může pohybovat, a nebo může být agresivnější a sázet na to, že cena se nijak zásadně měnit nebude a pokud ano, tak málo, a zisky se nadělají při pohybu do boku. - -V základním nastavení robot vždy obchoduje procentní rozdíl spreadu vůči aktuálnímu zůstatku (s připočtením `external_assets`) - -Velikost pokynu se počítá na základě ceny posledního obchodu `Tp` a cenu pokynu `Op` a aktuální balance assetů `A` +Při běhu robota je k dispozici unixový socket, který naleznete v adresáři `run`: ``` - ________ -S = A × (√ Tp / Op - 1) - - +run/mmbot.socket ``` +(složka je relativně k instalační složce) -Pokud se Bitcoin obchoduje na ceně 200000 Kč a spočítaný spread je 1700 Kč, pak prodejní příkaz bude na ceně `201700 Kč` o velikosti - -``` -Ss = -0,004223 × A -``` +### Připojení na nginx -Nákupní příkaz bude na ceně `198300 Kč`o velikosti +* Prostudujte si nastavení `proxy_pass` +* Nastavení lze provést přidáním do configu nginxu do sekce `server { }` ``` -Sb = 0,0042773 × A +server { + ... + ... + + location / { + proxy_pass http://unix://run/mmbot.socket:/; + } +} ``` +Kde `` doplňe absolutní cestu na složku s mmbotem. V zásadě je to o tom, že unix: napíšete absolutní cestu na onen `mmbot.socket` -Pokud dosadíte za **A = 1 BTC**, pak nakupovat budeme `0.0043 BTC`, což představuje objem `852,69Kč`, prodávat se bude `0.0042 BTC` o objemu `847,14Kč`. Cenový rozdíl zde dělá `3,55Kč`. Pokud by cena bitcoinu poklesla o polovinu (na 100000kč), musel bych předtím udělat zhruba **28tis obchodů** abych ztrátu dohnal. - -V tomto nastavení se nezdá, že by robot byl schopen vytvořit výrazný profit, který by ochránil investici před pohyby. Na druhou stranu, toto nastavení lze provozovat do nekonečna, protože vzoreček nikdy nevyjde pro prodej větší než A a díky polovině dokonce nikdy nebude větší než polovina A. Stejně tak lze dokázat, že nákupy nebudou dohromady nikdy stát víc, než objem odpovadjící počátečnímu A při počáteční ceně... - -Takže postačí mít na účtu například 0.1 BTC a 20000 Kč při ceně 200000Kč za BTC a obchodovat je možné do nekonečna. - -### Zvětšení objemu obchodů +Po restartu nginxu by měl být robot dostupný na zvolené adrese/doméně/cestě -Robot může obchodovat větší částky. Toho lze docílít tak, že robota přesvědčíme, že bokem mimo burzu disponujeme dalšímy penězi a assety. K tomu slouží v konfiguračním souboru položka `external_assets`. Do této položky napíšeme číslo o které navýšíme ono `A` ve vzorečku. Tím lze dosáhnout většího objemu. Má to však nevýhodu. -Pokud navýšíme počet assety držené mimo burzu, robot nyní může všechny assety vyprodat, nebo mu mohou dojít peníze na nákup. Protože vzoreček počítá s penězi mimo burzu. +### Zařídit si https -Přesto lze obchodovat i bez toho aby peníze fyzicky existovaly, pokud se cena bude držet v nějakém bezpečném pásmu +1. Potřebujete doménu +2. do sekce `server` vložte `server_name ;` +3. ujistěte se, že robot je dostupný přes http protokol na zadné doméně. +4. nainstaluje `python-certbot-nginx` a spusťte `certbox --nginx` jako root a postupujte podle instrukc9 -### Odhad bezpečného pásma +## Spouštění robota při restartu počítače -Robot disponuje funkcí odhadu bezpečného cenového pásma. - -Pokud máme robota delší čas v provozu například v režimu **na zkoušku**, lze z příkazové řádky napsat následující příkaz +K tomuto doporučuji jednoduchý trik: V rámci uživatele, který spouští robota zadejte ``` -$ bin/mmbot calc_range +$ crontab -e ``` -Robot vypíše pro každý měnový pár informaci o maximální a minimální ceně na základě stavu balance na burze +V následujícím editor přidejte na konec ``` -Trader LTC/CZK: - Assets: 28.7953 LTC - Assets value: 73668.4 CZK - Available assets: 8.79527 LTC - Available money: 77392.6 CZK - Min price: 0.030036 CZK - Max price: 4829.33 CZK +@reboot /cela/cesta/na/bin/mmbot start ``` -Příklad ukazuje: -- **Assets** - robot vidí následující množství coinů (LTC) -- **Assets value** - spočítáná hodnota assetů vůči aktuální ceně -- **Available assets** - skutečné množství assetů k dispozici na burze (tady je vidět, že `external_assets` je nastaven na 20) -- **Available money** - množství peněz na burze k nákupu -- **Min price** - minimální možná cena. Je vidět, že na burze je dostatek prostředku na pád Litecoinu až do oblasti halířů -- **Max price** - maximální možná cena. Je vidět, že při ceně 4829.33 Kč za 1 LTC robotovi dojdou LTC a nebude mít co prodávat - - Je třeba počítat s tím, že se jedna o odhad a to ještě za předpokladu, že by cena šla pouze jedním směrem bez korekcí a tak pomalu, aby třeba na situaci nezareagoval dynamický multiplikátor, který toto pásmo může ještě rozšířit díky efektivnějším nákupům. Určitou představu o možnostech spekulace výpočet je schopen poskytnout. - -### Nastavení optimálního parametru external_assets - -Doporučuje se zvyšovat objemy pouze pomocí tohoto nastavení. Ideální postup je nastavit nějaké číslo a nechat si to spočítat. Nastavit další číslo a opět si to nechat spočítat a takto postupovat až je nastavení vyhovující. +(samozřejmě se správnou cestou) -Volání calc_range se doporučuje pouštět až po nějaké době, kdy robot běží na zkoušku a sbírá data. Nejdříve tak po hodině ale čím déle tím lépe. Jde o to, aby robot měl dost údajů k výpočtu spreadu. -Opakované spouštění funkce **calc_range** může vracet různě odlišné výsledky v závislosti na aktuální spreadu. Je vždy dobré porovnat výsledky v různých časech a případně hodnotu doupravit +Soubor uložte a měl by se aktivovat automatický strart -### Další možnosti nastavení objemu - -**buy_mult**, **sell_mult** - přímo násobí vypočtenou hodnou multiplikátorem. - -**buy_step_mult**, **buy_step_mult** - násobí spočtený spread. Čím širší spread, tím větší objem (ale tím nižší šance na zásah) - -**dynmult_raise**, **dynmult_fall** - ve výchozím stavu je tato funkce zapnutá. Umožňuje reagovat na rychlý pohyb ceny (pumpu nebo dumpu) tím, že zvyšuje spread při každém zásahu o nastavenou hodnotu **dynmult_raise** a případně snižuje **dynmult_fall** v době klidu. Vyšší spread znamená vyšší objem a i vyšší zisk z jedné otočky. Pokud cena prudce akceleruje, robot umísťuje pokyny dál od ceny a tím zvyšuje spread a tedy objem. - -# Výsledky a monitoring - -## Kde sledovat výsledky? - -Grafický přehled je k dispozici prostřednictvím webové stránky, kterou je možné prohlížet pomocí prohlížeče, například `chrome`. Stránku je nutné zpřístupnit pomocí webserveru, například pomocí `Nginx` nebo `Apache`. Nouzově pro osobní použití lze aktivovat robotův malý ad-hoc http server pomocí volby [http_bind](config_cs.md#report). Tato možnost se ale nedoporučuje pro prohlížení výsledků přes internet nebo jakoukoliv nezabezpečenou síti. - -Webový server stačí nasměrovat do složky `www`, kde se nachází celá prezentace a kam robot ukládá soubory s reportem. - -## Sekce a grafy - -### Summary - -Představuje přehled všech obchodovaných párů a základních měn. Každý řádek obsahuje tyto údaje - -``` -NÁZEV PÁRU pos - -24h: -``` - -**Nákupní příkaz** se zobrazuje zelenou barvou a obsahuje nákupní cenu a pod tím menším písmem množství. **Prodejní příkaz** je červenou barvou a je zde uvedena prodejní cena a množství. Poslední cena (modře) by se měla pohybovat mezi těmito cenami. Pokud se cena posune přes prodejní nebo nákupní příkaz, dojde k obchodu. - -Seznam posledních obchodů se zobrazuje pod přehledem. Seznam se aktualizuje automaticky každou minutu - -#### Výsledky za 24h - -- **Relativní změna ceny** -- **Počet obchodů** (t) -- **Celkový objem** (vol) -- **Zmena pozice** (pos) - kladná čísla nákupy, záporná prodeje -- **Průměrná nákupka** (avg) -- **Zisk nebo ztráta** (p/l) -- **Přírustek normovaného zisku** (norm) viz níže - - - -### Grafy - -Grafy lze zobrazovat buď jeden graf za každý pár, nebo pro určitý pár všechny grafy - -- **P/L from positions** - Zobrazuje zisk nebo ztrátu z držením pozice. Tento ukazatel je vhodný, pokud robot záporné pozice opravdu shortuje (nedoporučuje se), protože při záporné pozici započítává zisk, když cena klesá. Pokud jde o exchange s rozdělením 50:50, tak pří záporné pozici a růstu ceny přesto účet jako celek získává profit, ale o onu uváděnou ztrátu menší, než by získával kdyby neobchodoval. Adekvátně při poklesu cen může kladná pozice mírnit celkovou ztrátu a tím se zobrazovat jak zisk - -- **Normalized profit** - Každý obchod je exekuován s tím, že vždy vzniká malý profit při použití jiné perspektivy pohledu na celého robota. Vic informací dále. Graf lépe vystihuje profit robota při dlouhodobějším obchodování - -- **Trades** - grafovaný průběh obchodů a jejich objemů. Červené tečky jsou prodeje, zelené nákupy - -- **Position** - graf vývoje pozice - -- **Price** - záznam vývoje ceny v místech, kde se obchodovalo. Lze na tom vidět, kde robot nakupoval a kde prodával (není vidět množství, to je vidět na **Trades** - -- **Total P/L** a **Total Normalized** - sloučený vývoj zisků a ztrát přes všechn páry vztažené na jejich základní měnu. Pokud je víc základních měn, je zde více grafů, pro každou měnu. - - -## Normalizovaný zisk a normalizované akumulované assety - -K pochopení normalizovaného zisku je třeba se vrátit k výše uvedenému vzorci. - -``` - _________ -S = A × ( √ Tp / Op - 1) -``` - -Tento vzorce lze upravit tak, aby vracel počet assetů na zadané ceně -``` - ______ -B = A × √ P / N -``` - -Kde `A` je počet asetů na ceně `P` a `N` je nová cena na které vychází počet assetů `B`. Zajímavé na vzorci je také to, že je reflexivní a tranzitivní. Tedy, že též platí: - - -``` - _______ -A = B × √ N / P -``` -(důkaz, dosaďte A horního vzorce a dostane B=B) - -Jinými slovy, když nastavím, `B` a `N` jako nový výchozí bod, mohu se zadáním `P` vrátit k původnímu `A` - -Tranzitivita se projevuje tak, že si mohu vybrat libovolnou cenu X. Z této ceny a z vypočtených assetů Y přejít na cenu N a získat původní B - -``` - _______ -Y = A × √ P / X - _______ -B = Y × √ X / N -``` -(důkaz opět dosazením, X se vykrátí , zůstane P/N) - -Tato vlastost v praxi znamená, že si stačí pamatovat výchozí nastavení `A` a `P`, a mohu po zadání libovolné ceny `N` spočítat, kolik mám mít assetů na účtu. Robot jednoduše k zadané ceně spočítá požadované množství assetů a vydá pokyn na burzu, aby se tak stalo. - -Pro pochopení důsledku tohoto vzorce je třeba si ještě určit, jak bude vypadat vývoj `currency` na účtu, tedy to, čím se platí. Pro robota se doporučuje, aby na účtu bylo připraveno vždy stejné množství currency jako odpovídá hodnotě assetů. Hodnota držených assetů `C` na ceně `N` je: - -``` - _______ _______ -C = B × N = A × √ P / N × N = A × √ P × N -``` + +## Strategie -Stejnou hodnotu musím držet v `currency` +### Linear -Například, pokud Bitcoin stojí 200000 Kč a mám na účtu právě 1 BTC, pak mám na účtu také 200000Kč v penězích a dohromady má moje portfolio 400000kč. Pokud cena BTC klesne na 180000Kč, pak budu mít na účtu +Nejjednodušší strategie, která nejlépe funguje tam, kde očekáváme vyšší zisky,vyšší riziko a +případně s marginem, futures, tedy tam, kde lze mít long nebo short pozici. Lze +ji provozovat i na klasických exchange kde není short s tím, že se short se vytvoří posunutím +neutrální pozice - tedy že určité množství assetů na účtu se prohlásí za nulový stav a short se pak realizuje odprojeme těchto assetů nad toto množství -``` - _____________ -B = 1 × √200000/180000 = 1,0541 BTC - _____________ -C = 1 × √200000×180000 = 189736,66 Kč -``` +Startegie odvozuje velikost pokynu přímo úměrné vzdálenosti od posledního obchodu (případně od equilibria) -Pokud ale budu provádět nákup na ceně 180000kč, pak mi na účtu zůstane +Lze nastavit + * **Velikost přírustku pozice při změně ceny o 1 %** - Toto určuje rizikovost strategie. Čím vyšší číslo, tím větší objemy, ale tím rychleji může dojít k liquidation. + * **Neutrální pozici** - specifikuje jaké množství assetů je neutrální pozice. Tam kde lze shortovat nechte nulu + * **Maximální pozice** - Definuje při jaké pozici doje přepnutí strategie do režimu redukce. I v tomti režimu může docházet k navyšování pozice, ale ne tak razantně. Robot redukuje pozici kdykoliv + se objeví opačný pohyb a dojde k exekuci příslušného pokynu. Jakmile je pozice redukována pod + tuto hranici, přepne se strategie zpět do normálního režimu. Jakékoliv razantní redukování pozice může znamenat ztrátu. Avšak pořád ta ztráta je menší, než stoploss, který se obecně nedoporučuje používat + * **Akumulace** - Má význam pro směnárny, kde nakoupení assety jsou ve vašem vlastnictví. Pokud je + akumulace nastavena na 1, pak zisk z obchodování je použit k navýšení assetů. Na margin a futures směnárnách by to vedlo k postupnému zvyšování longu. + +### Keep value -``` -C' = 200000 - (0.0541 × 180000) = 190262 Kč -``` +Tato strategie si klade za cíl udržet hodnotu assetů na počáteční hodnotě ať se cena pohne +jakýmkoliv směrem. Pokud hodnota klesne, dokoupí, pokud hodnota stoupne, prodá. Vyrovnání +vždy dochází ve skocích definované spreadem. -Rozdíl mezi `C` a `C'` se nazývá **normalizovaný zisk** +* **external assets** - lze nastavit kladné číslo určující kolik assetů držíte mimo burzu. Případně záporné číslo představující, kolik assetů se nepočítá do hodnoty -``` -Cn = C' - C = 525,34 Kč -``` +* **accumulation** - zisk z obchodování se použije k nákupu assetů. Tyto navíc assety se neúčastní systému udržení hodnoty -Tyto peníze už mohu z účtu odebrat, protože nikdy nebudou potřeba. Lze totiž využít reflexivity vzorce a prokázat, že **k dalším nákupům** za nižší ceny bude třeba **méně peněz** a při návratu na původní cenu nám tyto peníze k získání přesné poloviny nebudou chybět +### Half half -``` - _____________ -B2 = 1.0541 × √180000/200000 = 1 BTC - _____________ -C2 = 1.0541 × √180000×200000 ~= 200000 Kč +Tato strategie se snaží, aby poměr mezi hodnotou assetů a penězi na účtu ve směnárně byl v rovnováze. Kdykoliv je rovnováha narušena, robot rovnováhu nastolí obchodem. Tato strategie je určena pro klasické směnárny. Její výhodou je, že je spočítána přesně tak, aby robotovi nikdy nedošly ani assety ani peníze a to na jakékoliv ceně. Nevýhodou je, že zisky z tohoto obchodování jsou relativně nízké a vyžaduje volatilní trhy -C2' = 189736,66 - (-0.0541 × 200000) = 200556,66 Kč -``` + +* **external assets** - lze nastavit kladné číslo určující kolik assetů držíte mimo burzu. -A opět, rozdíl mezi `C2` a `C2'` se nazývá **normalizovaný zisk** +* **accumulation** - zisk z obchodování se použije k nákupu assetů. Tyto navíc assety se následně započítávají do rovnováhy. -``` -C2n = C2' - C2 = 556,66 Kč -``` -Idea normalizovaného zisku je tedy založena na tom, že mám na burze vyhrazené peníze přesně v poměru 50:50 a pouze přelévám prostředky z jedné strany na druhou podle vývoje aktuální ceny. Vůbec přitom není třeba hlídat aktuální hodnotu celého portfolia, protože ta z dlouhodobého hlediska není důležitá. Důležitější je umět správně změřit **normalizovaný zisk**, ten má totiž schopnost generovat **trvalý příjem** bez ohledu na to, co se děje na burze. Důležité samozřejmě je, aby se cena na burze hýbala, a aby se obchodovalo. Nezáleží kam, ale kolik. Z každého obchodu je pak **normalizovaný** zisk kladný. + -Robot umožňuje zapnout funkci, kdy část normalizovaného zisku se akumuluje do assetů. V takovém případě může i tento zisk být ovlivněn cenou vlastního assetu. Toto nastavení je třeba si dobře rozmyslet a určit si správně cíle. U potenciálně rostoucího assetu se vyplatí například polovinu zisku akumulovat. U potenciálně klesajícího assetu spíš neakumulovat. -Množství akumulovaných assetů navíc se pak říká **normalizované akumulované assety**. Robot je ovšem začlení do celkové balance `A` a tak se jejich akumulace projeví v drobném zesílení budoucích zisků. + diff --git a/doc/proxy.md b/doc/proxy.md index 33b6e90e..009df7d9 100644 --- a/doc/proxy.md +++ b/doc/proxy.md @@ -5,7 +5,7 @@ linux process and comunnicates using pipes. The Robot starts this broker once it is needed and keeps it running, until the robot is stopped. The connection is made using three pipes connected to **stdin**, **stdout** and **stderr**. So the broker reads its **stdin**, and replies to **stdout**. It can also log its actions to **stderr**, which is routed to the robot's logfile -The communication is synchronous: request-reply style. The broker must only send one response to each request. The broker can also use stderr only if the response is expected (note: the whole line need to be written to the stderr, including "\n", otherwise, the communication can deadlock and eventually timeour) +The communication is synchronous: request-reply style. The broker must only send one response to each request. The broker can also use stderr only if the response is expected (note: the whole line need to be written to the stderr, including "\n", otherwise, the communication can deadlock and eventually timeout) Every request is send on single line terminated by a new line character '\n' The response should be send as a single line too. @@ -54,7 +54,7 @@ If "placeOrder" fails, the order is considered as "not placed". Request to reset internal state of the broker. It is ideal place to clear any cache or perform garbage collecting. The function is called at the begining of every cycle. -#### Response +** Response ** ``` [ true ] ``` @@ -67,12 +67,12 @@ Enables or disables debug mode. This is optional feature. When debug mode is on, should send debug informations to the standard error. When debug mode is off, the broker should stay quiet. -#### Response when supported +** Response when supported ** ``` [true] ``` -#### Response when not supported +** Response when not supported ** ``` [false] @@ -112,27 +112,30 @@ Returns ticker for given pair #### TIP: To reduce count of requests, the broker can read multiple symbols and server them from cache until the "reset" -### getTrades +### syncTrades ``` -["getTrades", {"lastId": , "fromTime": , "pair": } ] +["syncTrades", {"lastId": , "pair": } ] ``` -Returns trades associated with user account. -* **lastId** - (optional) contains **id** of last received trade. It allows to receive - trades created after that id. If not specified, the all trades are returned -* **fromTime** - (optional) returns only trades newer then specified time +Reads last trades. +* **lastId** - contains **lastId** returned by previous read. For the very first read this field is missing or it is set to **null** * **pair** - specifies pair ``` -[ true, { "id": , - "time": , - "size":, - "price":, - "eff_size":, - "eff_price":, - } ] +[ true, {"lastId":, + "trades:[{ + "id": , + "time": , + "size":, + "price":, + "eff_size":, + "eff_price":, + } ] + } +] ``` +* **lastId** - this value is remembered and used on next call. It can by any arbitrary JSON value. * **id** - id of trade (can be string or number) * **time** - timestamp * **size** - amount of assets. The number is positive for buy, or negative for sell, @@ -212,6 +215,10 @@ Returns information about a trading pair "min_size": , "fees": , "feeScheme", "currency|assets|income|outcome" + "leverage": , + "invert_price": + "inverted_symbol": + "simulator": }] ``` @@ -229,10 +236,20 @@ search the balance. Example: "XBT" - **assets** - fees are substracted from assets - **income** - fees are substracted from currency when sell, or assets when buy - **outcome** - fees are substracted from currency when buy, or assets when sell -- **leverage** - (optional) when pair can use leverage. This value should be greater or +- **leverage** - when pair can use leverage. This value should be greater or equal to zero. If zero is specified (default), there is no leverage. Otherwise leverage is available (for example 100 means leverage 100x) and it also enables `shorts`. +- **invert_price** - the value `true` specifies, that price is inverted. It is used when + the trading pair is inversed futures. The quoted price must be send as 1/price, the position + and the size of the order must be multiplied by -1 and this flag must be set to `true`. +- **inverted_symbol** - for inversed futures specifies symbol for price in which the futures are + quoted. For example Deribit BTC/USD is inversed futures. Assets is 'contract', currency is BTC, + and inverted symbol is USD. +- **simulator** - set this to `true`, if the pair is not actual trading for the real money, but just + kind of simulator, paper trading, etc. + + ### getFees @@ -261,6 +278,97 @@ Returns all available tradable pairs on stockmarket [ true, [,,,...] ] ``` +### getBrokerInfo +``` +["getBrokerInfo"] +``` + +Returns general informations about the broker + +``` +{ + "name":, + "url":, + "version":, + "licence":, + "trading_enabled":, + "settings":, + "favicon": +} +``` +* **name** - name of the exchange (Binane, Deribit, BitMEX, Coinmate, etc] +* **url** - full url to exchange's home page +* **version** - version of the broker +* **licence** - licence text +* **trading_enabled** - set `true`, if the trading is enabled. Trading is disabled because +the API key is not set. +* **settings** - set `true`, if the broker has extra settings (getSettings, setSettings) +* **favicon** - icon in base64, content-type: image/png + +### getApiKeyFields +``` +["getApiKeyFields"] +``` +Returns list of fields need to be filled by a user to import API key. Different exchanges has +different format of the key, and the different count of requied fields. + +Description of return value can be found at function `getSettings` The function must not return +stored keys. These values must be never returned to the user. + +### setApiKey +``` +["setApiKey", ] +``` +Stores all values need to initialize API key. Parameter is object which contains key-value set +of fields. Names and types of this fields can be obtained by function `getApiKeyFields` + + +###getSettings +``` +["getSettings","hint"] +``` + +Allows to user to change some internal settings of the broker. This function must be enabled +in the result of the function `getBrokerInfo`. This function returns list of fields with their values to be changed by user. The returned data are used to build a form, which user can fill or edit. + +* **hint** - contains currenctly selected pair. The broker can use this value as hint which +fields to serve. + +** Return value ** + +Returned value is an array of fields + +``` +[{ + "name",, + "label",, + "type","string|number|textarea|enum|hidden", + "default":, + "options": { + "value1":"label1", + "value2":"label2", + ... + } +},....] +``` +* **name** - name of the field +* **label** - label of the field +* **type** - one of specified type +* **default** - (optional) if set, the input control is pre-filled by this value +* **options** - just for `enum` - contains list of values with label. The result control is + a combobox where user can choose one of the values + +###setSettings +``` +["setSettings", ] +``` + +Stores settings edited by the user. The settings must be stored permanently in the secured area, +similar to location where the API key is stored. The settings must be also immediatelly applied. + + + + ### When function is not implemented This protocol can be extended anytime in future. All new functions that arn't implemented diff --git a/doc/webadmin_cs.md b/doc/webadmin_cs.md new file mode 100644 index 00000000..385cd1c8 --- /dev/null +++ b/doc/webadmin_cs.md @@ -0,0 +1,110 @@ +## Web Admin - instalace a přechod + +### Co se mění přechodem na "webadmin" verzi + +Od verze nazvané "webadmin" se mění způsob zadávání nastavení. Namísto ruční editace konfiguračních souborů je možné použít webové rozhraní, které je mnohem pohodlnější a nabízí rozšířené funkce realizované přímo v prohlížeči. K dispozici je i mobilní verze včetně možnosti instalace jako aplikace typu PWA podporované na telefonech Android (a omezeně i na iPhone prostřednictvím Safari) + +S přidáním konfigurace prostřednictvím webového rozhraní ale přichází několik změn, které bylo nutné provést, aby webové prostředí fungovalo + +- **traders.conf** je deprecated - robot umí traders.conf načíst, ale tato možnost je v nastavení vypnuta zakomentováním řádku s patřičným @include traders.conf. **Protože se neprovádí konverze nastavení, je případně nutné obsah traders.conf znovu zadat do webového rozhraní** + +- **http_bind je povinný** - v původní verzi bylo použití `http_bind` pouze volitelný doplňek a propojení s webserverem se realizovalo prostřednictvím mapování složky v na url webserveru. Avšak webadmin je dostupný pouze přes `http_bind`, proto musí být povolen a správně nastaven. Ve výchozím nastavení robot otevírá port `localhost:11223`. Tento port lze namapovat na webserver přes nastavení reverzní proxy + +- **konfigurace je jednodušší** - konfigurační soubory jsou teď mnohem kratší, spousta nastavení se děje přímo v prohlížeči. Po instalaci lze robota hned spustit a webové prostředí je bez dalšího nastavení ihned k dispozici + +- **nastavení API klíčů se děje přes konfigurační soubory** - tato část se nezměnila. Ve webovém prostředí není možné nastavovat API klíče. K tomu je třeba upravit patřičnou sekci v konfiguračním souboru brokera. Je to záměr i kvůli bezpečnosti, aby neexistovala žádná možnost najít způsob jak přes webové rozhraní ukrást tyto klíče. + +### Instalace + +Verzi `webadmin` lze instalovat pouze z větve `webadmin`. Doporučuje se instalovat do odděleného adresáře a nepřepisovat starou verzi. Po převedení nastavení traderů lze pak starého robota vypnout. + +(před instalací je třeba zajistit si přítomnost těchto balíků v systému: +`cmake make g++ git libcurl4-openssl-dev libssl-dev libcurlpp-dev`, Doporučuje se také pro robota vytvořit odděleného uživatele) + +``` +$ git clone -b webadmin https://github.com/ondra-novak/mmbot.git mmbot_webadmin +$ cd mmbot_webadmin +$ ./update +$ bin/mmbot start +``` + +### Otevření webového rozhraní + +Webové rozhraní je přístupné v prohlížeči na adrese [http://localhost:11223/](http://localhost:11223/) . Na hlavní stránce je (zpravidla z počátku prázdná) report stránka a administraci najdete pod ikonkou ozubeného kola (vpravo nahoře) + +Pokud instalujete robota vzdáleně přes `ssh`, pak se doporučuje aktivovat ssh tunel pomocí přepínače `-L11223:localhost:11223`. Tím lze prostředí pomocí stejného odkazu otevřít lokálně a bude fungovat tak dlouho, dokud je `ssh` aktivní + +Než však můžete přidávat páry z různých směnáren, je třeba do konfiguračních souborů nastavit API klíče. Webové rozhraní sice umožní vytvořit nastavení pro směnárnu, ke které nemáte API klíč, ale bude na tuto skutečnost upozorňovat pomocí červeného vykřičníku a některé funkce nebudou pracovat správně + +Konfigurační soubory klíčů najdete v `conf/brokers/` + +Po nastavení klíčů je třeba robota restartovat `bin/mmbot restart` + +### Propojení s webserverem + +Ačkoliv by bylo možné `http_bind` nasměrovat do internetu, z bezpečnostních důvodů se to nedoporučuje. Navíc `http_bind` nepodporuje **https**! Nastavení **https** je dobré zprovoznit na webserveru přes službu **let's encrypt** + +Webserver lze použít **nginx** nebo **apache**. + +Nastavení pro **nginx** - následující část vložíte do patřičné sekce `server { }` + +``` +location / { + proxy_pass http://localhost:11223/; +} +``` + +Lze rozhraní namapovat i na cestu úpravou `location /cesta {` + + +Nastavení pro **apache** + +``` + + ProxyPass "http://localhost:11223/" + +``` + +Více informací hledejte v návodu daného webserveru + +### Zabezpečení + +Webové prostředí (včetně report stránky) je otevřeno všem, dokud není definován první uživatel s právy administrátora. Bez nastavení administrátora není možné žádné nastavení uložit, proto to udělejte jako první. + +Zároveň lze zvolit, jestli report stránka bude veřejná, nebo chráněná heslem pomocí volby u uživatele `` - ten se sice nedá smazat, ale lze jej vypnout nastavením `no access` + +#### když zapomenu heslo? + +Existuje způsob, jak se dostat do nastavení i v případě ztráty hesla. Ten způsob vede přes `web_admin.conf` v sekci `[web_admin]`. V této sekci lze nastavit pevný login administrátora v položce `auth`. Způsob nastavení je uveden přímo v configu v komentářové části. Je potřeba tento klíč povolit smazáním # a správně nastavit. Po restartu robota lze nově zvolený login použít pro přihlášení a změnit nastavení zabezpečení + +### Jak pracovat s nastavením + +- **veškeré volby je třeba uložit** - Dokud není nastavení uloženo, změny v nastavení se nijak neprojeví. Týká se to i přidávání a mazání traderů. I v případě, že smažete tradera, přesto pořád obchoduje, dokud nastavení není uloženo. Teprve po uložení je smazán a všechny jeho pokyny jsou zrušeny. + +- **Některá akční tlačítka mají okamžitou platnost** - jde o funkce **reset** a **repair** a o funkci zrušení všech pokynů. + +- **Dočasné zastavení obchodování** - ať už jedotlivě, nebo pomocí funkce **Global Stop** - všechna dočasná zastavení se obnoví **uložením** nového nastavení (stačí jen klepnout na **Save**) + +- **Backtest vyžaduje data** - Funkce Backtestu je k dispozici vždy až po určité době, kdy robot běží. Lze backtestovat i na datech získané po čas běhu v režimu "dry run", ale pozor, jakmile "dry run" vypnete, všechny obchody vytvořené v tomto režimu se smažou + + +### Jak importovat stará nastavení? + +Obecně nelze importovat volby z `traders.conf`. Je třeba je prostě přepsat. Pokud jde o stavové soubory (obsahují statistiky a záznamy obchodů a data pro výpočet spreadu), ty lze naimportovat jednoduše. Nejprve překopírujte `data` ze staré verze do nové + +V okamžiku zakládání tradera, kterého přenášíte z `traders.conf` jednoduše vyplňte `trader's UID` stejné jako máte v konfiguračním souboru. Po uložení by měl robot být schopen tyto data načíst a pokračovat tam kde přestal. Pokud se stane, že se nebudou zaznamenávat nově provedené obchody, tak funkce `repair` by to měla opravit. Ale nepoužívejte to dokud to není potřeba. + +### Pomalá odezva + +Webové prostředí není optimalizováno na rychlou odezvu. Proto pomalá odezva prostředí je běžnou věcí a není třeba se tím trápit. Co je příčinou pomalé odezvy? + + 1. Webové prostředí běží pouze v jednom vláknu. Nepředpokládá se, že by prostředí používaly stovky uživatelů. a zpravidla se předpokládá, že celý robot pracuje na malé a výkonově slabé VPS. Proto se větší část CPU nechává pro výpočty robota a webové prostředí má nižší prioritu + + 2. V některých situacích webové rozhraní přímo zasahuje do výpočtů robota, i v takovém případě mají výpočty přednost a webové rozhraní musí počkat, až se vše bez narušení spočítá + + 3. Brokeři umí jednu operaci naráz. Pokud tedy brokera zrovna používá robot k ovládání trhu, musí webové prostředí počkat, až se broker uvolní. Někdy také data ze směnárny mohou mít zpoždění, protože brokeři je cacheují aby se redukoval počet requestů za sekundu. U některých směnáren bývá kriticky nízký limit. + + 4. Websocket není a nebude podporován. + + + diff --git a/src/binance/CMakeLists.txt b/src/binance/CMakeLists.txt index bad31adc..102a2ceb 100644 --- a/src/binance/CMakeLists.txt +++ b/src/binance/CMakeLists.txt @@ -4,6 +4,6 @@ add_compile_options(-std=c++17) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin/brokers/) -add_executable (binance main.cpp config.cpp proxy.cpp ../brokers/api.cpp ../brokers/orderdatadb.cpp ) +add_executable (binance main.cpp proxy.cpp ../brokers/api.cpp ) target_link_libraries (binance LINK_PUBLIC imtjson curlpp ssl crypto curl stdc++fs pthread) install(TARGETS binance DESTINATION "bin/brokers") diff --git a/src/binance/config.cpp b/src/binance/config.cpp deleted file mode 100644 index 2f43a2cf..00000000 --- a/src/binance/config.cpp +++ /dev/null @@ -1,21 +0,0 @@ -/* - * config.cpp - * - * Created on: 21. 5. 2019 - * Author: ondra - */ - - - -#include "config.h" - -Config load(const ondra_shared::IniConfig::Section& cfg) { - Config r; - - r.privKey = cfg.mandatory["secret"].getString(); - r.pubKey = cfg.mandatory["key"].getString(); - r.apiUrl = cfg.mandatory["url"].getString(); - return r; -} - - diff --git a/src/binance/config.h b/src/binance/config.h deleted file mode 100644 index 7e47039e..00000000 --- a/src/binance/config.h +++ /dev/null @@ -1,23 +0,0 @@ -/* - * config.h - * - * Created on: 21. 5. 2019 - * Author: ondra - */ - -#ifndef SRC_COINMATE_CONFIG_H_ -#define SRC_COINMATE_CONFIG_H_ -#include "../shared/ini_config.h" - - - -struct Config { - std::string apiUrl; - std::string privKey; - std::string pubKey; - - }; - -Config load(const ondra_shared::IniConfig::Section &cfg); - -#endif /* SRC_COINMATE_CONFIG_H_ */ diff --git a/src/binance/main.cpp b/src/binance/main.cpp index 7a3b8588..3badf65b 100644 --- a/src/binance/main.cpp +++ b/src/binance/main.cpp @@ -11,7 +11,6 @@ #include #include #include "shared/toString.h" -#include "config.h" #include "proxy.h" #include "../main/istockapi.h" #include @@ -21,7 +20,6 @@ #include #include "../shared/linear_map.h" #include "../shared/iterator_stream.h" -#include "../brokers/orderdatadb.h" #include #include #include @@ -29,15 +27,25 @@ using namespace json; +static Value keyFormat = {Object + ("name","pubKey") + ("type","string") + ("label","Public key"), + Object + ("name","privKey") + ("type","string") + ("label","Private key")}; + class Interface: public AbstractBrokerAPI { public: - Proxy &px; + Proxy px; + - Interface(Proxy &cm):px(cm) {} + Interface(const std::string &path):AbstractBrokerAPI(path, keyFormat) {} virtual double getBalance(const std::string_view & symb) override; - virtual TradeHistory getTrades(json::Value lastId, std::uintptr_t fromTime, const std::string_view & pair) override; + virtual TradesSync syncTrades(json::Value lastId, const std::string_view & pair) override; virtual Orders getOpenOrders(const std::string_view & par)override; virtual Ticker getTicker(const std::string_view & piar)override; virtual json::Value placeOrder(const std::string_view & pair, @@ -51,6 +59,9 @@ class Interface: public AbstractBrokerAPI { virtual double getFees(const std::string_view &pair)override; virtual std::vector getAllPairs()override; virtual void enable_debug(bool enable) override; + virtual BrokerInfo getBrokerInfo() override; + virtual void onLoadApiKey(json::Value keyData) override; + virtual void onInit() override; using Symbols = ondra_shared::linear_map > ; using Tickers = ondra_shared::linear_map >; @@ -68,18 +79,15 @@ class Interface: public AbstractBrokerAPI { bool needSyncTrades = true; std::size_t lastFromTime = -1; - void syncTrades(std::size_t fromTime); - bool syncTradesCycle(std::size_t fromTime); - bool syncTradeCheckTime(const std::vector &cont, std::size_t time, Value tradeID); static bool tradeOrder(const Trade &a, const Trade &b); void updateBalCache(); - void init(); Value generateOrderId(Value clientId); std::intptr_t time_diff; std::uintptr_t idsrc; + void initSymbols(); }; @@ -122,53 +130,71 @@ void Interface::updateBalCache() { } */ - Interface::TradeHistory Interface::getTrades(json::Value lastId, std::uintptr_t fromTime, const std::string_view & pair) { + Interface::TradesSync Interface::syncTrades(json::Value lastId, const std::string_view & pair) { + initSymbols(); auto iter = symbols.find(pair); if (iter == symbols.end()) throw std::runtime_error("No such symbol"); const MarketInfo &minfo = iter->second; + if (lastId.hasValue()) { + + + Value r = px.private_request(Proxy::GET,"/api/v3/myTrades", Object + ("fromId", lastId) + ("symbol", pair) + ); + + r = r.map([&](Value x) ->Value{ + if (x["id"] == lastId) return json::undefined; + else return x; + }); + + TradeHistory h(mapJSON(r,[&](Value x){ + double size = x["qty"].getNumber(); + double price = x["price"].getNumber(); + StrViewA comass = x["commissionAsset"].getString(); + if (!x["isBuyer"].getBool()) size = -size; + double comms = x["commission"].getNumber(); + double eff_size = size; + double eff_price = price; + if (comass == StrViewA(minfo.asset_symbol)) { + eff_size -= comms; + } else if (comass == StrViewA(minfo.currency_symbol)) { + eff_price += comms/size; + } - Value r = px.private_request(Proxy::GET,"/api/v3/myTrades", Object - ("fromId", lastId) - ("startTime", fromTime) - ("symbol", pair) - ); - - r = r.map([&](Value x) ->Value{ - if (x["id"] == lastId) return json::undefined; - else return x; - }); - - TradeHistory h(mapJSON(r,[&](Value x){ - double size = x["qty"].getNumber(); - double price = x["price"].getNumber(); - StrViewA comass = x["commissionAsset"].getString(); - if (!x["isBuyer"].getBool()) size = -size; - double comms = x["commission"].getNumber(); - double eff_size = size; - double eff_price = price; - if (comass == StrViewA(minfo.asset_symbol)) { - eff_size -= comms; - } else if (comass == StrViewA(minfo.currency_symbol)) { - eff_price += comms/size; - } - - return Trade { - x["id"], - x["time"].getUInt(), - size, - price, - eff_size, - eff_price + return Trade { + x["id"], + x["time"].getUIntLong(), + size, + price, + eff_size, + eff_price + }; + }, TradeHistory())); + + std::sort(h.begin(), h.end(),[&](const Trade &a, const Trade &b) { + return Value::compare(a.id,b.id) < 0; + }); + if (!h.empty()) lastId = h.back().id; + return TradesSync{ + h, + lastId }; - }, TradeHistory())); - - std::sort(h.begin(), h.end(),[&](const Trade &a, const Trade &b) { - return Value::compare(a.id,b.id) < 0; - }); - return h; + } else { + Value r = px.private_request(Proxy::GET,"/api/v3/myTrades", Object + ("symbol", pair)); + Value id = r.reduce([](Value l, Value itm) { + Value id = itm["id"]; + return id.getUInt() > l.getUInt()?id:l; + }, Value(0)); + return TradesSync { + {}, + id + }; + } } @@ -205,7 +231,7 @@ Interface::Orders Interface::getOpenOrders(const std::string_view & pair) { }, Orders()); } -static std::uintptr_t now() { +static std::uint64_t now() { return std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ).count(); @@ -251,6 +277,7 @@ Interface::Ticker Interface::getTicker(const std::string_view &pair) { } std::vector Interface::getAllPairs() { + initSymbols(); std::vector res; for (auto &&v: symbols) res.push_back(v.first); return res; @@ -308,45 +335,50 @@ bool Interface::reset() { return true; } -void Interface::init() { - Value res = px.public_request("/api/v1/exchangeInfo",Value()); - - std::uintptr_t srvtm = res["serverTime"].getUInt(); - std::uintptr_t localtm = now(); - time_diff = srvtm - localtm; - - using VT = Symbols::value_type; - std::vector bld; - for (Value smb: res["symbols"]) { - MarketInfo nfo; - nfo.asset_symbol = smb["baseAsset"].getString(); - nfo.currency_symbol = smb["quoteAsset"].getString(); - nfo.currency_step = std::pow(10,-smb["quotePrecision"].getNumber()); - nfo.asset_step = std::pow(10,-smb["baseAssetPrecision"].getNumber()); - nfo.feeScheme = income; - nfo.min_size = 0; - nfo.min_volume = 0; - nfo.fees = 0; - for (Value f: smb["filters"]) { - auto ft = f["filterType"].getString(); - if (ft == "LOT_SIZE") { - nfo.min_size = f["minQty"].getNumber(); - nfo.asset_step = f["stepSize"].getNumber(); - } else if (ft == "PRICE_FILTER") { - nfo.currency_step = f["tickSize"].getNumber(); - } else if (ft == "MIN_NOTIONAL") { - nfo.min_volume = f["minNotional"].getNumber(); +void Interface::initSymbols() { + if (symbols.empty()) { + Value res = px.public_request("/api/v1/exchangeInfo",Value()); + + std::uintptr_t srvtm = res["serverTime"].getUInt(); + std::uintptr_t localtm = now(); + time_diff = srvtm - localtm; + + using VT = Symbols::value_type; + std::vector bld; + for (Value smb: res["symbols"]) { + MarketInfo nfo; + nfo.asset_symbol = smb["baseAsset"].getString(); + nfo.currency_symbol = smb["quoteAsset"].getString(); + nfo.currency_step = std::pow(10,-smb["quotePrecision"].getNumber()); + nfo.asset_step = std::pow(10,-smb["baseAssetPrecision"].getNumber()); + nfo.feeScheme = income; + nfo.min_size = 0; + nfo.min_volume = 0; + nfo.fees = 0; + for (Value f: smb["filters"]) { + auto ft = f["filterType"].getString(); + if (ft == "LOT_SIZE") { + nfo.min_size = f["minQty"].getNumber(); + nfo.asset_step = f["stepSize"].getNumber(); + } else if (ft == "PRICE_FILTER") { + nfo.currency_step = f["tickSize"].getNumber(); + } else if (ft == "MIN_NOTIONAL") { + nfo.min_volume = f["minNotional"].getNumber(); + } } + std::string symbol = smb["symbol"].getString(); + bld.push_back(VT(symbol, nfo)); } - std::string symbol = smb["symbol"].getString(); - bld.push_back(VT(symbol, nfo)); + symbols = Symbols(std::move(bld)); } - symbols = Symbols(std::move(bld)); - idsrc = now(); +} +void Interface::onInit() { + idsrc = now(); } inline Interface::MarketInfo Interface::getMarketInfo(const std::string_view &pair) { + initSymbols(); auto iter = symbols.find(pair); if (iter == symbols.end()) @@ -356,7 +388,7 @@ inline Interface::MarketInfo Interface::getMarketInfo(const std::string_view &pa } inline double Interface::getFees(const std::string_view &pair) { - if (px.hasKey) { + if (px.hasKey()) { if (!feeInfo.defined()) { updateBalCache(); } @@ -364,42 +396,6 @@ inline double Interface::getFees(const std::string_view &pair) { } else { return 0.001; } - -} - - - -void Interface::syncTrades(std::size_t fromTime) { - std::size_t startTime ; - do { - startTime = 0; - startTime--; - for (auto &&k : tradeMap) { - if (!k.second.empty()) { - const auto &v = k.second.back(); - startTime = std::min(startTime, v.time-1); - } - } - ++startTime; - } while (syncTradesCycle(std::max(startTime,fromTime))); -} - - -bool Interface::syncTradesCycle(std::size_t fromTime) { - return true; -} - - - - -inline bool Interface::syncTradeCheckTime(const std::vector &cont, - std::size_t time, Value tradeID) { - - if (cont.empty()) return true; - const Trade &b = cont.back(); - if (b.time < time) return true; - if (b.time == time && Value::compare(b.id, tradeID) < 0) return true; - return false; } @@ -411,6 +407,11 @@ bool Interface::tradeOrder(const Trade &a, const Trade &b) { return Value::compare(a.id,b.id) < 0; } +inline void Interface::onLoadApiKey(json::Value keyData) { + px.privKey = keyData["privKey"].getString(); + px.pubKey = keyData["pubKey"].getString(); +} + inline Value Interface::generateOrderId(Value clientId) { std::ostringstream stream; Value(json::array,{idsrc++, clientId.stripKey()},false).serializeBinary([&](char c){ @@ -427,28 +428,60 @@ void Interface::enable_debug(bool enable) { px.debug = enable; } +Interface::BrokerInfo Interface::getBrokerInfo() { + return BrokerInfo{ + px.hasKey(), + "binance", + "Binance", + "https://www.binance.com/", + "1.0", + "Copyright (c) 2019 Ondřej Novák\n\n" + +"Permission is hereby granted, free of charge, to any person " +"obtaining a copy of this software and associated documentation " +"files (the \"Software\"), to deal in the Software without " +"restriction, including without limitation the rights to use, " +"copy, modify, merge, publish, distribute, sublicense, and/or sell " +"copies of the Software, and to permit persons to whom the " +"Software is furnished to do so, subject to the following " +"conditions: " +"\n\n" +"The above copyright notice and this permission notice shall be " +"included in all copies or substantial portions of the Software. " +"\n\n" +"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, " +"EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES " +"OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND " +"NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT " +"HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, " +"WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING " +"FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR " +"OTHER DEALINGS IN THE SOFTWARE.", +"iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAABlBMVEX31lXzui274TLJAAAAAXRS" +"TlMAQObYZgAAAYlJREFUeNrt20FuwzAQQ9Hw/pfuku0iNQxCfQ1gLgMin4ltSR6NXo8efboSzU80" +"P9H8RPMTxa8Uv1L8SvErxa8Uv1L8SvErxa8Uv1L8SvErHiCKXyl+pfiV4leKXyl+pfiV4lean7/h" +"r5Pzzt8S7Pwtwc5fE+z88wnyVjdch/hbgp2/JNj5m3cPsJn3BJt5T7CZ9wSbeU8wmvcEd8ygGnR8" +"OrxjNhXBmllNdOa/H2h/9Vbv+Dd+wIlPX7cu4fi/XJiHq33h3c23bvcD5lsP/AFzvguMj/mpgzPE" +"PpVlMu+TeUbzupzJbN4WdMluXpa0mcz1+gD+Evib0D+GfiDSQ7GfjPx07Bckfkm2L0on86kF+MUX" +"qxcT8GpmX07967kvUPgSjS9S+TKdL1TyUi0vVvNyvd6w0Fs2etNKb9vpjUu9das3r/X2vW9gwAl8" +"E4tN8F8amVQC38xmE+h+Qt1R6ZtabQLf2GwT+OZ2m8AfcLAJ/CEXm8AfdLIJ/GE3m8AfeLQJ+KHX" +"R48+XF9VnRBZ1a2+VQAAAABJRU5ErkJggg==" + + + }; +} + int main(int argc, char **argv) { using namespace json; if (argc < 2) { - std::cerr << "No config given, terminated" << std::endl; + std::cerr << "Required storage path" << std::endl; return 1; } try { - ondra_shared::IniConfig ini; - - ini.load(argv[1]); - - Config cfg = load(ini["api"]); - Proxy proxy(cfg); - - - Interface ifc(proxy); - - ifc.init(); - + Interface ifc(argv[1]); ifc.dispatch(); diff --git a/src/binance/proxy.cpp b/src/binance/proxy.cpp index cf27da3f..fcfb399f 100644 --- a/src/binance/proxy.cpp +++ b/src/binance/proxy.cpp @@ -23,20 +23,18 @@ using ondra_shared::logDebug; -static constexpr std::uint64_t start_time = 1557858896532; -Proxy::Proxy(Config config):config(config) { - auto now = std::chrono::system_clock::now(); - std::size_t init_time = std::chrono::duration_cast(now.time_since_epoch()).count() - start_time; - nonce = init_time * 100; - hasKey = !config.privKey.empty() && !config.pubKey.empty(); +Proxy::Proxy() { + apiUrl = "https://api.binance.com"; + auto init_time = now(); + nonce = init_time * 100; } void Proxy::setTimeDiff(std::intptr_t t) { this->time_diff = t; } -std::uintptr_t Proxy::now() { +std::uint64_t Proxy::now() { return std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ).count(); @@ -58,7 +56,7 @@ void Proxy::buildParams(const json::Value& params, std::ostream& data) { json::Value Proxy::public_request(std::string method, json::Value data) { std::ostringstream urlbuilder; - urlbuilder << config.apiUrl << method; + urlbuilder << apiUrl << method; buildParams(data, urlbuilder); std::ostringstream response; curl_handle.reset(); @@ -96,19 +94,19 @@ static std::string signData(std::string_view key, std::string_view data) { } json::Value Proxy::private_request(Method method, std::string command, json::Value data) { - if (!hasKey) + if (!hasKey()) throw std::runtime_error("Function requires valid API keys"); data = data.replace("timestamp", now()+time_diff); std::ostringstream urlbuilder; - urlbuilder << config.apiUrl << command; + urlbuilder << apiUrl << command; std::ostringstream databld; buildParams(data, databld); std::string request = databld.str().substr(1); - std::string sign = signData(config.privKey,request); + std::string sign = signData(privKey,request); std::string url = urlbuilder.str(); request.append("&signature=").append(sign); @@ -138,7 +136,7 @@ json::Value Proxy::private_request(Method method, std::string command, json::Val } std::list headers; - headers.push_back("X-MBX-APIKEY: "+config.pubKey); + headers.push_back("X-MBX-APIKEY: "+pubKey); curl_handle.setOpt(new cURLpp::Options::HttpHeader(headers)); @@ -159,4 +157,6 @@ json::Value Proxy::private_request(Method method, std::string command, json::Val return v; } - +bool Proxy::hasKey() const { + return !privKey.empty() && !pubKey.empty(); +} diff --git a/src/binance/proxy.h b/src/binance/proxy.h index a3ea720e..1a301a8c 100644 --- a/src/binance/proxy.h +++ b/src/binance/proxy.h @@ -10,14 +10,16 @@ #include #include -#include "config.h" class Proxy { public: - Proxy(Config config); + Proxy(); + + std::string apiUrl; + std::string privKey; + std::string pubKey; - Config config; cURLpp::Easy curl_handle; std::uint64_t nonce; @@ -32,9 +34,9 @@ class Proxy { json::Value public_request(std::string method, json::Value data); json::Value private_request(Method method, std::string command, json::Value data); - bool hasKey; + bool hasKey() const; void setTimeDiff(std::intptr_t t); - static std::uintptr_t now(); + static std::uint64_t now(); bool debug = false; diff --git a/src/bitmex/CMakeLists.txt b/src/bitmex/CMakeLists.txt new file mode 100644 index 00000000..64623c6f --- /dev/null +++ b/src/bitmex/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 2.8) +add_compile_options(-std=c++17) + + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin/brokers/) + +add_executable (bitmex main.cpp proxy.cpp ../brokers/api.cpp ) +target_link_libraries (bitmex LINK_PUBLIC imtjson curlpp ssl crypto curl stdc++fs pthread) +install(TARGETS bitmex DESTINATION "bin/brokers") diff --git a/src/bitmex/main.cpp b/src/bitmex/main.cpp new file mode 100644 index 00000000..8d53d929 --- /dev/null +++ b/src/bitmex/main.cpp @@ -0,0 +1,582 @@ +/* + * main.cpp + * + * Created on: 21. 5. 2019 + * Author: ondra + */ +#include +#include +#include +#include + +#include +#include "shared/toString.h" +#include "proxy.h" +#include +#include + +#include "../brokers/api.h" +#include +#include "../shared/linear_map.h" +#include "../shared/iterator_stream.h" +#include +#include +#include +#include "../imtjson/src/imtjson/string.h" +#include "../main/sgn.h" + +using namespace json; + +class Interface: public AbstractBrokerAPI { +public: + Proxy px; + + Interface(const std::string &path):AbstractBrokerAPI(path, { + Object + ("name","key") + ("label","ID") + ("type","string"), + Object + ("name","secret") + ("label","Secret") + ("type","string"), + Object + ("name","server") + ("label","Server") + ("type","enum") + ("options",Object + ("main","www.bitmex.com") + ("testnet","testnet.bitmex.com")) + ("default","main")}) + ,optionsFile(path+".conf"){} + + + virtual double getBalance(const std::string_view & symb) override; + virtual TradesSync syncTrades(json::Value lastId, const std::string_view & pair) override; + virtual Orders getOpenOrders(const std::string_view & par)override; + virtual Ticker getTicker(const std::string_view & piar)override; + virtual json::Value placeOrder(const std::string_view & pair, + double size, + double price, + json::Value clientId, + json::Value replaceId, + double replaceSize)override; + virtual bool reset()override; + virtual MarketInfo getMarketInfo(const std::string_view & pair)override; + virtual double getFees(const std::string_view &pair)override; + virtual std::vector getAllPairs()override; + virtual void enable_debug(bool enable) override; + virtual BrokerInfo getBrokerInfo() override; + virtual void onLoadApiKey(json::Value keyData) override; + virtual void onInit() override; + + + struct SymbolInfo { + String id; + String qtc; + bool inverse; + double multiplier; + double lotSize; + double leverage; + double tickSize; + double quantoMult; + + }; + + using SymbolList = ondra_shared::linear_map; + + SymbolList slist; + + const SymbolInfo &getSymbol(const std::string_view &id); + virtual json::Value getSettings(const std::string_view &) const override; + virtual void setSettings(json::Value v) override; + + + +private: + std::size_t uid_cnt = Proxy::now(); + void updateSymbols(); + + Value balanceCache; + Value positionCache; + Value orderCache; + + Value readOrders(); + + + std::size_t quoteEachMin = 5; + bool allowSmallOrders = false; + std::string optionsFile; + + void saveOptions(); + void loadOptions(); + Value getBalanceCache(); +}; + + + + +int main(int argc, char **argv) { + using namespace json; + + if (argc < 2) { + std::cerr << "Requires a signle parametr" << std::endl; + return 1; + } + + Interface ifc(argv[1]); + ifc.dispatch(); + +} + +Value Interface::getBalanceCache() { + if (!balanceCache.hasValue()) { + balanceCache = px.request("GET","/api/v1/user/margin", Object("currency","XBt") + ("columns",{"marginBalance"})); + } + return balanceCache; +} + +inline double Interface::getBalance(const std::string_view &symb) { + if (symb == "BTC") { + return getBalanceCache()["marginBalance"].getNumber()*1e-8; + } else if (symb == "USD") { + return getBalanceCache()["marginBalance"].getNumber()*1e-8/0.000001; + } else { + const SymbolInfo &s = getSymbol(symb); + if (!positionCache.hasValue()) { + positionCache = px.request("GET","/api/v1/position",Object("columns",{"symbol","currentQty"})); + } + Value x = positionCache.find([&](Value v){return v["symbol"] == symb;}); + double q = x["currentQty"].getNumber(); + if (s.inverse) q = -q; + return q*s.multiplier; + } + return 0; +} + +static std::size_t parseTime(json::String date) { + int y,M,d,h,m; + float s; + sscanf(date.c_str(), "%d-%d-%dT%d:%d:%fZ", &y, &M, &d, &h, &m, &s); + float sec; + float msec = std::modf(s,&sec)*1000; + std::tm t={0}; + t.tm_year = y - 1900; + t.tm_mon = M-1; + t.tm_mday = d; + t.tm_hour = h; + t.tm_min = m; + t.tm_sec = static_cast(sec); + std::size_t res = timegm(&t) * 1000 + static_cast(msec); + return res; + +} + + + + +inline Interface::TradesSync Interface::syncTrades(json::Value lastId, const std::string_view &pair) { + const SymbolInfo &s = getSymbol(pair); + Value trades; + Value lastExecId = lastId[1]; + Value columns = {"execID","transactTime","side","lastQty","lastPx","symbol","execType"}; + if (lastId.hasValue()) { + trades = px.request("GET","/api/v1/execution/tradeHistory",Object + ("filter", Object("execType",Value(json::array,{"Trade"}))) + ("startTime",lastId[0]) + ("count", 100) + ("symbol", pair) + ("columns",columns)); + } else { + trades = px.request("GET","/api/v1/execution/tradeHistory",Object + ("filter", Object("execType",Value(json::array,{"Trade"}))) + ("reverse",true) + ("count", 1) + ("symbol", pair) + ("columns",columns)); + + } + + auto idx = trades.findIndex([&](Value item) { + return item["execID"] == lastExecId; + }); + if (idx != -1) { + trades = trades.slice(idx+1); + } + + Value lastExecTime = lastId[0]; + TradesSync resp; + for (Value item: trades) { + lastExecId = item["execID"]; + lastExecTime = item["transactTime"]; + StrViewA side = item["side"].getString(); + double mult = side=="Buy"?1:side=="Sell"?-1:0; + if (mult == 0) continue; + if (s.inverse) mult=-mult; + double size = mult*item["lastQty"].getNumber()*s.multiplier; + double price = s.inverse?1.0/item["lastPx"].getNumber():item["lastPx"].getNumber(); + resp.trades.push_back(Trade{ + lastExecId, + parseTime(lastExecTime.toString()), + size, + price, + size, + price + }); + } + + if (resp.trades.empty()) { + resp.lastId = lastId; + } else { + resp.lastId = {lastExecTime, lastExecId}; + } + return resp; +} + +inline Interface::Orders Interface::getOpenOrders(const std::string_view &pair) { + const SymbolInfo &s = getSymbol(pair); + + Value orders = readOrders(); + Value myorders = orders.filter([&](const Value &v) { + return v["symbol"].getString() == pair; + }); + Orders resp; + for (Value ord: myorders) { + double mult = ord["side"].getString() == "Sell"?-1:1; + double size = ord["orderQty"].getNumber(); + double price = ord["price"].getNumber(); + StrViewA clid = ord["clOrdID"].getString(); + Value id = ord["orderID"]; + Value clientId; + if (!clid.empty()) try{ + clientId = Value::fromString(clid)[0]; + } catch (...) { + + } + if (s.inverse) { + mult = -mult; + price = 1/price; + } + resp.push_back(Order { + id,clientId,size*s.multiplier*mult,price + }); + + } + return resp; +} + +inline Interface::Ticker Interface::getTicker(const std::string_view &pair) { + const SymbolInfo &s = getSymbol(pair); + Value resp = px.request("GET","/api/v1/orderBook/L2", Object("symbol",pair)("depth",1)); + double bid = 0; + double ask = 0; + for (Value v: resp) { + double price = v["price"].getNumber(); + if (v["side"].getString() == "Sell") { + if (s.inverse) bid =1/price; else ask = price; + } + else if (v["side"].getString() == "Buy") { + if (s.inverse) ask =1/price; else bid = price; + } + if (bid == 0) bid = ask; + if (ask == 0) ask = bid; + } + return Ticker{bid, ask, sqrt(bid*ask), px.now()*1000}; +} + +static bool almostSame(double a, double b) { + double mdl = (fabs(a) + fabs(b))/2; + return fabs(a - b) < mdl*1e-6; +} + +inline json::Value Interface::placeOrder(const std::string_view &pair, + double size, double price, json::Value clientId, json::Value replaceId, + double replaceSize) { + + std::intptr_t now = std::intptr_t(px.now()*1000); + + const SymbolInfo &s = getSymbol(pair); + if (s.inverse && price) { + size = -size; + price = 1/price; + price = round(price/s.tickSize)*s.tickSize; + } + + Value side = size < 0?"Sell":"Buy"; + Value qty = fabs(size/s.multiplier); + + Value curOrders = readOrders(); + if (replaceId.hasValue()) { + Value toCancel = curOrders.find([&](Value v) { + return v["orderID"] == replaceId; + }); + if (toCancel.hasValue()) { + std::intptr_t orderTime = std::intptr_t (parseTime(toCancel["transactTime"].getString())); + std::intptr_t limitTime = std::intptr_t(quoteEachMin*60000); + if (size != 0 && quoteEachMin && now-orderTime < limitTime) { + if (px.debug) std::cerr << "Re-quote disallowed for this time (" << (now-orderTime) <<" < " << limitTime <<") "<< std::endl; + return toCancel["orderID"]; + } + if (toCancel["Side"] == side && toCancel["symbol"].getString() == pair + && almostSame(toCancel["orderQty"].getNumber() , qty.getNumber()) + && almostSame(toCancel["price"].getNumber() , price)) { + return toCancel["orderID"]; + } else { + Value resp = px.request("DELETE","/api/v1/order",Object("orderID",replaceId)); + Value remain = resp[0]["orderQty"].getNumber(); + if (!almostSame(remain.getNumber(), replaceSize) && remain.getNumber() < replaceSize) { + return nullptr; + } + } + } + } + if (size == 0) return nullptr; + Value clId; + if (clientId.hasValue()) { + clId = {clientId, ++uid_cnt}; + clId = clId.toString(); + } + Object order; + order.set("symbol", pair) + ("side",side) + ("orderQty",qty) + ("price",price) + ("clOrdID", clId) + ("ordType","Limit") + ("execInst","ParticipateDoNotInitiate"); + Value resp = px.request("POST","/api/v1/order",Value(),order); + return resp["orderID"]; +} + +inline bool Interface::reset() { + balanceCache = nullptr; + positionCache = nullptr; + orderCache = nullptr; + + + return true; +} + +inline Interface::MarketInfo Interface::getMarketInfo(const std::string_view &pair) { + const SymbolInfo &s = getSymbol(pair); + + if (s.inverse) { + return MarketInfo{ + std::string(pair), + s.qtc.str(), + s.multiplier*s.lotSize, + 0, + s.multiplier*s.lotSize, + allowSmallOrders?0:0.0026/s.quantoMult, + 0, + currency, + s.leverage, + true, + "USD", + px.testnet + }; + } else { + return MarketInfo{ + std::string(pair), + s.qtc.str(), + s.multiplier*s.lotSize, + s.tickSize, + s.multiplier*s.lotSize, + allowSmallOrders?0:0.0026/s.quantoMult, + 0, + currency, + s.leverage, + false, + "XBT", + px.testnet + }; + } +} + +inline double Interface::getFees(const std::string_view &pair) { + return 0; +} + +void Interface::enable_debug(bool enable) { + px.debug = enable; +} + +Interface::BrokerInfo Interface::getBrokerInfo() { + return BrokerInfo{ + px.hasKey(), + "bitmex", + "BitMEX", + "https://www.bitmex.com/", + "1.0", + R"mit(Copyright (c) 2019 Ondřej Novák + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE.)mit", +"iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAB1klEQVR42u3a0VECQRBF0dEyEb4x" +"K+IyK/kmFE2Bhemd7ulzEtBa3vbFKscAAAAAAAAAAAAAdvCx8oc/vq9/VR7U5fce9qyut8f053D/" +"uTz1+356B9aK+PCPMIANPfv2Lx1ApfO/Mxeg8fk3gARfAA2AZf1fNgD9dwH0P0H/DaB5/w2gef+X" +"DED/XQD9T9J/A2jefwNo3v/TB6D/LoD+J+q/ATTvvwE07/+pA9B/F0D/k/XfAJr33wCa9/+0Aei/" +"C6D/CftvAM37bwDN+3/KAPTfBdD/pP03gOb9N4Dm/Q8fgP67APqfuP9jjPGl1S6AD79p/0MHoP8u" +"gP4n778BUGsA+j+3/2ED0H8XQP8L9N8AqDMA/Z/f/5AB6L8LoP9F+m8A1BiA/sf0f/oA9N8F0P9C" +"/TcA8g9A/+P6P3UA+u8C6H+x/hsAuQeg/7H9H2PSfwVX63/FUy0BGIC3v9EA9D++/1MG4O9/FwAD" +"0H8D0P9y/X97APrvAmAA+m8A+l+y/28NQP9dAAxA/w1A/8v2/+UB6L8LgAHovwHof+n+vzQA/XcB" +"MAD9NwD9L9//wwPQfxcAA9B/A9D/Lfp/aAD67wJgAPpvAPq/Tf8BAAAAAAAAAAAA2MQ/A0e46eQ0" +"zFcAAAAASUVORK5CYII=", true + }; +} + +inline std::vector Interface::getAllPairs() { + if (slist.empty()) updateSymbols(); + std::vector out; + out.reserve(slist.size()); + for (auto &&k: slist) { + out.push_back(k.second.id.str()); + } + return out; +} + +inline void Interface::onLoadApiKey(json::Value keyData) { + px.setTestnet(keyData["server"].getString() == "testnet"); + px.privKey = keyData["secret"].getString(); + px.pubKey = keyData["key"].getString(); +} + +inline void Interface::onInit() { + loadOptions(); +} + +void Interface::updateSymbols() { + Value resp = px.request("GET", "/api/v1/instrument/active", + Object("columns",{"optionUnderlyingPrice","isQuanto","settlCurrency","symbol","isInverse","rootSymbol","quoteCurrency","multiplier","lotSize","initMargin","tickSize"})); + std::vector smap; + for (Value s : resp) { + SymbolInfo sinfo; + sinfo.id = s["symbol"].toString(); + if (s["optionUnderlyingPrice"].hasValue()) + continue; + + if (s["settlCurrency"].getString() != "XBt") + continue; + + sinfo.inverse = s["isInverse"].getBool(); + if (sinfo.inverse) { + if (s["rootSymbol"].getString() != "XBT") + continue; + sinfo.qtc = "BTC"; + } + sinfo.qtc = "BTC"; + sinfo.quantoMult = 1; + + sinfo.multiplier = fabs(s["multiplier"].getNumber())/ (100000000.0*sinfo.quantoMult); + sinfo.lotSize = s["lotSize"].getNumber(); + sinfo.leverage = 1/s["initMargin"].getNumber(); + sinfo.tickSize = s["tickSize"].getNumber(); + smap.push_back( { sinfo.id.str(), sinfo }); + } + slist.swap(smap); +} + +const Interface::SymbolInfo& Interface::getSymbol(const std::string_view &id) { + if (slist.empty()) { + updateSymbols(); + } + auto iter = slist.find(id); + if (iter == slist.end()) throw std::runtime_error("Unknown symbol"); + return iter->second; +} + +inline json::Value Interface::getSettings(const std::string_view&) const { + char m[4]; + m[0] = 'm'; + m[1] = (quoteEachMin/10)%10+'0'; + m[2] = quoteEachMin%10+'0'; + m[3] = 0; + + + return { + Object + ("name","quoteEachMin") + ("label","Allow to move the order") + ("type","enum") + ("options",Object + ("m00", "anytime") + ("m01", "every 1 minute") + ("m02", "every 2 minutes") + ("m03", "every 3 minutes") + ("m04", "every 4 minutes") + ("m05", "every 5 minutes") + ("m07", "every 6 minutes") + ("m10", "every 10 minutes") + ("m10", "every 12 minutes") + ("m15", "every 15 minutes") + ("m20", "every 20 minutes") + ("m30", "every 30 minutes") + ("m60", "every 60 minutes")) + ("default",m), + Object + ("name","allowSmallOrders") + ("label","Allow small orders (spam orders)") + ("type","enum") + ("options", Object + ("allow", "Allow (not recommended)") + ("disallow", "Disallow")) + ("default",allowSmallOrders?"allow":"disallow") + }; +} + +inline void Interface::setSettings(json::Value v) { + quoteEachMin = std::strtod(v["quoteEachMin"].getString().data+1,nullptr); + allowSmallOrders = v["allowSmallOrders"].getString() == "allow"; + saveOptions(); +} + +Value Interface::readOrders() { + if (!orderCache.hasValue()) { + orderCache = px.request("GET","/api/v1/order",Object + ("filter",Object + ("ordStatus",{"New","PartiallyFilled","DoneForDay","Stopped"})) + ); + } + return orderCache; + + +} + +inline void Interface::saveOptions() { + Object opt; + opt.set("quoteEachMin",quoteEachMin); + opt.set("allowSmallOrders", allowSmallOrders); + std::ofstream file(optionsFile,std::ios::out|std::ios::trunc); + Value(opt).toStream(file); +} + +inline void Interface::loadOptions() { + try { + std::ifstream file(optionsFile, std::ios::in); + if (!file) return; + Value v = Value::fromStream(file); + quoteEachMin = v["quoteEachMin"].getUInt(); + allowSmallOrders = v["allowSmallOrders"].getUInt(); + } catch (std::exception &e) { + std::cerr<<"Failed to load config: " << e.what() << std::endl; + } +} diff --git a/src/bitmex/proxy.cpp b/src/bitmex/proxy.cpp new file mode 100644 index 00000000..fd46bef6 --- /dev/null +++ b/src/bitmex/proxy.cpp @@ -0,0 +1,187 @@ +/* + * proxy.cpp + * + * Created on: 21. 5. 2019 + * Author: ondra + */ + + + + +#include "proxy.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include "../shared/logOutput.h" + +using json::Value; +using ondra_shared::logDebug; + +Proxy::Proxy() { + setTestnet(false); +} + + +bool Proxy::hasKey() const { + return !privKey.empty() && !pubKey.empty() ; +} + + +void Proxy::urlEncode(const std::string_view &text, std::ostream &out) { + char* esc = curl_easy_escape(curl_handle.getHandle(), text.data(), + text.length()); + out << esc; + curl_free(esc); +} + +std::string Proxy::buildPath(const std::string_view path, const json::Value &query) { + std::ostringstream pathbuild; + pathbuild << path; + if (query.type() == json::object && !query.empty()) { + char sep = '?'; + for (Value v : query) { + pathbuild << sep; + sep = '&'; + urlEncode(v.getKey(), pathbuild); + pathbuild << '='; + urlEncode(v.toString().str(), pathbuild); + } + } + return pathbuild.str(); +} + +json::Value Proxy::request( + const std::string_view &verb, + const std::string_view path, + const json::Value &query, + const json::Value &data) { + + curl_handle.reset(); + std::string fpath = buildPath(path, query); + std::string fdata = data.hasValue()?data.stringify().str():json::StrViewA(); + + std::list headers; + + if (hasKey()) { + auto authdata = signRequest(verb, fpath, fdata); + + headers.push_back("api-expires: "+authdata.expires); + headers.push_back("api-key: "+authdata.key); + headers.push_back("api-signature: "+authdata.signature); + + } + + if (verb != "GET" && !fdata.empty()) { + headers.push_back("Content-Type: application/json"); + headers.push_back("Accepts: application/json"); + } + + std::istringstream request(fdata); + std::ostringstream response; + + std::string url = apiUrl + fpath; + + curl_handle.reset(); + if (verb == "POST") { + curl_handle.setOpt(new cURLpp::Options::Post(true)); + curl_handle.setOpt(new cURLpp::Options::ReadStream(&request)); + curl_handle.setOpt(new cURLpp::Options::PostFieldSize(fdata.length())); + } else if (verb == "PUT") { + curl_handle.setOpt(new cURLpp::Options::Put(true)); + curl_handle.setOpt(new cURLpp::Options::ReadStream(&request)); + curl_handle.setOpt(new cURLpp::Options::PostFieldSize(fdata.length())); + } else if (verb == "DELETE") { + curl_handle.setOpt(new cURLpp::Options::CustomRequest("DELETE")); + } + + curl_handle.setOpt(new cURLpp::Options::HttpHeader(headers)); + + curl_handle.setOpt(new cURLpp::Options::Url(url)); + curl_handle.setOpt(new cURLpp::Options::WriteStream(&response)); + + if (debug) { + std::cerr << "SEND: " << verb << " " << url << std::endl; + if (!fdata.empty()) std::cerr << "SEND BODY: " << fdata << std::endl; + } + + curl_handle.perform(); + + if (debug) { + std::cerr << "RECV: " << response.str() << std::endl; + } + + + + auto resp_code = curlpp::infos::ResponseCode::get(curl_handle); + if (resp_code /100 != 2) { + std::string errmsg; + try { + Value p = Value::fromString(response.str()); + errmsg = std::to_string(resp_code).append(" ") + .append(std::string_view(p["error"]["message"].getString())) + .append(" ") + .append(std::string_view(p["error"]["ordRejReason"].getString())); + } catch (...) { + errmsg = std::to_string(resp_code).append(" ").append(response.str()); + } + throw std::runtime_error(errmsg); + } else { + try { + Value p = Value::fromString(response.str()); + return p; + } catch (...) { + std::string errmsg; + errmsg = std::to_string(500).append(" ").append(response.str()); + throw std::runtime_error(errmsg); + } + + } +} + + +std::uint64_t Proxy::now() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() + ).count(); + +} + +void Proxy::setTestnet(bool testnet) { + apiUrl = testnet?"https://testnet.bitmex.com":"https://www.bitmex.com"; + this->testnet = testnet; +} + +Proxy::AuthData Proxy::signRequest(const std::string_view &verb, + const std::string_view &path, const std::string_view &data) { + + std::string expires = std::to_string(now()+30); + std::ostringstream buff; + buff << verb << path << expires << data; + std::string msg = buff.str(); + unsigned char digest[256]; + unsigned int digest_len = sizeof(digest); + HMAC(EVP_sha256(),privKey.data(),privKey.length() + , reinterpret_cast(msg.data()), msg.length(), + digest, &digest_len); + + std::ostringstream digeststr; + for (unsigned int i = 0; i < digest_len; i++) { + digeststr << std::hex << std::setw(2) << std::setfill('0') + << static_cast(digest[i]); + } + return AuthData { + pubKey, + digeststr.str(), + expires + }; +} diff --git a/src/bitmex/proxy.h b/src/bitmex/proxy.h new file mode 100644 index 00000000..f1c10369 --- /dev/null +++ b/src/bitmex/proxy.h @@ -0,0 +1,58 @@ +/* + * proxy.h + * + * Created on: 21. 5. 2019 + * Author: ondra + */ + +#ifndef SRC_COINMATE_PROXY_H_ +#define SRC_COINMATE_PROXY_H_ +#include +#include + +#include + +class Proxy { +public: + + Proxy(); + + std::string apiUrl; + std::string privKey; + std::string pubKey; + cURLpp::Easy curl_handle; + bool testnet = false; + + json::Value request( + const std::string_view &verb, + const std::string_view path, + const json::Value &query = json::Value(), + const json::Value &data = json::Value()); + + bool hasKey() const; + bool debug = false; + + static std::uint64_t now(); + + void setTestnet(bool testnet); + + struct AuthData { + std::string key; + std::string signature; + std::string expires; + }; + + AuthData signRequest(const std::string_view &verb, + const std::string_view &path, + const std::string_view &data); + + + void urlEncode(const std::string_view &text, std::ostream &out); + std::string buildPath(const std::string_view path, const json::Value &query); + + +}; + + + +#endif /* SRC_COINMATE_PROXY_H_ */ diff --git a/src/brokers/api.cpp b/src/brokers/api.cpp index dea66b27..105e6b5c 100644 --- a/src/brokers/api.cpp +++ b/src/brokers/api.cpp @@ -5,14 +5,17 @@ * Author: ondra */ -#include +#include #include "api.h" +#include #include #include #include #include #include +#include +#include #include "../main/istockapi.cpp" using namespace json; @@ -20,26 +23,26 @@ using namespace json; -static Value getBalance(IStockApi &handler, const Value &request) { +static Value getBalance(AbstractBrokerAPI &handler, const Value &request) { return handler.getBalance(request.getString()); } -static Value getTrades(IStockApi &handler, const Value &request) { - IStockApi::TradeHistory hst( - handler.getTrades( +static Value syncTrades(AbstractBrokerAPI &handler, const Value &request) { + AbstractBrokerAPI::TradesSync hst( + handler.syncTrades( request["lastId"], - request["fromTime"].getUInt(), request["pair"].getString())); Array response; - response.reserve(hst.size()); - for (auto &&itm: hst) { + response.reserve(hst.trades.size()); + for (auto &&itm: hst.trades) { response.push_back(itm.toJSON()); } - return response; + return Object("trades",response) + ("lastId", hst.lastId); } -static Value getOpenOrders(IStockApi &handler, const Value &request) { - IStockApi::Orders ords(handler.getOpenOrders(request.getString())); +static Value getOpenOrders(AbstractBrokerAPI &handler, const Value &request) { + AbstractBrokerAPI::Orders ords(handler.getOpenOrders(request.getString())); Array response; response.reserve(ords.size()); @@ -53,8 +56,8 @@ static Value getOpenOrders(IStockApi &handler, const Value &request) { return response; } -static Value getTicker(IStockApi &handler, const Value &req) { - IStockApi::Ticker tk(handler.getTicker(req.getString())); +static Value getTicker(AbstractBrokerAPI &handler, const Value &req) { + AbstractBrokerAPI::Ticker tk(handler.getTicker(req.getString())); return Object ("bid", tk.bid) @@ -64,7 +67,7 @@ static Value getTicker(IStockApi &handler, const Value &req) { } -static Value placeOrder(IStockApi &handler, const Value &req) { +static Value placeOrder(AbstractBrokerAPI &handler, const Value &req) { return handler.placeOrder(req["pair"].getString(), req["size"].getNumber(), req["price"].getNumber(), @@ -73,7 +76,7 @@ static Value placeOrder(IStockApi &handler, const Value &req) { req["replaceOrderSize"].getNumber()); } -static Value enableDebug(IStockApi &handler, const Value &req) { +static Value enableDebug(AbstractBrokerAPI &handler, const Value &req) { AbstractBrokerAPI *h = dynamic_cast(&handler); if (h) { h->enable_debug(req.getBool()); @@ -82,12 +85,23 @@ static Value enableDebug(IStockApi &handler, const Value &req) { } -static Value reset(IStockApi &handler, const Value &req) { +static Value getBrokerInfo(AbstractBrokerAPI &handler, const Value &req) { + AbstractBrokerAPI::BrokerInfo nfo = handler.getBrokerInfo(); + return Object("name",nfo.exchangeName) + ("url",nfo.exchangeUrl) + ("version",nfo.version) + ("licence",nfo.licence) + ("trading_enabled", nfo.trading_enabled) + ("settings",nfo.settings) + ("favicon",Value(BinaryView(StrViewA(nfo.favicon)),base64)); +} + +static Value reset(AbstractBrokerAPI &handler, const Value &req) { handler.reset(); return Value(); } -static Value getAllPairs(IStockApi &handler, const Value &req) { +static Value getAllPairs(AbstractBrokerAPI &handler, const Value &req) { auto r = handler.getAllPairs(); Array response; response.reserve(r.size()); @@ -95,13 +109,13 @@ static Value getAllPairs(IStockApi &handler, const Value &req) { return response; } -static Value getFees(IStockApi &handler, const Value &req) { +static Value getFees(AbstractBrokerAPI &handler, const Value &req) { return handler.getFees(req.getString()); } -static Value getInfo(IStockApi &handler, const Value &req) { - IStockApi::MarketInfo nfo ( handler.getMarketInfo(req.getString()) ); +static Value getInfo(AbstractBrokerAPI &handler, const Value &req) { + AbstractBrokerAPI::MarketInfo nfo ( handler.getMarketInfo(req.getString()) ); return Object ("asset_step",nfo.asset_step) ("currency_step", nfo.currency_step) @@ -110,20 +124,39 @@ static Value getInfo(IStockApi &handler, const Value &req) { ("min_size", nfo.min_size) ("min_volume", nfo.min_volume) ("fees", nfo.fees) - ("feeScheme",IStockApi::strFeeScheme[nfo.feeScheme]) + ("feeScheme",AbstractBrokerAPI::strFeeScheme[nfo.feeScheme]) ("leverage", nfo.leverage) ("invert_price", nfo.invert_price) - ("inverted_symbol", nfo.inverted_symbol); + ("inverted_symbol", nfo.inverted_symbol) + ("simulator", nfo.simulator); +} + +static Value setApiKey(AbstractBrokerAPI &handler, const Value &req) { + handler.setApiKey(req); + return Value(); +} + +static Value getApiKeyFields(AbstractBrokerAPI &handler, const Value &req) { + return handler.getApiKeyFields(); +} + +static Value setSettings(AbstractBrokerAPI &handler, const Value &req) { + handler.setSettings(req);; + return Value(); +} + +static Value getSettings(AbstractBrokerAPI &handler, const Value &req) { + return handler.getSettings(req.toString().str()); } ///Handler function -using HandlerFn = Value (*)(IStockApi &handler, const Value &request); -using MethodMap = ondra_shared::linear_map ; +using HandlerFn = Value (*)(AbstractBrokerAPI &handler, const Value &request); +using MethodMap = ondra_shared::linear_map ; static MethodMap methodMap ({ {"getBalance",&getBalance}, - {"getTrades",&getTrades}, + {"syncTrades",&syncTrades}, {"getOpenOrders",&getOpenOrders}, {"getTicker",&getTicker}, {"placeOrder",&placeOrder}, @@ -131,11 +164,16 @@ static MethodMap methodMap ({ {"getAllPairs",&getAllPairs}, {"getFees",&getFees}, {"getInfo",&getInfo}, - {"enableDebug",&enableDebug} + {"enableDebug",&enableDebug}, + {"getBrokerInfo",&getBrokerInfo}, + {"setApiKey",&setApiKey}, + {"getApiKeyFields",&getApiKeyFields}, + {"setSettings",&setSettings}, + {"getSettings",&getSettings} }); -Value callMethod(IStockApi &api, std::string_view name, Value args) { +Value callMethod(AbstractBrokerAPI &api, std::string_view name, Value args) { try { auto iter = methodMap.find(name); if (iter == methodMap.end()) throw std::runtime_error("Method not implemented"); @@ -148,21 +186,70 @@ Value callMethod(IStockApi &api, std::string_view name, Value args) { } -void AbstractBrokerAPI::dispatch(std::istream& input, std::ostream& output, IStockApi &handler) { - +void AbstractBrokerAPI::dispatch(std::istream& input, std::ostream& output, AbstractBrokerAPI &handler) { - - while (true) { - int i = input.get(); - if (i == EOF) return; - input.putback(i); + try { Value v = Value::fromStream(input); - callMethod(handler, v[0].getString(), v[1]).toStream(output); + handler.loadKeys(); + handler.onInit(); + while (true) { + callMethod(handler, v[0].getString(), v[1]).toStream(output); + output << std::endl; + int i = input.get(); + while (i != EOF && isspace(i)) i = input.get(); + if (i == EOF) break; + input.putback(i); + v = Value::fromStream(input); + } + } catch (std::exception &e) { + Value({false, e.what()}).toStream(output); output << std::endl; } +} + +AbstractBrokerAPI::AbstractBrokerAPI(const std::string &secure_storage_path, + const Value &apiKeyFormat) +:secure_storage_path(secure_storage_path),apiKeyFormat(apiKeyFormat) { } + +void AbstractBrokerAPI::loadKeys() { + try { + std::ifstream f(secure_storage_path); + if (!f) return; + Value key = json::Value::parseBinary([&] { + return f.get(); + }, base64); + onLoadApiKey(key); + } catch (std::exception &e) { + std::cerr << e.what(); + } +} + void AbstractBrokerAPI::dispatch() { dispatch(std::cin, std::cout, *this); } + +void AbstractBrokerAPI::setApiKey(json::Value keyData) { + + onLoadApiKey(keyData); + + umask( S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); + std::ofstream f(secure_storage_path); + if (!f) throw std::runtime_error("Failed to store API key"); + keyData.serializeBinary([&](char c){f.put(c);}, compressKeys); + if (!f) throw std::runtime_error("Failed to store API key"); +} + +json::Value AbstractBrokerAPI::getApiKeyFields() const { + return apiKeyFormat; +} + +json::Value AbstractBrokerAPI::getSettings(const std::string_view & ) const { + throw std::runtime_error("unsupported"); +} + +void AbstractBrokerAPI::setSettings(json::Value) { + throw std::runtime_error("unsupported"); +} diff --git a/src/brokers/api.h b/src/brokers/api.h index bee8db0d..9b705144 100644 --- a/src/brokers/api.h +++ b/src/brokers/api.h @@ -9,11 +9,20 @@ #define SRC_BROKERS_API_H_ #include + +#include +#include "../main/apikeys.h" +#include "../main/ibrokercontrol.h" #include "../main/istockapi.h" -class AbstractBrokerAPI: public IStockApi { + + +class AbstractBrokerAPI: public IStockApi, public IApiKey, public IBrokerControl { public: + AbstractBrokerAPI(const std::string &secure_storage_path, + const json::Value &apiKeyFormat); + ///Called when mmbot is started with debug mode enabled /** * @param enable if set to true, debug mode is enabled. The broker should send more debug informations @@ -23,9 +32,25 @@ class AbstractBrokerAPI: public IStockApi { */ virtual void enable_debug(bool enable) {debug_mode = enable;} + virtual BrokerInfo getBrokerInfo() override {throw std::runtime_error("unsupported");} + + + static void dispatch(std::istream &input, std::ostream &output, AbstractBrokerAPI &handler); + - static void dispatch(std::istream &input, std::ostream &output, IStockApi &handler); + ///Called when new keys are set or loaded + virtual void onLoadApiKey(json::Value keyData) = 0; + ///Called when broker should be initialized + /** When broker starts, it cannot show any error message until it is requested for the very first time. + * This function is called before the first command is executed. If exception is throw, the exception is + * carried into caller and the broker can graceusly exit. + */ + virtual void onInit() = 0; + + virtual json::Value getSettings(const std::string_view & pairHint) const override; + + virtual void setSettings(json::Value v) override; void dispatch(); @@ -48,19 +73,22 @@ class AbstractBrokerAPI: public IStockApi { } - virtual bool isTest() const override { - return false; - } virtual void testBroker() override {} + virtual void setApiKey(json::Value keyData) override; + virtual json::Value getApiKeyFields() const override; + + virtual void loadKeys(); protected: bool debug_mode = false; - + std::string secure_storage_path; + json::Value apiKeyFormat; }; #endif /* SRC_BROKERS_API_H_ */ + diff --git a/src/coinmate/CMakeLists.txt b/src/coinmate/CMakeLists.txt index 33eb7197..e0a4691f 100644 --- a/src/coinmate/CMakeLists.txt +++ b/src/coinmate/CMakeLists.txt @@ -5,6 +5,6 @@ file(GLOB main_HDR "*.h" "*.tcc") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin/brokers/) -add_executable (coinmate ${main_SRC} ../brokers/api.cpp ) +add_executable (coinmate main.cpp proxy.cpp ../brokers/api.cpp ) target_link_libraries (coinmate LINK_PUBLIC imtjson curlpp ssl crypto curl stdc++fs pthread) install(TARGETS coinmate DESTINATION "bin/brokers") diff --git a/src/coinmate/config.cpp b/src/coinmate/config.cpp deleted file mode 100644 index b34ea3f1..00000000 --- a/src/coinmate/config.cpp +++ /dev/null @@ -1,22 +0,0 @@ -/* - * config.cpp - * - * Created on: 21. 5. 2019 - * Author: ondra - */ - - - -#include "config.h" - -Config load(const ondra_shared::IniConfig::Section& cfg) { - Config r; - - r.privKey = cfg.mandatory["private_key"].getString(); - r.pubKey = cfg.mandatory["public_key"].getString(); - r.clientid = cfg.mandatory["clientid"].getString(); - r.apiUrl= cfg.mandatory["api_url"].getString(); - return r; -} - - diff --git a/src/coinmate/config.h b/src/coinmate/config.h deleted file mode 100644 index 44a5df38..00000000 --- a/src/coinmate/config.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * config.h - * - * Created on: 21. 5. 2019 - * Author: ondra - */ - -#ifndef SRC_COINMATE_CONFIG_H_ -#define SRC_COINMATE_CONFIG_H_ -#include "../shared/ini_config.h" - - - -struct Config { - std::string apiUrl; - std::string privKey; - std::string pubKey; - std::string clientid; - - }; - -Config load(const ondra_shared::IniConfig::Section &cfg); - -#endif /* SRC_COINMATE_CONFIG_H_ */ diff --git a/src/coinmate/main.cpp b/src/coinmate/main.cpp index 3fd0d3f9..48194851 100644 --- a/src/coinmate/main.cpp +++ b/src/coinmate/main.cpp @@ -9,7 +9,6 @@ #include #include "../imtjson/src/imtjson/operations.h" -#include "config.h" #include "proxy.h" #include "../main/istockapi.h" #include "../shared/linear_map.h" @@ -20,13 +19,26 @@ using namespace json; class Interface: public AbstractBrokerAPI { public: - Proxy &cm; - - Interface(Proxy &cm):cm(cm) {} + Proxy cm; + + Interface(const std::string &path) + :AbstractBrokerAPI(path,{Object + ("name","pubKey") + ("label","Public key") + ("type", "string"), + Object + ("name","privKey") + ("label","Private key") + ("type", "string"), + Object + ("name","clientId") + ("label","Client ID") + ("type", "string") + }) {} virtual double getBalance(const std::string_view & symb) override; - virtual TradeHistory getTrades(json::Value lastId, std::uintptr_t fromTime, const std::string_view & pair) override; + virtual TradesSync syncTrades(json::Value lastId, const std::string_view & pair) override; virtual Orders getOpenOrders(const std::string_view & par) override; virtual Ticker getTicker(const std::string_view & piar) override; virtual json::Value placeOrder(const std::string_view & pair, @@ -40,15 +52,15 @@ class Interface: public AbstractBrokerAPI { virtual double getFees(const std::string_view &pair) override ; virtual std::vector getAllPairs() override ; virtual void enable_debug(bool enable) override {cm.debug = enable;} - + virtual BrokerInfo getBrokerInfo() override; + virtual void onLoadApiKey(json::Value keyData) override; + virtual void onInit() override {} Value balanceCache; Value orderCache; - Array trades; - bool fetchTrades = true; - Value firstId; + Value tradeCache; Value all_pairs; - std::size_t lastFrom = -1; + bool fetch_trades = true; struct FeeInfo { double fee; @@ -70,95 +82,93 @@ class Interface: public AbstractBrokerAPI { return balanceCache[symb]["balance"].getNumber(); } - Value Interface::readTradesPerPartes(Value lastId, Value fromTime) { - Array result; - bool rep; - do { +inline void Interface::onLoadApiKey(json::Value keyData) { + cm.privKey = keyData["privKey"].getString(); + cm.pubKey = keyData["pubKey"].getString(); + cm.clientid = keyData["clientId"].getString(); + balanceCache = Value(); + orderCache = Value(); + tradeCache = Value(); + all_pairs = Value(); + fetch_trades = true; + +} + +static bool greaterThanId(Value tx, Value id) { + return Value::compare(id, tx["transactionId"]) < 0; +} +static int sortById(Value tx1, Value tx2) { + return Value::compare(tx1["transactionId"],tx2["transactionId"]); +} + +Interface::TradesSync Interface::syncTrades(json::Value lastId, const std::string_view & pair) { + + if (!tradeCache.hasValue()) { json::Value args (json::object, { - json::Value("lastId",lastId), - json::Value("sort", "ASC"), - json::Value("timestampFrom",fromTime), - json::Value("limit",1000) + json::Value("limit",10) }); + Value c = cm.request(Proxy::POST, "tradeHistory", args).sort(sortById); - Value res = cm.request(Proxy::POST, "tradeHistory", args); - result.addSet(res); - rep = !res.empty(); - if (rep) lastId = result[result.size()-1]["transactionId"]; + if (c.empty()) return TradesSync{ {}, nullptr }; - } while (rep); - return result; - } + tradeCache = c; + fetch_trades = false; + } -Interface::TradeHistory Interface::getTrades(json::Value lastId, std::uintptr_t fromTime, const std::string_view & pair) { + if (lastId.hasValue() && greaterThanId(tradeCache[0], lastId)) { - if (!lastId.defined() && fromTime < lastFrom) { - trades.clear(); - fetchTrades = true; + json::Value args (json::object, { + json::Value("sort", "ASC"), + json::Value("lastId",lastId), + json::Value("limit",1000) + }); + tradeCache = cm.request(Proxy::POST, "tradeHistory", args); + fetch_trades = false; } + Value l = tradeCache[tradeCache.size()-1]["transactionId"]; - if (fetchTrades) { - auto fetchLastId = lastId; - auto fetchFromTime = fromTime; - if (!trades.empty()) { - fetchLastId = trades[trades.size()-1]["transactionId"]; - } else { - firstId = lastId; - } -// std::cerr << "[debug] Fetch trades from: " << fetchLastId << std::endl; + if (fetch_trades) { - Value res = readTradesPerPartes(fetchLastId, fetchFromTime); - trades.addSet(res); - fetchTrades = false; + json::Value args (json::object, { + json::Value("sort", "ASC"), + json::Value("lastId",l), + json::Value("limit",1000) + }); + tradeCache = tradeCache.merge(cm.request(Proxy::POST, "tradeHistory", args)); + fetch_trades = false; + l = tradeCache[tradeCache.size()-1]["transactionId"]; } - auto start = lastId.defined()? - (lastId == firstId?trades.begin(): - std::find_if(trades.begin(), trades.end(), [&](const Value &v) { - return v["transactionId"] == lastId; - })):trades.begin(); - - Value result; - if (start == trades.end() && lastId.defined()) { - trades.clear(); - Value res = readTradesPerPartes(lastId,fromTime); - trades.addSet(res); - start = trades.begin(); - firstId = lastId; - } - if (start == trades.end()) return {}; + if (lastId.hasValue()) { + + Value trades = tradeCache.filter([&](Value v) { + return v["currencyPair"] == pair && greaterThanId(v, lastId); + }); - lastFrom = fromTime?fromTime:trades[0]["createdTimestamp"].getUInt(); + return TradesSync{ + mapJSON (trades, [&](Value x){ + double mlt = x["type"].getString() == "SELL"?-1:1; + double price = x["price"].getNumber(); + double fee = x["fee"].getNumber(); + double size = x["amount"].getNumber()*mlt; + double eff_price = price + fee/size; + return Trade { + x["transactionId"], + x["createdTimestamp"].getUInt(), + size, + price, + size, + eff_price + }; + }), l}; - if ((*start)["transactionId"] == lastId) - ++start; - Array part; - while (start != trades.end()) { - Value v = *start; - if (v["currencyPair"] == pair) - part.push_back(v); - ++start; + } else { + return TradesSync{ {}, l}; } - result = part; - - return mapJSON (result, [&](Value x){ - double mlt = x["type"].getString() == "SELL"?-1:1; - double price = x["price"].getNumber(); - double fee = x["fee"].getNumber(); - double size = x["amount"].getNumber()*mlt; - double eff_price = price + fee/size; - return Trade { - x["transactionId"], - x["createdTimestamp"].getUInt(), - size, - price, - size, - eff_price - }; - }); + } @@ -241,7 +251,7 @@ json::Value Interface::placeOrder(const std::string_view & pair, bool Interface::reset() { balanceCache = Value(); orderCache = Value(); - fetchTrades = true; + fetch_trades = true; return true; } @@ -255,7 +265,7 @@ std::vector Interface::getAllPairs() { } double Interface::getFees(const std::string_view &pair) { - if (cm.hasKey) { + if (cm.hasKey()) { auto now = std::chrono::system_clock::now(); auto iter = feeMap.find(StrViewA(pair)); if (iter == feeMap.end() || iter->second.expiration < now) { @@ -291,29 +301,63 @@ Interface::MarketInfo Interface::getMarketInfo(const std::string_view &pair) { getFees(pair) }; } + +Interface::BrokerInfo Interface::getBrokerInfo() { + return BrokerInfo{ + cm.hasKey(), + "coinmate", + "Coinmate", + "https://www.coinmate.io/", + "1.0", + R"mit(Copyright (c) 2019 Ondřej Novák + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE.)mit", +"iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAD1BMVEUAAAAsYnlHi6FfqcNXwdo8" +"rDwaAAAAAXRSTlMAQObYZgAAAqpJREFUeAHtmle2IyEQQ5+uvP8tT+ozNQF3OYBeRP9Gt0LjOsDL" +"1gfW1tbW1hZCEm9gDNi2Lz9k2wZeiUOCw3iQAYXtr7uXbEcR1LiXcgzicqcSCBLuXbMI1+z9t0YC" +"ov42SKr0AHYwCfK/5tIdLWovJMAn7i2D0WqC0b5HYGERfO8uA07kgMa+2zG8jEB6tGKHzMtbSPQ5" +"yEv4axH0jUCQQAC6SRAN0+4Jsp8Cv9Zv/kTSbeB2ecnhFIg+PpElEL4xfohYESRs/zOhtG2AwhOy" +"jdQQWDH7HqGKQNC+ZJRPQTuh2zrdj4j4D1O5yaZA/tuLX26A3RDURLnU33CSGJNLgZq/WHE6ALBq" +"OxTt3xs+6UStakPc51I++Rh//25RAaw+RWggX9IEumMZ2VWe5d+B7llFXMVjQRPUInr+t16RAM38" +"eD4BJkrfSEwtMd+F8tRgM78VMbcCiwCsDwIQ+A79xgB6cwA2wIcECHyGvB2A8zth8jv0PMBcDQp/" +"ugmsiQJa6ZHwFsCKmdCK9mAj+empVJ4HuG+6l1aVr1mmWQdb09VrhNurGOGLTaQCFUlDUKeGylSg" +"CAphsD9khSpQNsPtqQT2cDoQSEARFMQhd+dkono3c0rWnxQGDgrx7Yv7+FHpqX97rrNO4Ltfb7jY" +"Yof17b06Dl4Y4HKv4GP3Bc3yrtg7f+Uvrbq9W9FrO93cuXmbe0OWNIBaANum958sAD29ijBzcyue" +"zZ+W+R8EM/5aMIbqTf37VVr7yQ+geRKU828I9LC9WTWId6s116po8Us6uD/6xBTaV0Iw0K6Q5OE1" +"I/oHQxrfNJrkg1KbX5L4Jbu/xA4/Ku2H4whBL6O3etZcs3FG3GRwyL4kcGNP0r0EtseuT7oPkgB8" +"qPaF15YOTVtvbW1tbW1tbX0HniAh/KR9ZWcAAAAASUVORK5CYII=" + }; +} + + int main(int argc, char **argv) { using namespace json; if (argc < 2) { - std::cerr << "No config given, terminated" << std::endl; + std::cerr << "Required one argument" << std::endl; return 1; } - try { - - ondra_shared::IniConfig ini; - - ini.load(argv[1]); - - Config cfg = load(ini["api"]); - Proxy coinmate(cfg); - Interface ifc(coinmate); - - ifc.dispatch(); - - } catch (std::exception &e) { - std::cerr << "Error: " << e.what() << std::endl; - return 2; - } + Interface ifc(argv[1]); + ifc.dispatch(); } diff --git a/src/coinmate/proxy.cpp b/src/coinmate/proxy.cpp index 50de7d05..366a20b8 100644 --- a/src/coinmate/proxy.cpp +++ b/src/coinmate/proxy.cpp @@ -23,23 +23,27 @@ using ondra_shared::logDebug; static constexpr std::uint64_t start_time = 1557858896532; -Proxy::Proxy(Config config):config(config) { +Proxy::Proxy() { + apiUrl = "https://coinmate.io/api/"; auto now = std::chrono::system_clock::now(); - std::size_t init_time = std::chrono::duration_cast(now.time_since_epoch()).count() - start_time; + std::uint64_t init_time = std::chrono::duration_cast(now.time_since_epoch()).count() - start_time; nonce = init_time * 100; - hasKey = !config.privKey.empty() - && !config.pubKey.empty() - && !config.clientid.empty(); +} + +bool Proxy::hasKey() const { +return !privKey.empty() + && !pubKey.empty() + && !clientid.empty(); } std::pair Proxy::createSignature() { std::ostringstream msgbuff; - msgbuff<(msg.data()), msg.size(), dbuff, &dbuff_size); std::ostringstream digest; @@ -54,7 +58,7 @@ json::Value Proxy::request(Method method, std::string path, json::Value data) { std::string d = createQuery(data); std::ostringstream response; - std::string url = config.apiUrl+path; + std::string url = apiUrl+path; std::istringstream src(d); /* auto reader = [&](char *buffer, size_t size, size_t nitems) { @@ -63,7 +67,7 @@ json::Value Proxy::request(Method method, std::string path, json::Value data) { return cnt; };*/ - if (!hasKey && method != GET) + if (!hasKey() && method != GET) throw std::runtime_error("This operation requires valid API key"); const char *m = ""; @@ -112,10 +116,10 @@ json::Value Proxy::request(Method method, std::string path, json::Value data) { std::string Proxy::createQuery(json::Value data) { std::ostringstream out; - if (hasKey) { + if (hasKey()) { auto sig = createSignature(); - out << "clientId=" << config.clientid - << "&publicKey=" << config.pubKey + out << "clientId=" << clientid + << "&publicKey=" << pubKey << "&nonce=" << sig.second << "&signature=" << sig.first; } diff --git a/src/coinmate/proxy.h b/src/coinmate/proxy.h index 16d5d664..d18dc769 100644 --- a/src/coinmate/proxy.h +++ b/src/coinmate/proxy.h @@ -10,14 +10,17 @@ #include #include -#include "config.h" class Proxy { public: - Proxy(Config config); + Proxy(); + + std::string apiUrl; + std::string privKey; + std::string pubKey; + std::string clientid; - Config config; cURLpp::Easy curl_handle; std::uint64_t nonce; @@ -28,7 +31,7 @@ class Proxy { GET, POST, PUT }; - bool hasKey; + bool hasKey() const;; bool debug = false; json::Value request(Method method, std::string path, json::Value data); diff --git a/src/deribit/CMakeLists.txt b/src/deribit/CMakeLists.txt index 84bba364..70af0c9d 100644 --- a/src/deribit/CMakeLists.txt +++ b/src/deribit/CMakeLists.txt @@ -4,6 +4,6 @@ add_compile_options(-std=c++17) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin/brokers/) -add_executable (deribit main.cpp config.cpp proxy.cpp ../brokers/api.cpp ../brokers/orderdatadb.cpp ) +add_executable (deribit main.cpp proxy.cpp ../brokers/api.cpp ) target_link_libraries (deribit LINK_PUBLIC imtjson curlpp ssl crypto curl stdc++fs pthread) install(TARGETS deribit DESTINATION "bin/brokers") diff --git a/src/deribit/config.cpp b/src/deribit/config.cpp deleted file mode 100644 index 95e28434..00000000 --- a/src/deribit/config.cpp +++ /dev/null @@ -1,22 +0,0 @@ -/* - * config.cpp - * - * Created on: 21. 5. 2019 - * Author: ondra - */ - - - -#include "config.h" - -Config load(const ondra_shared::IniConfig::Section& cfg) { - Config r; - - r.privKey = cfg.mandatory["secret"].getString(); - r.pubKey = cfg.mandatory["key"].getString(); - r.apiUrl = cfg.mandatory["url"].getString(); - r.scopes = cfg.mandatory["scope"].getString(); - return r; -} - - diff --git a/src/deribit/config.h b/src/deribit/config.h deleted file mode 100644 index e49c0138..00000000 --- a/src/deribit/config.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * config.h - * - * Created on: 21. 5. 2019 - * Author: ondra - */ - -#ifndef SRC_COINMATE_CONFIG_H_ -#define SRC_COINMATE_CONFIG_H_ -#include "../shared/ini_config.h" - - - -struct Config { - std::string apiUrl; - std::string privKey; - std::string pubKey; - std::string scopes; - - }; - -Config load(const ondra_shared::IniConfig::Section &cfg); - -#endif /* SRC_COINMATE_CONFIG_H_ */ diff --git a/src/deribit/main.cpp b/src/deribit/main.cpp index 0ff598de..114fd4b2 100644 --- a/src/deribit/main.cpp +++ b/src/deribit/main.cpp @@ -11,7 +11,6 @@ #include #include #include "shared/toString.h" -#include "config.h" #include "proxy.h" #include "../main/istockapi.h" #include @@ -31,13 +30,35 @@ using namespace json; class Interface: public AbstractBrokerAPI { public: - Proxy &px; - - Interface(Proxy &cm):px(cm) {} + Proxy px; + + Interface(const std::string &path):AbstractBrokerAPI(path, { + Object + ("name","key") + ("label","Key") + ("type","string"), + Object + ("name","secret") + ("label","Secret") + ("type","string"), + Object + ("name","scopes") + ("label","Scopes") + ("type","string") + ("default","session:apiconsole"), + Object + ("name","server") + ("label","Server") + ("type","enum") + ("options",Object + ("main","www.deribit.com") + ("test","test.deribit.com")) + ("default","main")}) + {} virtual double getBalance(const std::string_view & symb) override; - virtual TradeHistory getTrades(json::Value lastId, std::uintptr_t fromTime, const std::string_view & pair) override; + virtual TradesSync syncTrades(json::Value lastId, const std::string_view & pair) override; virtual Orders getOpenOrders(const std::string_view & par)override; virtual Ticker getTicker(const std::string_view & piar)override; virtual json::Value placeOrder(const std::string_view & pair, @@ -51,6 +72,9 @@ class Interface: public AbstractBrokerAPI { virtual double getFees(const std::string_view &pair)override; virtual std::vector getAllPairs()override; virtual void enable_debug(bool enable) override; + virtual BrokerInfo getBrokerInfo() override; + virtual void onLoadApiKey(json::Value keyData) override; + virtual void onInit() override; ondra_shared::linear_map > tick_cache; @@ -66,29 +90,13 @@ int main(int argc, char **argv) { using namespace json; if (argc < 2) { - std::cerr << "No config given, terminated" << std::endl; + std::cerr << "Requires a signle parametr" << std::endl; return 1; } - try { - - ondra_shared::IniConfig ini; - - ini.load(argv[1]); - - Config cfg = load(ini["api"]); - Proxy proxy(cfg); - - - Interface ifc(proxy); - - ifc.dispatch(); - + Interface ifc(argv[1]); + ifc.dispatch(); - } catch (std::exception &e) { - std::cerr << "Error: " << e.what() << std::endl; - return 2; - } } inline double Interface::getBalance(const std::string_view &symb) { @@ -101,42 +109,60 @@ inline double Interface::getBalance(const std::string_view &symb) { } else { auto response = px.request("private/get_account_summary",Object ("currency",symb),true); - return response["balance"].getNumber(); + return response["available_funds"].getNumber(); } } -inline Interface::TradeHistory Interface::getTrades(json::Value lastId, - std::uintptr_t fromTime, const std::string_view &pair) { +inline Interface::TradesSync Interface::syncTrades(json::Value lastId, const std::string_view &pair) { auto resp = px.request("private/get_user_trades_by_instrument",Object ("instrument_name",pair) ("sorting","asc") - ("start_seq", lastId),true); + ("count", 1000) + ("include_old", true) + ("start_seq", lastId.hasValue()?lastId:Value()),true); + resp = resp["trades"]; - if (resp[0]["trade_seq"] == lastId) { - resp = resp.slice(1); - } - return mapJSON(resp, [&](Value itm){ - double amount = itm["amount"].getNumber(); - double price = 1.0/itm["price"].getNumber(); - auto dir = itm["direction"].getString(); - if (dir == "buy") amount = -amount; - double fee = itm["fee"].getNumber(); - double eff_price = price; - if (fee > 0) { - eff_price += -price/amount; + if (!lastId.hasValue()) { + + if (resp.empty()) { + return TradesSync{ {}, Value(nullptr) }; + } else { + return TradesSync{ {}, resp[resp.size()-1]["trade_seq"]}; } - return Trade{ - itm["trade_seq"], - itm["timestamp"].getUInt(), - amount, - price, - amount, - eff_price - }; - },TradeHistory()); + } else { + + + if (resp[0]["trade_seq"] == lastId) { + resp = resp.slice(1); + } + + + auto trades = mapJSON(resp, [&](Value itm){ + double amount = itm["amount"].getNumber(); + double price = 1.0/itm["price"].getNumber(); + auto dir = itm["direction"].getString(); + if (dir == "buy") amount = -amount; + double fee = itm["fee"].getNumber(); + double eff_price = price; + if (fee > 0) { + eff_price += -price/amount; + } + lastId = itm["trade_seq"]; + return Trade{ + itm["trade_seq"], + itm["timestamp"].getUInt(), + amount, + price, + amount, + eff_price + }; + },TradeHistory()); + return TradesSync { trades, lastId }; + + } } inline Interface::Orders Interface::getOpenOrders(const std::string_view &pair) { @@ -278,7 +304,8 @@ inline Interface::MarketInfo Interface::getMarketInfo(const std::string_view &pa IStockApi::currency, leverage, true, - "USD" + "USD", + px.testnet }; @@ -292,6 +319,44 @@ void Interface::enable_debug(bool enable) { px.debug = enable; } +Interface::BrokerInfo Interface::getBrokerInfo() { + return BrokerInfo{ + px.hasKey(), + "deribit", + "Deribit", + "https://www.deribit.com/", + "1.0", + R"mit(Copyright (c) 2019 Ondřej Novák + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE.)mit", +"iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAABlBMVEUAAAAtrpq1rIdBAAAAAXRS" +"TlMAQObYZgAAANRJREFUeAHt2jcSwzAMBVHs/S9t18r5cwa7rYNeJYkkysxWYt7BrwkQIECAAAEC" +"BAh47fpw/Gsn47XiAIA4AIgDIA6AOADiAOIA4gCIA4gDiAOIA4gDiAMQQBzAYIB33jbZ7APAvzhg" +"i/ARoCoOWBV8Bqg4YE3wHaDigBoTwIeAigNKQBiwIhAgQIAAAT6MOgHoDui+LiAKyK8NWanL/gBZ" +"ACQBbOVGZe+9Yrfru58ZdT837H523Hx+oPcMSe85oqogIDlL5kCjAAECBAgQIECAmR3sB12WHA4r" +"Mg73AAAAAElFTkSuQmCC" + }; +} + inline std::vector Interface::getAllPairs() { std::vector resp; auto currencies = px.request("public/get_currencies", Object(),false); @@ -307,3 +372,14 @@ inline std::vector Interface::getAllPairs() { } return resp; } + +inline void Interface::onLoadApiKey(json::Value keyData) { + px.setTestnet(keyData["server"].getString() == "test"); + px.privKey = keyData["secret"].getString(); + px.pubKey = keyData["key"].getString(); + px.scopes = keyData["scopes"].getString(); +} + +inline void Interface::onInit() { + //empty +} diff --git a/src/deribit/proxy.cpp b/src/deribit/proxy.cpp index 7c57b0d6..f60c5051 100644 --- a/src/deribit/proxy.cpp +++ b/src/deribit/proxy.cpp @@ -25,14 +25,18 @@ using ondra_shared::logDebug; -Proxy::Proxy(Config config):config(config) { - hasKey = !config.privKey.empty() && !config.pubKey.empty(); +Proxy::Proxy() { + setTestnet(false); } void Proxy::setTimeDiff(std::intptr_t t) { this->time_diff = t; } +bool Proxy::hasKey() const { + return !privKey.empty() && !pubKey.empty() && !scopes.empty(); +} + json::Value Proxy::request(std::string_view method, json::Value params, bool auth) { const std::string *tk(auth?&getAccessToken():nullptr); @@ -64,7 +68,7 @@ json::Value Proxy::request(std::string_view method, json::Value params, bool aut curl_handle.setOpt(new cURLpp::Options::HttpHeader(headers)); } - curl_handle.setOpt(new cURLpp::Options::Url(config.apiUrl)); + curl_handle.setOpt(new cURLpp::Options::Url(apiUrl)); curl_handle.setOpt(new cURLpp::Options::WriteStream(&response)); curl_handle.perform(); @@ -128,9 +132,9 @@ const std::string &Proxy::getAccessToken() { auto resp = request("public/auth",json::Object ("grant_type","client_credentials") /* Don't know, but client_signature doesn't work for me*/ - ("client_id",config.pubKey) - ("client_secret",config.privKey) - ("scope",config.scopes) + ("client_id",pubKey) + ("client_secret",privKey) + ("scope",scopes) /* ("timestamp",time) ("signature",signature) ("nonce",nonce)*/, false); @@ -141,12 +145,14 @@ const std::string &Proxy::getAccessToken() { return auth_token; } -std::uintptr_t Proxy::now() { +std::uint64_t Proxy::now() { return std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ).count(); } - - +void Proxy::setTestnet(bool testnet) { + apiUrl = testnet?"https://test.deribit.com/api/v2":"https://www.deribit.com/api/v2"; + this->testnet = testnet; +} diff --git a/src/deribit/proxy.h b/src/deribit/proxy.h index 7de304c1..b7e1d6c5 100644 --- a/src/deribit/proxy.h +++ b/src/deribit/proxy.h @@ -11,15 +11,18 @@ #include #include -#include "config.h" class Proxy { public: - Proxy(Config config); + Proxy(); - Config config; + std::string apiUrl; + std::string privKey; + std::string pubKey; + std::string scopes; cURLpp::Easy curl_handle; + bool testnet; ///Send request /** @@ -32,12 +35,13 @@ class Proxy { json::Value request(std::string_view method, json::Value params, bool auth); const std::string &getAccessToken(); - bool hasKey; + bool hasKey() const; bool debug = false; void setTimeDiff(std::intptr_t t); - static std::uintptr_t now(); + static std::uint64_t now(); + void setTestnet(bool testnet); private: diff --git a/src/imtjson b/src/imtjson index a4ece393..79f5e32c 160000 --- a/src/imtjson +++ b/src/imtjson @@ -1 +1 @@ -Subproject commit a4ece393689aca74174ee51cb62d0393682a0bef +Subproject commit 79f5e32ce2587f6d4c5e4a160648eb01d254cb19 diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt index d37e02e8..b0df57bf 100644 --- a/src/main/CMakeLists.txt +++ b/src/main/CMakeLists.txt @@ -3,18 +3,22 @@ add_compile_options(-std=c++17) add_executable (mmbot abstractExtern.cpp + authmapper.cpp ext_stockapi.cpp mtrader.cpp - spread_calc.cpp - calculator.cpp istockapi.cpp - ordergen.cpp storage.cpp emulator.cpp main.cpp report.cpp - backtest_broker.cpp - backtest.cpp + webcfg.cpp + traders.cpp + strategy.cpp + strategy_halfhalf.cpp + strategy_plfrompos.cpp + strategy_keepvalue.cpp + localdailyperfmod.cpp + extdailyperfmod.cpp ) target_link_libraries (mmbot LINK_PUBLIC simpleServer imtjson curlpp ssl crypto curl stdc++fs pthread) install(TARGETS mmbot DESTINATION "bin") diff --git a/src/main/abstractExtern.cpp b/src/main/abstractExtern.cpp index 3bb13b4e..5e0a27f3 100644 --- a/src/main/abstractExtern.cpp +++ b/src/main/abstractExtern.cpp @@ -26,6 +26,7 @@ #include +#include "../shared/linux_waitpid.h" #include "istockapi.h" const int AbstractExtern::invval = -1; @@ -90,6 +91,13 @@ static void report_error(const char *desc) { throw std::runtime_error(buff.str()); } +static void report_timeout(const char *desc) { + std::ostringstream buff; + buff << "TIMEOUT while '" << desc << '"'; + throw std::runtime_error(buff.str()); + +} + AbstractExtern::Pipe AbstractExtern::makePipe() { int tmp[2]; @@ -103,6 +111,7 @@ AbstractExtern::Pipe AbstractExtern::makePipe() { void AbstractExtern::spawn() { + Sync _(lock); using ondra_shared::Handle; int status; @@ -111,7 +120,7 @@ void AbstractExtern::spawn() { { - log.progress("Connecting to market: cmdline='$1', workdir='$2'", cmdline, workingDir); + log.progress("Connecting to broker: cmdline='$1', workdir='$2'", cmdline, workingDir); Pipe proc_input (makePipe()); Pipe proc_output (makePipe()); @@ -173,25 +182,27 @@ void AbstractExtern::handleClose(int fd) { } -static int termThenKill(int id) { - int status = 0; - for (int i = 1; i < 20; i++) { - int r = ::waitpid(id,&status,WNOHANG); - if (r == -1) return -1; - if (r == id) return status; - ::kill(id, SIGTERM); - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - } - ::kill(id, SIGKILL); - return -1; - -} - void AbstractExtern::kill() { + Sync _(lock); if (chldid != -1) { - ::kill(chldid,SIGTERM); - int status = termThenKill(chldid); - log.note("terminated with status: $1", status); + + ondra_shared::WaitPid wpid(chldid); + extin.close(); + if (!wpid.wait_for(std::chrono::seconds(3))) { + ::kill(chldid, SIGTERM); + if (!wpid.wait_for(std::chrono::seconds(10))) { + ::kill(chldid, SIGKILL); + if (!wpid.wait_for(std::chrono::seconds(10))) { + log.error("Unable to terminate broker! (TIMEOUT waiting on SIGKILL)"); + } + } + } + int status = wpid.getExitCode(); + if (WIFSIGNALED(status)) { + log.note("Broker process disconnected because signal: $1", WTERMSIG(status)); + } else { + log.note("Broker process disconnected. Exit code : $1", WEXITSTATUS(status)); + } chldid = -1; } } @@ -274,12 +285,18 @@ json::Value AbstractExtern::readJSON(FD& fd) { void AbstractExtern::preload() { + Sync _(lock); if (chldid == -1) { spawn(); } } +void AbstractExtern::stop() { + kill(); +} + json::Value AbstractExtern::jsonExchange(json::Value request) { + Sync _(lock); std::string z; std::string lastStdErr; @@ -302,7 +319,7 @@ json::Value AbstractExtern::jsonExchange(json::Value request) { fds[1].events = POLLIN; fds[1].revents = 0; int r = poll(fds,2,30000); - if (r == 0) report_error("timeout"); + if (r == 0) report_timeout("poll"); if (r < 0) report_error("poll"); if (fds[1].revents) { Reader errrd(exterr); @@ -344,6 +361,7 @@ json::Value AbstractExtern::jsonExchange(json::Value request) { } json::Value AbstractExtern::jsonRequestExchange(json::String name, json::Value args) { + Sync _(lock); auto resp = jsonExchange({name, args}); if (resp[0].getBool() == true) { auto result = resp[1]; diff --git a/src/main/abstractExtern.h b/src/main/abstractExtern.h index e440b6f7..8861a9b9 100644 --- a/src/main/abstractExtern.h +++ b/src/main/abstractExtern.h @@ -7,6 +7,8 @@ #ifndef SRC_MAIN_ABSTRACTEXTERN_H_ #define SRC_MAIN_ABSTRACTEXTERN_H_ +#include + #include #include #include @@ -21,6 +23,7 @@ class AbstractExtern { void preload(); virtual void onConnect() {} + void stop(); protected: static const int invval; @@ -42,6 +45,8 @@ class AbstractExtern { std::string workingDir; ondra_shared::LogObject log; + mutable std::recursive_mutex lock; + using Sync = std::unique_lock; class Reader; diff --git a/src/main/apikeys.h b/src/main/apikeys.h new file mode 100644 index 00000000..c20b1e5d --- /dev/null +++ b/src/main/apikeys.h @@ -0,0 +1,60 @@ +/* + * apikeys.h + * + * Created on: 14. 10. 2019 + * Author: ondra + */ + +#ifndef SRC_MAIN_APIKEYS_H_ +#define SRC_MAIN_APIKEYS_H_ +#include + + +class IApiKey { +public: + + ///sets api keys to the broker + /** This method is called from WebAdmin when user sets new API keys. The broker should + * store the new keys in secure storage. It also activates the keys. The broker also must + * load the keys everytime it starts. + * + * @param keyData JSON object contains set of fields + * @exception any + */ + virtual void setApiKey(json::Value keyData) = 0; + + + ///Retrieves list of fields with format of each field. + /** + * This function is called by WebAdmin to retrieve list of fields for creation of the form to let user to fill + * the secure data into it. The function must return an JSON array where each item contains an object with + * following items + * + * @code + * [ + * { + "name":"string" + * "type":"string|number|boolean|enum" + * "label":"string", + * "min":nn - for "number" the minimum + * "max":nn - for "number" the maximum + * "step":nn - for "number" change step + * + * "options": { - for "enum" + * "value":"caption" + * .... + * } + * "default":"string" - default value + * } + *] + * @endcode + * + * @return + */ + virtual json::Value getApiKeyFields() const = 0; + + +}; + + +#endif /* SRC_MAIN_APIKEYS_H_ */ diff --git a/src/main/authmapper.cpp b/src/main/authmapper.cpp new file mode 100644 index 00000000..f500d9eb --- /dev/null +++ b/src/main/authmapper.cpp @@ -0,0 +1,124 @@ +/* + * authmapper.cpp + * + * Created on: 17. 9. 2019 + * Author: ondra + */ + +#include "authmapper.h" +#include +#include +#include +#include +#include + +bool AuthUserList::findUser(const std::string &user, const std::string &pwdhash) const { + Sync _(lock); + auto iter = users.find(user); + return iter != users.end() && pwdhash == iter->second; +} + + +void AuthUserList::setUsers(std::vector > &&users) { + Sync _(lock); + if (!users.empty()) { + users.insert(users.end(), cfgusers.begin(), cfgusers.end()); + + } + this->users.swap(users); + +} + +void AuthUserList::setCfgUsers(std::vector > &&users) { + Sync _(lock); + this->cfgusers.swap(users); +} + +bool AuthUserList::empty() const { + Sync _(lock); + return users.empty(); +} + +std::string AuthUserList::hashPwd(const std::string& user, + const std::string& pwd) { + + unsigned char result[256]; + unsigned int result_len; + HMAC(EVP_sha384(),pwd.data(),pwd.length(), + reinterpret_cast(user.data()), user.length(), + result,&result_len); + std::string out; + json::base64url->encodeBinaryValue(json::BinaryView(result,result_len),[&](json::StrViewA x){ + out.append(x.data,x.length); + }); + return out; +} + +AuthUserList::LoginPwd AuthUserList::decodeBasicAuth(const json::StrViewA auth) { + json::Value v = json::base64->decodeBinaryValue(auth); + json::StrViewA dec = v.getString(); + auto splt = dec.split(":",2); + json::StrViewA user = splt(); + json::StrViewA pwd = splt(); + std::string pwdhash = hashPwd(user,pwd); + return {std::string(user), pwdhash}; +} + +std::vector AuthUserList::decodeMultipleBasicAuth( + const json::StrViewA auth) { + + std::vector res; + + auto splt = auth.split(" "); + while (!!splt) { + json::StrViewA x = splt(); + if (!x.empty()) { + res.push_back(decodeBasicAuth(x)); + } + } + return res; +} + +AuthMapper::AuthMapper( std::string realm, ondra_shared::RefCntPtr users):users(users),realm(realm) {} + AuthMapper &AuthMapper::operator >>= (simpleServer::HTTPHandler &&hndl) { + handler = std::move(hndl); + return *this; + } + +bool AuthMapper::checkAuth(const simpleServer::HTTPRequest &req) const { + using namespace ondra_shared; + if (!users->empty()) { + auto hdr = req["Authorization"]; + auto hdr_splt = hdr.split(" "); + StrViewA type = hdr_splt(); + StrViewA cred = hdr_splt(); + if (type != "Basic") { + genError(req); + return false; + } + auto credobj = AuthUserList::decodeBasicAuth(cred); + if (!users->findUser(credobj.first, credobj.second)) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + genError(req); + return false; + } + } + return true; +} + + +void AuthMapper::operator()(const simpleServer::HTTPRequest &req) const { + if (checkAuth(req)) handler(req); +} +bool AuthMapper::operator()(const simpleServer::HTTPRequest &req, const ondra_shared::StrViewA &) const { + if (checkAuth(req)) handler(req); + return true; +} + +void AuthMapper::genError(simpleServer::HTTPRequest req) const { + req.sendResponse(simpleServer::HTTPResponse(401) + .contentType("text/html") + ("WWW-Authenticate","Basic realm=\""+realm+"\""), + "

401 Unauthorized

" + ); +} diff --git a/src/main/authmapper.h b/src/main/authmapper.h new file mode 100644 index 00000000..518093f0 --- /dev/null +++ b/src/main/authmapper.h @@ -0,0 +1,66 @@ +/* + * authmapper.h + * + * Created on: 21. 7. 2019 + * Author: ondra + */ + +#ifndef SRC_MAIN_AUTHMAPPER_H_ +#define SRC_MAIN_AUTHMAPPER_H_ +#include +#include +#include +#include +#include + +#include "../server/src/simpleServer/http_parser.h" +#include "../shared/linear_map.h" + +class AuthUserList: public ondra_shared::RefCntObj { +public: + using Sync = std::unique_lock; + using UserMap = ondra_shared::linear_map; + using LoginPwd = UserMap::value_type; + + bool findUser(const std::string &user, const std::string &pwdhash) const; + + static std::string hashPwd(const std::string &user, const std::string &pwd); + static LoginPwd decodeBasicAuth(const json::StrViewA auth); + static std::vector decodeMultipleBasicAuth(const json::StrViewA auth); + + void setUsers(std::vector > &&users); + void setCfgUsers(std::vector > &&users); + bool empty() const; + +protected: + mutable std::recursive_mutex lock; + //standard user table + UserMap users; + //user table from config + UserMap cfgusers; +}; + + +class AuthMapper { +public: + + AuthMapper( std::string realm, ondra_shared::RefCntPtr users); + AuthMapper &operator >>= (simpleServer::HTTPHandler &&hndl); + bool checkAuth(const simpleServer::HTTPRequest &req) const; + void operator()(const simpleServer::HTTPRequest &req) const; + bool operator()(const simpleServer::HTTPRequest &req, const ondra_shared::StrViewA &) const; + void genError(simpleServer::HTTPRequest req) const; + ondra_shared::RefCntPtr getUsers() const {return users;} + +protected: +// AuthMapper( std::string users, std::string realm, simpleServer::HTTPHandler &&handler):users(users), handler(std::move(handler)) {} + ondra_shared::RefCntPtr users; + std::string realm; + simpleServer::HTTPHandler handler; +}; + + + + + +#endif /* SRC_MAIN_AUTHMAPPER_H_ */ diff --git a/src/main/backtest.cpp b/src/main/backtest.cpp deleted file mode 100644 index a9c52c64..00000000 --- a/src/main/backtest.cpp +++ /dev/null @@ -1,225 +0,0 @@ -/* - * backtest.cpp - * - * Created on: 22. 8. 2019 - * Author: ondra - */ - - -#include "backtest.h" - -#include -#include -#include - -#include "stats2report.h" - -BacktestControl::BacktestControl(IStockSelector &stockSel, - std::unique_ptr &&rpt, const Config &config, - ondra_shared::StringView chart, - double balance) { - - prepareChart(config,chart); - - - IStockApi *orig_broker = stockSel.getStock(config.broker); - if (orig_broker == nullptr) - throw std::runtime_error(std::string("Unknown stock market name: ")+std::string(config.broker)); - - auto minfo = orig_broker->getMarketInfo(config.pairsymb); - -// PStatSvc statsvc ( new Stats2Report([=](CalcSpreadFn &&fn) {fn();}, "backtest", rpt, config.calc_spread_minutes)); - - class FakeStockSelector: public IStockSelector { - public: - virtual IStockApi *getStock(const std::string_view &stockName) const override { - return api; - } - virtual void forEachStock(EnumFn fn) const override { - fn("backtest_broker",*api); - } - - FakeStockSelector(IStockApi *api):api(api) {} - - protected: - IStockApi *api; - }; - - - broker.emplace(this->chart, minfo, 0, config.mirror); - FakeStockSelector fakeStockSell(&(*broker)); - trader.emplace(fakeStockSell, nullptr, std::move(rpt), config); - trader->setInternalBalance(config.initial_balance!=-1e99?config.initial_balance:balance); -} - - - -bool BacktestControl::step() { - if (!broker->reset()) return false; - trader->perform(); - return true; -} - -BacktestControl::Config BacktestControl::loadConfig(const std::string &fname, - const std::string §ion, - const std::vector &custom_options, double spread) { - - ondra_shared::IniConfig cfg; - cfg.load(fname); - for (auto &&x: custom_options) { - cfg.load(x); - } - Config c(MTrader::load(cfg[section],true)); - c.enabled = true; - auto sect = cfg[section]; - c.calc_spread_minutes = sect["spread_calc_interval"].getUInt(0); - c.mirror = sect["mirror"].getBool(true); - c.repeat = sect["repeat"].getUInt(0); - c.trend = sect["trend"].getNumber(0); - c.random_merge = sect["merge"].getBool(false); - c.random_mins = sect["random"].getNumber(0); - if (c.random_mins) { - c.random_seed = sect.mandatory["seed"].getUInt(); - ondra_shared::StrViewA randoms = sect.mandatory["stddev"].getString(); - auto splt = randoms.split("/"); - while (!!splt) { - ondra_shared::StrViewA r = splt(); - double rn = strtod(r.data,nullptr); - c.randoms.push_back(rn); - } - } - c.dump_chart = sect["dump_chart"].getPath(); - c.title="BT:"+c.title; - if (c.calc_spread_minutes == 0 && c.force_spread == 0) { - c.force_spread = spread; - } - c.initial_balance = sect["init_balance"].getNumber(-1e99); - return c; -} - -BacktestControl::BtReport::BtReport(PStatSvc &&rpt):rpt(std::move(rpt)) { -} - -void BacktestControl::BtReport::reportOrders( - const std::optional &buy, - const std::optional &sell) { - this->buy = buy; - this->sell = sell; -} - -void BacktestControl::BtReport::reportTrades( - ondra_shared::StringView trades, bool margin) { - this->trades = trades; - this->margin = margin; -} - -void BacktestControl::BtReport::reportPrice(double price) { - this->price = price; -} - -void BacktestControl::BtReport::setInfo(const Info &info) { - this->info = info; -} - -void BacktestControl::BtReport::reportMisc(const MiscData &miscData) { - this->miscData = miscData; -} - -void BacktestControl::BtReport::reportError(const ErrorObj &e) { - this->buyError =e.buyError; - this->sellError = e.sellError; - -} - -double BacktestControl::BtReport::calcSpread( - ondra_shared::StringView chart, const MTrader_Config &config, - const IStockApi::MarketInfo &minfo, double balance, - double prev_value) const { - return rpt->calcSpread(chart,config,minfo,balance,prev_value); -} - -std::size_t BacktestControl::BtReport::getHash() const { - return 1; -} - -void BacktestControl::BtReport::flush() { - rpt->setInfo(info); - rpt->reportMisc(miscData); - rpt->reportOrders(buy,sell); - rpt->reportPrice(price); - rpt->reportTrades(trades,margin); - rpt->reportError(ErrorObj (buyError,sellError)); -} - -void BacktestControl::prepareChart(const Config &config, - ondra_shared::StringView chart) { - - double init_price = 1; - if (!chart.empty()) init_price = chart[0].last; - std::vector relchart; - double beg = init_price; - double t = exp(config.trend/100.0); - std::vector diffs(config.randoms.size(),1.0); - if (config.random_mins) { - std::default_random_engine rnd(config.random_seed); - std::vector > norms; - for (auto &&c : config.randoms) { - norms.emplace_back(0.0, c*0.01); - } - for (std::size_t i = 0; i < config.random_mins; i++) { - double diff = 1.0; - double mdiff = 1.0; - if (!chart.empty() && config.random_merge) { - auto &&x = chart[i & chart.length]; - mdiff = x.last/beg; - beg = x.last; - } - for (std::size_t j = 0; j < norms.size(); j++) { - bool recalc = (relchart.size() % (1 << j) == 0); - if (recalc) diffs[j] = norms[j](rnd); - diff *= diffs[j]; - } - diff *= mdiff; - relchart.push_back(diff*t); - } - } else { - for (const auto &x : chart) { - double diff = x.last/beg; - relchart.push_back(diff*t); - beg = x.last; - } - } - - std::size_t cnt = relchart.size(); - for (std::size_t i = 0; i < config.repeat; i++) { - for (std::size_t j = 0; j < cnt; j++) { - relchart.push_back(relchart[j]); - } - } - - cnt = relchart.size(); - std::size_t begtime = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count(); - constexpr std::size_t minute = 60000; - begtime -= cnt * minute; - this->chart.clear(); - beg = init_price; - for (auto &&x: relchart) { - double p = beg * x; - beg = p; - this->chart.push_back(ChartItem { - begtime, p,p,p - }); - begtime+=minute; - } - - if (!config.dump_chart.empty()) { - std::ofstream f(config.dump_chart, std::ios::out|std::ios::trunc); - if (!f) throw std::runtime_error("Can't open: "+ config.dump_chart); - for (auto &&x : this->chart) { - time_t t = x.time/1000; - f << std::put_time(gmtime(&t), "%FT%TZ") << "," << x.last << std::endl; - } - } - -} diff --git a/src/main/backtest.h b/src/main/backtest.h deleted file mode 100644 index 20fc3f8f..00000000 --- a/src/main/backtest.h +++ /dev/null @@ -1,108 +0,0 @@ -/* - * backtest.h - * - * Created on: 22. 8. 2019 - * Author: ondra - */ - - - -#ifndef SRC_MAIN_BACKTEST_H_ -#define SRC_MAIN_BACKTEST_H_ - -#include - -#include "backtest_broker.h" -#include "mtrader.h" - - -class BacktestControl { -public: - - struct Config: public MTrader::Config { - Config() {} - Config(const MTrader::Config &x):MTrader::Config(x) {} - Config(MTrader::Config &&x):MTrader::Config(std::move(x)) {} - - std::size_t calc_spread_minutes; - std::size_t repeat; - std::size_t random_mins; - std::size_t random_seed; - std::vector randoms; - double trend; - bool mirror; - bool random_merge; - std::string dump_chart; - double initial_balance; - - }; - - class BtReport: public IStatSvc { - public: - BtReport(PStatSvc &&rpt); - - virtual void reportOrders(const std::optional &buy, - const std::optional &sell) override; - virtual void reportTrades(ondra_shared::StringView trades, bool margin) override; - virtual void reportPrice(double price) override; - virtual void setInfo(const Info &info) override; - virtual void reportMisc(const MiscData &miscData) override; - virtual void reportError(const ErrorObj &errorObj) override; - virtual double calcSpread(ondra_shared::StringView chart, - const MTrader_Config &config, - const IStockApi::MarketInfo &minfo, - double balance, - double prev_value) const override; - virtual std::size_t getHash() const override; - - ///Interacts with report object - call in worker - report object is not MT safe - void flush(); - - protected: - PStatSvc rpt; - - std::optional buy; - std::optional sell; - ondra_shared::StringView trades; - std::string buyError; - std::string sellError; - double price; - double margin; - MiscData miscData; - Info info; - - }; - - - BacktestControl(IStockSelector &stockSel, - std::unique_ptr &&rpt, - const Config &config, - ondra_shared::StringView chart, - double balance); - - bool step(); - - static Config loadConfig(const std::string &fname, - const std::string §ion, - const std::vector &custom_options, - double spread); - - - - -protected: - - using ChartItem = IStatSvc::ChartItem; - - std::optional broker; - std::optional trader; - std::vector chart; - - void prepareChart(const Config &config, ondra_shared::StringView chart); - - -}; - - - -#endif /* SRC_MAIN_BACKTEST_H_ */ diff --git a/src/main/backtest_broker.cpp b/src/main/backtest_broker.cpp deleted file mode 100644 index d3ad4d2e..00000000 --- a/src/main/backtest_broker.cpp +++ /dev/null @@ -1,123 +0,0 @@ -/* - * backtest_broker.cpp - * - * Created on: 22. 8. 2019 - * Author: ondra - */ - - -#include "backtest_broker.h" - - -BacktestBroker::BacktestBroker(ondra_shared::StringView chart, - const MarketInfo &minfo, double balance, bool mirror) - :chart(chart),minfo(minfo),balance(balance),initial_balance(balance) { - pos = mirror?chart.length:0; - back = mirror; -} - -BacktestBroker::TradeHistory BacktestBroker::getTrades(json::Value lastId, std::uintptr_t fromTime, const std::string_view & pair) { - return TradeHistory(trades.begin()+lastId.getUInt(), trades.end()); -} - - -BacktestBroker::Orders BacktestBroker::getOpenOrders(const std::string_view & par) { - Orders ret; - if (!buy_ex) { - ret.push_back(buy); - } - if (!sell_ex) { - ret.push_back(sell); - } - return ret; -} - -BacktestBroker::Ticker BacktestBroker::getTicker(const std::string_view & piar) { - auto tm = chart[pos].time; - if (back) { - tm = 2*chart[0].time-tm; - } - - return Ticker { - chart[pos].bid, - chart[pos].ask, - chart[pos].last, - tm - }; -} - -json::Value BacktestBroker::placeOrder(const std::string_view & , - double size, double price,json::Value clientId, - json::Value ,double ) { - - Order ord{0,clientId, size, price}; - if (size < 0) { - sell = ord; - sell_ex = false; - } else { - buy = ord; - buy_ex = false; - } - - return 1; -} - - - -bool BacktestBroker::reset() { - auto nx = pos+(back?-1:1); - if (nx < 0) { - back = false; - return reset(); - } else if (static_cast(nx) >= chart.length) { - return false; - } - - pos = nx; - const IStatSvc::ChartItem &p = chart[pos]; - - auto txid = trades.size()+1; - auto tm = p.time; - if (back) { - tm = 2*chart[0].time-tm; - } - - if (p.last > sell.price && !sell_ex) { - Trade tr; - tr.eff_price = sell.price; - tr.eff_size = sell.size; - tr.id = txid; - tr.price = sell.price; - tr.size = sell.size; - tr.time = tm; - minfo.removeFees(tr.eff_size, tr.eff_price); - sell_ex = true; - trades.push_back(tr); - balance += tr.eff_size; - currency -= tr.eff_size*tr.eff_price; - sells++; - } - if (p.last < buy.price && !buy_ex) { - Trade tr; - tr.eff_price = buy.price; - tr.eff_size = buy.size; - tr.id = txid; - tr.price = buy.price; - tr.size = buy.size; - tr.time = tm; - minfo.removeFees(tr.eff_size, tr.eff_price); - buy_ex = true; - trades.push_back(tr); - balance += tr.eff_size; - currency -= tr.eff_size*tr.eff_price; - buys++; - } - - return true; - -} - -double BacktestBroker::getBalance(const std::string_view & x) { - return balance; -} - diff --git a/src/main/backtest_broker.h b/src/main/backtest_broker.h deleted file mode 100644 index d6077971..00000000 --- a/src/main/backtest_broker.h +++ /dev/null @@ -1,52 +0,0 @@ -#include - -#include "../shared/stringview.h" -#include "istatsvc.h" -#include "istockapi.h" - - -class BacktestBroker: public IStockApi { -public: - - BacktestBroker(ondra_shared::StringView chart, - const MarketInfo &minfo, - double balance, bool mirror); - virtual TradeHistory getTrades(json::Value lastId, std::uintptr_t fromTime, const std::string_view & pair) override; - virtual Orders getOpenOrders(const std::string_view & par) override; - virtual Ticker getTicker(const std::string_view & piar) override; - virtual json::Value placeOrder(const std::string_view & pair, - double size, double price,json::Value clientId, - json::Value replaceId,double replaceSize) override; - virtual bool reset() override ; - virtual void testBroker() override {} - virtual double getBalance(const std::string_view &) override; - virtual bool isTest() const override {return false;} - virtual MarketInfo getMarketInfo(const std::string_view &) override{ - return minfo; - } - virtual double getFees(const std::string_view &) override{ - return minfo.fees; - } - virtual std::vector getAllPairs() override {return {};} - - double getScore() const { - return currency+sqrt(chart[0].bid*chart[0].ask)*balance; - } - unsigned int getTradeCount() const { - return std::min(buys,sells); - } - -protected: - double currency=0; - ondra_shared::StringView chart; - TradeHistory trades; - Order buy, sell; - bool buy_ex = true, sell_ex = true; - int pos; - bool back ; - MarketInfo minfo; - double balance; - double initial_balance; - unsigned int buys=0, sells=0; - -}; diff --git a/src/main/calculator.cpp b/src/main/calculator.cpp deleted file mode 100644 index 6567d249..00000000 --- a/src/main/calculator.cpp +++ /dev/null @@ -1,124 +0,0 @@ -/* - * calculator.cpp - * - * Created on: 17. 6. 2019 - * Author: ondra - */ - -#include -#include - -#include "sgn.h" -#include "calculator.h" -#include "../shared/logOutput.h" - -using ondra_shared::logDebug; - -void Calculator::update(double new_price, double abs_balance) { - - - - //we will adjust the balance - price = new_price; - balance = abs_balance; -} - -void Calculator::update_after_trade(double new_price, double new_balance, double old_balance, double acum) { - - if (achieve_mode) { - double expected = price2balance(new_price); - double diff = std::fabs(expected - new_balance); - if (diff < expected*0.001) { - achieve_mode = false; - } - } else { - if (acum == 0) return; - double prev_price = balance2price(old_balance); - double extra = calcExtra(prev_price, new_price); - double fin_balance = price2balance(new_price)+ extra*acum; - update(new_price, fin_balance); - } - -} -double Calculator::price2balance(double new_price) const { - - //basic formula to map price to balance - return balance*sqrt(price/new_price); - -} - -double Calculator::balance2price(double new_balance) const { - - if (new_balance == 0) return 9e99; - //formula to map balance to price - /* - * c = b * sqrt(p/n) - * c/b = sqrt(p/n) - * pow2(c/b) = (p/n) - * pow2(c/b)/p = 1/n - * p*pow(c/b) = n - */ - return price * pow2(balance/new_balance); - -} - - -double Calculator::calcExtra(double last_price, double new_price) const { - if (achieve_mode) return 0; - //balance after last trade (at last price) - double b1 = price2balance(last_price); - //balance at new price - double b2 = price2balance(new_price); - //so we must buy (+) or sell (-) that assets - double sz = b2 - b1; - //currency need for the trade - double cur = -sz * new_price; - //currency change due changed equilibrum - double cur2 = b2* new_price - b1 * last_price; - //difference between these currencies (extra profit) - //divided by new price - //- extra profit can be used to increase balance - return (cur - cur2)/new_price; - - -} - -json::Value Calculator::toJSON() const { - return json::Value(json::object,{ - json::Value("price", price), - json::Value("balance", balance), - json::Value("achieve", achieve_mode) - }); -} - -double Calculator::price2currency(double new_price) const { - return balance *sqrt(price * new_price); -} - -double Calculator::currency2price(double currency) const { - /* - * c = b * sqrt(p * n) - * pow2(c) = pow2(b) * p * n - * pow2(c/b)/p = n - */ - return pow2(currency/balance) / price; -} - -void Calculator::achieve(double new_price, double new_balance) { - achieve_mode = true; - price = new_price; - balance = new_balance; -} - -Calculator::Calculator() {} - -Calculator::Calculator(double price, double balance, bool achieve):price(price),balance(balance),achieve_mode(achieve) { -} - -Calculator Calculator::fromJSON(json::Value v) { - return Calculator( - v["price"].getNumber(), - v["balance"].getNumber(), - v["achieve"].getBool() - ); -} diff --git a/src/main/calculator.h b/src/main/calculator.h deleted file mode 100644 index 39d2ac49..00000000 --- a/src/main/calculator.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * calculator.h - * - * Created on: 17. 6. 2019 - * Author: ondra - */ - -#ifndef SRC_CALCULATOR_H_ -#define SRC_CALCULATOR_H_ - -namespace json { - class Value; -} - -class Calculator { -public: - - Calculator(); - Calculator(double price, double balance, bool achieve); - - - void update(double new_price, double abs_balance); - - void update_after_trade(double new_price, double new_balance, double old_balance, double acum); - - double balance2price(double balance) const; - - double price2balance(double price) const; - - double price2currency(double price) const; - - double currency2price(double balance) const; - - double calcExtra(double prev_price, double new_price) const; - - json::Value toJSON() const; - - static Calculator fromJSON(json::Value v); - - double getBalance() const { - return balance; - } - - double getPrice() const { - return price; - } - - bool isValid() const { - return price > 0 && balance > 0; - } - - void achieve(double new_price, double new_balance); - bool isAchieveMode() const {return achieve_mode;} - -protected: - double price = 0; - double balance = 0; - bool achieve_mode = false; - -}; - -#endif /* SRC_CALCULATOR_H_ */ diff --git a/src/main/emulator.cpp b/src/main/emulator.cpp index fc59f466..3369e9dc 100644 --- a/src/main/emulator.cpp +++ b/src/main/emulator.cpp @@ -53,10 +53,21 @@ double EmulatorAPI::getBalance(const std::string_view & symb) { } } -EmulatorAPI::TradeHistory EmulatorAPI::getTrades(json::Value lastId, std::uintptr_t , - const std::string_view &) { +EmulatorAPI::TradesSync EmulatorAPI::syncTrades(json::Value lastId, const std::string_view &) { + + if (lastId.hasValue()) { + std::size_t idx = lastId.getUInt(); + return TradesSync{ + TradeHistory(trades.begin()+idx, trades.end()), + json::Value(trades.size()) + }; + } else { + return TradesSync { + trades, + json::Value(nullptr) + }; + } - return TradeHistory(std::move(trades)); } EmulatorAPI::Orders EmulatorAPI::getOpenOrders(const std::string_view & pair) { @@ -68,6 +79,7 @@ EmulatorAPI::Ticker EmulatorAPI::getTicker(const std::string_view & pair) { this->pair = pair; Ticker tk = datasrc.getTicker(pair); simulation(tk); + lastTicker = tk; return tk; } @@ -75,6 +87,13 @@ json::Value EmulatorAPI::placeOrder(const std::string_view & pair, double size, double price,json::Value clientId, json::Value replaceId,double replaceSize) { + if (size > 0) { + if (price > lastTicker.last) price = lastTicker.last*(1-1e-8); + } else if (size < 0) { + if (price < lastTicker.last) price = lastTicker.last*(1+1e-8); + } + + Order order{genID(), clientId, size, price}; if (replaceId.defined()) { @@ -82,12 +101,19 @@ json::Value EmulatorAPI::placeOrder(const std::string_view & pair, return o.id == replaceId; }); if (iter != orders.end()) { - *iter = order; - return iter->id; + if (size == 0) { + orders.erase(iter); + return nullptr; + } + else { + *iter = order; + return iter->id; + } } else { return nullptr; } } else { + if (price <= 0) throw std::runtime_error("Invalid order price"); orders.push_back(order); return order.id; } @@ -98,6 +124,7 @@ EmulatorAPI::MarketInfo EmulatorAPI::getMarketInfo(const std::string_view & pair balance_symb = minfo.asset_symbol; currency_symb = minfo.currency_symbol; margin = minfo.leverage > 0; + minfo.simulator = true; return minfo; } @@ -111,6 +138,21 @@ std::vector EmulatorAPI::getAllPairs() { return datasrc.getAllPairs(); } +EmulatorAPI::BrokerInfo EmulatorAPI::getBrokerInfo() { + return datasrc.getBrokerInfo(); +} + +void EmulatorAPI::saveIconToDisk(const std::string &path) const { + const IBrokerIcon *icn = dynamic_cast(&datasrc); + if (icn) icn->saveIconToDisk(path); +} + +std::string EmulatorAPI::getIconName() const { + const IBrokerIcon *icn = dynamic_cast(&datasrc); + if (icn) return icn->getIconName(); + else return std::string(); +} + void EmulatorAPI::simulation(const Ticker &tk) { double cur = tk.last; @@ -159,9 +201,6 @@ bool EmulatorAPI::reset() { return true; } -bool EmulatorAPI::isTest() const { - return true; -} std::size_t EmulatorAPI::genID() { return ++prevId; diff --git a/src/main/emulator.h b/src/main/emulator.h index e10dd4a1..49b6e6df 100644 --- a/src/main/emulator.h +++ b/src/main/emulator.h @@ -10,29 +10,34 @@ #include #include "../shared/logOutput.h" +#include "ibrokercontrol.h" #include "istockapi.h" -class EmulatorAPI: public IStockApi { +class EmulatorAPI: public IStockApi, public IBrokerIcon { public: EmulatorAPI(IStockApi &datasrc, double initial_currency); virtual double getBalance(const std::string_view & symb) override; - virtual TradeHistory getTrades(json::Value lastId, std::uintptr_t fromTime, const std::string_view & pair) override; + virtual TradesSync syncTrades(json::Value lastId, const std::string_view & pair) override; virtual Orders getOpenOrders(const std::string_view & par) override; virtual Ticker getTicker(const std::string_view & piar) override; virtual json::Value placeOrder(const std::string_view & pair, double size, double price,json::Value clientId, json::Value replaceId,double replaceSize) override; virtual bool reset() override; - virtual bool isTest() const override; virtual MarketInfo getMarketInfo(const std::string_view & pair) override; virtual double getFees(const std::string_view &pair) override; virtual std::vector getAllPairs() override; virtual void testBroker() override {datasrc.testBroker();} + virtual BrokerInfo getBrokerInfo() override; + //saves image to disk to specified path + virtual void saveIconToDisk(const std::string &path) const override; + //retrieves name of saved image + virtual std::string getIconName() const override; static std::string_view prefix; @@ -44,6 +49,7 @@ class EmulatorAPI: public IStockApi { Orders orders; TradeHistory trades; + Ticker lastTicker; std::string balance_symb; std::string currency_symb; diff --git a/src/main/ext_stockapi.cpp b/src/main/ext_stockapi.cpp index 6094e051..6461a2c1 100644 --- a/src/main/ext_stockapi.cpp +++ b/src/main/ext_stockapi.cpp @@ -11,7 +11,11 @@ #include "ext_stockapi.h" #include +#include +#include +#include +#include "../shared/finally.h" using namespace ondra_shared; @@ -28,14 +32,15 @@ double ExtStockApi::getBalance(const std::string_view & symb) { } -ExtStockApi::TradeHistory ExtStockApi::getTrades(json::Value lastId, std::uintptr_t fromTime, const std::string_view & pair) { - auto r = jsonRequestExchange("getTrades",json::Object +ExtStockApi::TradesSync ExtStockApi::syncTrades(json::Value lastId, const std::string_view & pair) { + auto r = jsonRequestExchange("syncTrades",json::Object ("lastId",lastId) - ("fromTime", fromTime) ("pair",StrViewA(pair))); TradeHistory th; - for (json::Value v: r) th.push_back(Trade::fromJSON(v)); - return th; + for (json::Value v: r["trades"]) th.push_back(Trade::fromJSON(v)); + return TradesSync { + th, r["lastId"] + }; } ExtStockApi::Orders ExtStockApi::getOpenOrders(const std::string_view & pair) { @@ -101,6 +106,7 @@ ExtStockApi::MarketInfo ExtStockApi::getMarketInfo(const std::string_view & pair res.feeScheme = strFeeScheme[v["feeScheme"].getString()]; res.leverage= v["leverage"].getNumber(); res.invert_price= v["invert_price"].getBool(); + res.simulator= v["simulator"].getBool(); res.inverted_symbol= v["inverted_symbol"].getString(); return res; @@ -129,3 +135,70 @@ void ExtStockApi::onConnect() { } } + +ExtStockApi::BrokerInfo ExtStockApi::getBrokerInfo() { + + try { + auto resp = jsonRequestExchange("getBrokerInfo", json::Value()); + return BrokerInfo { + resp["trading_enabled"].getBool(), + this->name, + resp["name"].getString(), + resp["url"].getString(), + resp["version"].getString(), + resp["licence"].getString(), + StrViewA(resp["favicon"].getBinary()), + resp["settings"].getBool() + }; + } catch (IStockApi::Exception &) { + return BrokerInfo { + true, + this->name, + this->name, + }; + } + +} + +void ExtStockApi::setApiKey(json::Value keyData) { + jsonRequestExchange("setApiKey",keyData); +} + +json::Value ExtStockApi::getApiKeyFields() const { + return const_cast(this)->jsonRequestExchange("getApiKeyFields",json::Value()); +} + +json::Value ExtStockApi::getSettings(const std::string_view & pairHint) const { + return const_cast(this)->jsonRequestExchange("getSettings",json::Value(pairHint)); +} + +void ExtStockApi::setSettings(json::Value v) { + jsonRequestExchange("setSettings", v); +} + + + + +void ExtStockApi::saveIconToDisk(const std::string &path) const { + Sync _(lock); + + static std::set files; + auto clean_call = []{ + for (auto &&k: files) std::remove(k.c_str()); + }; + static ondra_shared::FinallyImpl finally(std::move(clean_call)); + + std::string name =getIconName(); + std::string fullpath = path+"/"+name; + if (files.find(fullpath) == files.end()) { + std::ofstream f(fullpath, std::ios::out|std::ios::trunc|std::ios::binary); + BrokerInfo binfo = const_cast(this)->getBrokerInfo(); + json::Binary b = json::base64->decodeBinaryValue(binfo.favicon).getBinary(json::base64); + f.write(reinterpret_cast(b.data), b.length); + files.insert(fullpath); + } +} + +std::string ExtStockApi::getIconName() const { + return name+".png"; +} diff --git a/src/main/ext_stockapi.h b/src/main/ext_stockapi.h index cea132e1..dcd4ddf3 100644 --- a/src/main/ext_stockapi.h +++ b/src/main/ext_stockapi.h @@ -10,10 +10,12 @@ #include "istockapi.h" #include "abstractExtern.h" +#include "apikeys.h" +#include "ibrokercontrol.h" -class ExtStockApi: public AbstractExtern, public IStockApi { +class ExtStockApi: public AbstractExtern, public IStockApi, public IApiKey, public IBrokerControl, public IBrokerIcon { public: ExtStockApi(const std::string_view & workingDir, const std::string_view & name, const std::string_view & cmdline); @@ -21,19 +23,26 @@ class ExtStockApi: public AbstractExtern, public IStockApi { virtual double getBalance(const std::string_view & symb) override; - virtual TradeHistory getTrades(json::Value lastId, std::uintptr_t fromTime, const std::string_view & pair) override; + virtual TradesSync syncTrades(json::Value lastId, const std::string_view & pair) override; virtual Orders getOpenOrders(const std::string_view & par) override; virtual Ticker getTicker(const std::string_view & piar) override; virtual json::Value placeOrder(const std::string_view & pair, double size, double price,json::Value clientId, json::Value replaceId,double replaceSize) override; virtual bool reset() override; - virtual bool isTest() const override {return false;} virtual MarketInfo getMarketInfo(const std::string_view & pair) override; virtual double getFees(const std::string_view & pair) override; virtual std::vector getAllPairs() override; virtual void testBroker() override {preload();} virtual void onConnect() override; + virtual BrokerInfo getBrokerInfo() override; + virtual void setApiKey(json::Value keyData) override; + virtual json::Value getApiKeyFields() const override; + virtual json::Value getSettings(const std::string_view & pairHint) const override; + virtual void setSettings(json::Value v) override; + virtual void saveIconToDisk(const std::string &path) const override; + virtual std::string getIconName() const override; + }; diff --git a/src/main/extdailyperfmod.cpp b/src/main/extdailyperfmod.cpp new file mode 100644 index 00000000..caa7c0b4 --- /dev/null +++ b/src/main/extdailyperfmod.cpp @@ -0,0 +1,51 @@ +/* + * extdailyperfmod.cpp + * + * Created on: 26. 10. 2019 + * Author: ondra + */ + +#include "extdailyperfmod.h" + +#include "../shared/logOutput.h" +using json::Object; +using json::Value; +using ondra_shared::logError; + +#include "imtjson/object.h" + +std::size_t ExtDailyPerfMod::daySeconds = 86400; + +void ExtDailyPerfMod::sendItem(const PerformanceReport &report) { + + + try { + + Object jrep; + jrep.set("broker",report.broker); + jrep.set("currency",report.currency); + jrep.set("magic",report.magic); + jrep.set("price",report.price); + jrep.set("size",report.size); + jrep.set("tradeId",report.tradeId); + jrep.set("uid",report.uid); + jsonRequestExchange("sendItem", jrep); + + } catch (std::exception &e) { + logError("ExtDailyPerfMod: $1", e.what()); + } +} + +json::Value ExtDailyPerfMod::getReport() { + std::size_t newidx = time(nullptr)/daySeconds; + if (dayIndex != newidx) { + try { + reportCache = jsonRequestExchange("getReport", json::Value()); + dayIndex = newidx; + } catch (std::exception &e) { + return Object("hdr",Value(json::array, {"error"})) + ("rows",Value(json::array, {Value(json::array,{e.what()})})); + } + } + return reportCache; +} diff --git a/src/main/extdailyperfmod.h b/src/main/extdailyperfmod.h new file mode 100644 index 00000000..eeab13c1 --- /dev/null +++ b/src/main/extdailyperfmod.h @@ -0,0 +1,29 @@ +/* + * extdailyperfmod.h + * + * Created on: 26. 10. 2019 + * Author: ondra + */ + +#ifndef SRC_MAIN_EXTDAILYPERFMOD_H_ +#define SRC_MAIN_EXTDAILYPERFMOD_H_ + +#include "abstractExtern.h" +#include "idailyperfmod.h" + +class ExtDailyPerfMod: public IDailyPerfModule, public AbstractExtern { +public: + using AbstractExtern::AbstractExtern; + + virtual void sendItem(const PerformanceReport &report) override; + virtual json::Value getReport() override; + +public: + json::Value reportCache; + static std::size_t daySeconds; + std::size_t dayIndex = 0; + + +}; + +#endif /* SRC_MAIN_EXTDAILYPERFMOD_H_ */ diff --git a/src/main/ibrokercontrol.h b/src/main/ibrokercontrol.h new file mode 100644 index 00000000..5152fecf --- /dev/null +++ b/src/main/ibrokercontrol.h @@ -0,0 +1,28 @@ +/* + * ibrokercontrol.h + * + * Created on: 20. 10. 2019 + * Author: ondra + */ + +#ifndef SRC_MAIN_IBROKERCONTROL_H_ +#define SRC_MAIN_IBROKERCONTROL_H_ +#include + +class IBrokerControl { +public: + + virtual json::Value getSettings(const std::string_view &pairHint) const = 0; + virtual void setSettings(json::Value v) = 0; + virtual ~IBrokerControl() {} +}; + +class IBrokerIcon { +public: + //saves image to disk to specified path + virtual void saveIconToDisk(const std::string &path) const = 0; + //retrieves name of saved image + virtual std::string getIconName() const = 0; +}; + +#endif /* SRC_MAIN_IBROKERCONTROL_H_ */ diff --git a/src/main/idailyperfmod.h b/src/main/idailyperfmod.h new file mode 100644 index 00000000..a986ac2a --- /dev/null +++ b/src/main/idailyperfmod.h @@ -0,0 +1,33 @@ +/* + * idailyperfmod.h + * + * Created on: 24. 10. 2019 + * Author: ondra + */ + +#ifndef SRC_MAIN_IDAILYPERFMOD_H_ +#define SRC_MAIN_IDAILYPERFMOD_H_ +#include + + struct PerformanceReport { + std::size_t magic; + std::size_t uid; + std::string tradeId; + std::string currency; + std::string broker; + double price; + double size; + }; + + +class IDailyPerfModule { +public: + + virtual void sendItem(const PerformanceReport &report) = 0; + virtual json::Value getReport() = 0; + virtual ~IDailyPerfModule() {} +}; + + + +#endif /* SRC_MAIN_IDAILYPERFMOD_H_ */ diff --git a/src/main/istatsvc.h b/src/main/istatsvc.h index b2e13a84..0cf761ff 100644 --- a/src/main/istatsvc.h +++ b/src/main/istatsvc.h @@ -8,17 +8,20 @@ #ifndef SRC_MAIN_ISTATSVC_H_ #define SRC_MAIN_ISTATSVC_H_ +#include "istatsvc.h" #include #include "istockapi.h" struct MTrader_Config; +struct PerformanceReport; +class Strategy; class IStatSvc { public: struct ChartItem { - std::uintptr_t time; + std::uint64_t time; double ask; double bid; double last; @@ -26,18 +29,14 @@ class IStatSvc { struct MiscData { int trade_dir; - bool achieve; double calc_price; double spread; double dynmult_buy; double dynmult_sell; - double size_mult; - double value; - double boost; double lowest_price; double highest_price; std::size_t total_trades; - std::size_t total_time; + std::uint64_t total_time; }; @@ -46,6 +45,7 @@ class IStatSvc { std::string_view assetSymb; std::string_view currencySymb; std::string_view priceSymb; + std::string_view brokerIcon; double position_offset; bool inverted; bool margin; @@ -62,19 +62,37 @@ class IStatSvc { }; + struct TradeRecord: public IStockApi::Trade { + + double norm_profit; + double norm_accum; + + TradeRecord(const IStockApi::Trade &t, double norm_profit, double norm_accum) + :IStockApi::Trade(t),norm_profit(norm_profit),norm_accum(norm_accum) {} + + static TradeRecord fromJSON(json::Value v) { + return TradeRecord(IStockApi::Trade::fromJSON(v), v["np"].getNumber(), v["ap"].getNumber()); + } + json::Value toJSON() const { + return IStockApi::Trade::toJSON().merge(json::Value(json::object,{ + json::Value("np",norm_profit), + json::Value("ap",norm_accum) + })); + } + + + }; + virtual void reportOrders(const std::optional &buy, const std::optional &sell) = 0; - virtual void reportTrades(ondra_shared::StringView trades, bool margin) = 0; + virtual void reportTrades(ondra_shared::StringView trades) = 0; virtual void reportPrice(double price) = 0; virtual void setInfo(const Info &info) = 0; virtual void reportMisc(const MiscData &miscData) = 0; virtual void reportError(const ErrorObj &errorObj) = 0; - virtual double calcSpread(ondra_shared::StringView chart, - const MTrader_Config &config, - const IStockApi::MarketInfo &minfo, - double balance, - double prev_value) const = 0; + virtual void reportPerformance(const PerformanceReport &repItem) = 0; virtual std::size_t getHash() const = 0; + virtual void clear() = 0; virtual ~IStatSvc() {} }; diff --git a/src/main/istockapi.cpp b/src/main/istockapi.cpp index f2be7877..8baec15c 100644 --- a/src/main/istockapi.cpp +++ b/src/main/istockapi.cpp @@ -23,7 +23,7 @@ IStockApi::Trade IStockApi::Trade::fromJSON(json::Value x) { return IStockApi::Trade { x["id"].stripKey(), - x["time"].getUInt(), + x["time"].getUIntLong(), size, price, size, @@ -32,7 +32,7 @@ IStockApi::Trade IStockApi::Trade::fromJSON(json::Value x) { } else { return IStockApi::Trade { x["id"].stripKey(), - x["time"].getUInt(), + x["time"].getUIntLong(), size, price, x["eff_size"].getNumber(), diff --git a/src/main/istockapi.h b/src/main/istockapi.h index a40387db..961c810c 100644 --- a/src/main/istockapi.h +++ b/src/main/istockapi.h @@ -65,8 +65,6 @@ class IStockApi { json::Value toJSON() const; }; - using TWBHistory = std::vector; - ///Order struct Order { @@ -93,7 +91,7 @@ class IStockApi { ///Last price double last; ///Time when read - std::uintptr_t time; + std::uint64_t time; }; enum FeeScheme { @@ -150,6 +148,10 @@ class IStockApi { ///When invert_price is true, the broker should also supply symbol name of inverted price std::string inverted_symbol; + ///This flag must be true, if the broker is just simulator and doesn't do live trading + /** Simulators are not included into daily performance */ + bool simulator = false; + ///Adds fees to values /** * @param assets reference to current asset change. Negative value is sell, @@ -170,6 +172,30 @@ class IStockApi { } }; + struct BrokerInfo { + ///must contain true to enlist broker in the web interface. + bool trading_enabled; + ///Name of the broker + std::string name; + ///Name of the exchange + std::string exchangeName; + ///url to homepage of the exchange + std::string exchangeUrl; + ///version identifier + std::string version; + ///licence text + std::string licence; + ///favicon binary image/png + std::string favicon; + ///this option must be true,if the broker supports getSetting/setSettings + bool settings = false; + }; + + + struct TradesSync { + TradeHistory trades; + json::Value lastId; + }; using Orders = std::vector; @@ -186,7 +212,8 @@ class IStockApi { * @param pair specify trading pair * @return list of trades */ - virtual TradeHistory getTrades(json::Value lastId, std::uintptr_t fromTime, const std::string_view & pair) = 0; + virtual TradesSync syncTrades(json::Value lastId, const std::string_view & pair) = 0; + ///Retrieve open orders /** * @param par trading pair @@ -240,14 +267,6 @@ class IStockApi { * internal brokers and emulators. */ virtual bool reset() = 0; - ///Determines whether the API is emulator - /** - * @retval true API is emulator - * @retval false API is not emulator - * - * @note external brokers cannot set this to 'true' - */ - virtual bool isTest() const = 0; ///Retrieve market information /** @@ -274,6 +293,8 @@ class IStockApi { using std::runtime_error::runtime_error; }; + virtual BrokerInfo getBrokerInfo() = 0; + virtual ~IStockApi() {} static json::NamedEnum strFeeScheme; diff --git a/src/main/istorage.h b/src/main/istorage.h index 1e7f6899..c6df414c 100644 --- a/src/main/istorage.h +++ b/src/main/istorage.h @@ -17,6 +17,7 @@ class IStorage { virtual void store(json::Value data) = 0; virtual json::Value load() = 0; + virtual void erase() = 0; virtual ~IStorage() {} diff --git a/src/main/istrategy.h b/src/main/istrategy.h new file mode 100644 index 00000000..a9582a49 --- /dev/null +++ b/src/main/istrategy.h @@ -0,0 +1,79 @@ +/* + * istrategy.h + * + * Created on: 17. 10. 2019 + * Author: ondra + */ + +#ifndef SRC_MAIN_ISTRATEGY_H_ +#define SRC_MAIN_ISTRATEGY_H_ +#include +#include +#include "../shared/refcnt.h" + +class IStrategy: public ondra_shared::RefCntObj { +public: + + ///Strategy is initialized and valid + /** + * @retval true valid and ready + * @retval false not ready, you must do initial setup + */ + virtual bool isValid() const = 0; + + + ///Initialized strategy. Because the object is immutable, it creates a new revision and returns it + /** + * @param curPrice current price + * @param assets current assets (or position) + * @param currency current currencies + * @return initialized strategy + */ + virtual IStrategy *init(double curPrice, double assets, double currency) const = 0; + + + struct OnTradeResult { + double normProfit; + double normAccum; + }; + + ///Creates a new state after trade + /** + * @param tradePrice price where the trade has been executed + * @param tradeSize size of the execution. If this value is 0, then trade has been created by accept_loss + * @param assetsLeft assets left on the account (or position) + * @param currencyLeft currency left on the account + * @return result of trade and pointer to a new state + */ + virtual std::pair onTrade(double tradePrice, double tradeSize, double assetsLeft, double currencyLeft) const = 0; + + ///Export state to JSON + virtual json::Value exportState() const = 0; + + ///Import state from JSON + /** Creates new instance */ + virtual IStrategy *importState(json::Value src) const = 0; + + + virtual double calcOrderSize(double price, double assets) const = 0; + + + struct MinMax { + double min; + double max; + }; + + virtual MinMax calcSafeRange(double assets, double currencies) const = 0; + + virtual double getEquilibrium() const = 0; + + virtual IStrategy *reset() const = 0; + + virtual std::string_view getID() const = 0; + + virtual ~IStrategy() {} +}; + + + +#endif /* SRC_MAIN_ISTRATEGY_H_ */ diff --git a/src/main/localdailyperfmod.cpp b/src/main/localdailyperfmod.cpp new file mode 100644 index 00000000..86c42f67 --- /dev/null +++ b/src/main/localdailyperfmod.cpp @@ -0,0 +1,184 @@ +/* + * localdailyperfmod.cpp + * + * Created on: 25. 10. 2019 + * Author: ondra + */ + +#include +#include +#include + +#include "localdailyperfmod.h" + +#include "../shared/stringview.h" +using ondra_shared::logError; +using ondra_shared::StrViewA; + +#include "../shared/logOutput.h" + +std::size_t LocalDailyPerfMonitor::daySeconds = 86400; + +LocalDailyPerfMonitor::LocalDailyPerfMonitor(PStorage &&storage, std::string logfile) + :storage(std::move(storage)), logfile(logfile) +{ +} + +void LocalDailyPerfMonitor::checkInit() { + time_t t = std::time(nullptr); + unsigned int newdayindex = t/daySeconds; + + if (!dailySums.defined()) { + init(newdayindex); + } + if (newdayindex != dayIndex) { + aggregate(newdayindex); + } + + +} + +void LocalDailyPerfMonitor::sendItem(const PerformanceReport &report) { + checkInit(); + + json::Object sentence; + sentence.set + ("uid",report.uid) + ("currency",report.currency) + ("price",report.price) + ("size",report.size); + + json::Value(sentence).toStream(logf); + logf.put('\n'); + logf.flush(); + +} + +void LocalDailyPerfMonitor::prepareReport() { + using namespace json; + std::unordered_set header; + for (Value row: dailySums) { + Value data = row[1]; + for (Value x:data) { + header.insert(x.getKey()); + } + } + + + Value jheader (json::array, header.begin(), header.end(), [](StrViewA x){return x;}); + jheader.unshift("Date"); + Array reportrows; + for (Value row: dailySums) { + Value data = row[1]; + Array rrow; + rrow.push_back(row[0].getUInt()*daySeconds); + for (auto &h : header) { + rrow.push_back(data[h].getNumber()); + } + reportrows.push_back(rrow); + } + + report = Object + ("hdr", jheader) + ("rows", reportrows); + + + +} + + +json::Value LocalDailyPerfMonitor::getReport() { + checkInit(); + + return report; +} + +void LocalDailyPerfMonitor::init(unsigned int curDayIndex) { + json::Value data = storage->load(); + if (data.hasValue()) { + dayIndex = data["day"].getUInt(); + dailySums = data["sum"]; + } else { + dayIndex = curDayIndex; + dailySums = json::array; + save(); + } + logf.open(logfile, std::ios::app); + prepareReport(); + +} + +void LocalDailyPerfMonitor::aggregate(unsigned int curDayIndex) { + + struct Position { + std::string currency; + double price = 0; + double pos = 0; + bool hit = false; + }; + + + std::unordered_map positions; + std::unordered_map pldb; + + logf.close(); + try { + + std::ifstream inf(logfile); + int i; + while (( i = inf.get())!= EOF) { + if (isspace(i)) continue; + inf.putback(i); + json::Value row = json::Value::fromStream(inf); + + std::size_t uid = row["uid"].getUInt(); + std::string currency = row["currency"].getString(); + double price = row["price"].getNumber(); + double size = row["size"].getNumber(); + + Position &pos = positions[uid]; + double pl = (price - pos.price) * pos.pos; + if (pl) pos.hit = true; + pldb[currency] += pl; + pos.currency = currency; + pos.pos += size; + pos.price = price; + } + + json::Object curs; + for (auto &&t: pldb) { + if (t.second) { + curs.set(t.first, t.second); + } + } + dailySums.push({curDayIndex, curs}); + dayIndex = curDayIndex; + + save(); + inf.close(); + logf.clear(std::ios::badbit|std::ios::eofbit); + + logf.open(logfile, std::ios::out| std::ios::trunc); + { + for(auto &&t: positions) { + if (t.second.hit) { + sendItem(PerformanceReport { + 0,t.first,"",t.second.currency,"",t.second.price,t.second.pos + }); + } + } + } + logf.flush(); + prepareReport(); + + } catch (std::exception &e) { + logError("failed to flush daily performance data - $1", e.what()); + dayIndex = curDayIndex; + logf.open(logfile, std::ios::app); + return; + } +} + +void LocalDailyPerfMonitor::save() { + storage->store(json::Object("day", dayIndex)("sum", dailySums)); +} diff --git a/src/main/localdailyperfmod.h b/src/main/localdailyperfmod.h new file mode 100644 index 00000000..d8f5cfa6 --- /dev/null +++ b/src/main/localdailyperfmod.h @@ -0,0 +1,49 @@ +/* + * localdailyperfmod.h + * + * Created on: 25. 10. 2019 + * Author: ondra + */ + +#ifndef SRC_MAIN_LOCALDAILYPERFMOD_H_ +#define SRC_MAIN_LOCALDAILYPERFMOD_H_ +#include +#include + +#include +#include +#include "idailyperfmod.h" +#include "istorage.h" +#include "report.h" + +class LocalDailyPerfMonitor: public IDailyPerfModule { +public: + + LocalDailyPerfMonitor(PStorage &&storage, std::string logfile); + + + virtual void sendItem(const PerformanceReport &report) override; + virtual json::Value getReport() override; + +protected: + PStorage storage; + unsigned int dayIndex; + std::ofstream logf; + std::string logfile; + json::Value dailySums; + json::Value report; + + void init(unsigned int curDayIndex); + void aggregate(unsigned int curDayIndex); + void save(); + void prepareReport(); + void checkInit(); + + static std::size_t daySeconds; + + +}; + + + +#endif /* SRC_MAIN_LOCALDAILYPERFMOD_H_ */ diff --git a/src/main/main.cpp b/src/main/main.cpp index 8c0b0239..4b2f5f20 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -8,6 +8,7 @@ #include "../server/src/simpleServer/abstractStream.h" #include "../server/src/simpleServer/address.h" #include "../server/src/simpleServer/http_filemapper.h" +#include "../server/src/simpleServer/http_pathmapper.h" #include "../server/src/simpleServer/http_server.h" #include "../shared/linux_crash_handler.h" @@ -16,16 +17,20 @@ #include "shared/cmdline.h" #include "shared/future.h" #include "../shared/sch2wrk.h" +#include "abstractExtern.h" #include "istockapi.h" #include "istatsvc.h" -#include "ordergen.h" #include "mtrader.h" #include "report.h" -#include "spread_calc.h" #include "ext_stockapi.h" -#include "stats2report.h" -#include "backtest.h" +#include "authmapper.h" +#include "webcfg.h" +#include "spawn.h" +#include "extdailyperfmod.h" +#include "localdailyperfmod.h" +#include "stats2report.h" +#include "traders.h" using ondra_shared::StdLogFile; using ondra_shared::StrViewA; @@ -46,153 +51,7 @@ using ondra_shared::RefCntObj; using ondra_shared::RefCntPtr; using ondra_shared::schedulerGetWorker; - -using StatsSvc = Stats2Report; - -class NamedMTrader: public MTrader { -public: - NamedMTrader(IStockSelector &sel, StoragePtr &&storage, PStatSvc statsvc, Config cfg, std::string &&name) - :MTrader(sel, std::move(storage), std::move(statsvc), cfg), ident(std::move(name)) { - } - - bool perform() { - LogObject lg(ident); - LogObject::Swap swap(lg); - try { - return MTrader::perform(); - } catch (std::exception &e) { - logError("$1", e.what()); - return false; - } - } - - std::string ident; - -}; - -class StockSelector: public IStockSelector{ -public: - using PStockApi = std::unique_ptr; - using StockMarketMap = ondra_shared::linear_map>; - - StockMarketMap stock_markets; - - void loadStockMarkets(const ondra_shared::IniConfig::Section &ini, bool test) { - std::vector data; - for (auto &&def: ini) { - ondra_shared::StrViewA name = def.first; - ondra_shared::StrViewA cmdline = def.second.getString(); - ondra_shared::StrViewA workDir = def.second.getCurPath(); - data.push_back(StockMarketMap::value_type(name,std::make_unique(workDir, name, cmdline))); - } - StockMarketMap map(std::move(data)); - stock_markets.swap(map); - } - virtual IStockApi *getStock(const std::string_view &stockName) const { - auto f = stock_markets.find(stockName); - if (f == stock_markets.cend()) return nullptr; - return f->second.get(); - } - void addStockMarket(ondra_shared::StrViewA name, PStockApi &&market) { - stock_markets.insert(std::pair(name,std::move(market))); - } - - virtual void forEachStock(EnumFn fn) const { - for(auto &&x: stock_markets) { - fn(x.first, *x.second); - } - } - void clear() { - stock_markets.clear(); - } -}; - - - -static std::vector traders; -static StockSelector stockSelector; - -class ActionQueue: public RefCntObj { -public: - ActionQueue(const Scheduler &sch):sch(sch) {} - - template - void push(Fn &&fn) { - bool e = dsp.empty(); - std::move(fn) >> dsp; - if (e) goon(); - } - - void exec() { - if (!dsp.empty()) { - dsp.pump(); - goon(); - } - } - - void goon() { - sch.after(std::chrono::seconds(1)) >> [me = RefCntPtr(this)]{ - me->exec(); - }; - } - -protected: - Dispatcher dsp; - Scheduler sch; -}; - - -void loadTraders(const ondra_shared::IniConfig &ini, - ondra_shared::StrViewA names, - StorageFactory &sf, - Scheduler sch, - Report &rpt, - bool force_dry_run, - int spread_calc_interval, - Stats2Report::SharedPool pool) { - traders.clear(); - std::vector nv; - - RefCntPtr aq ( new ActionQueue(sch) ); - - auto nspl = names.split(" "); - while (!!nspl) { - StrViewA x = nspl(); - if (!x.empty()) nv.push_back(x); - } - - for (auto n: nv) { - LogObject lg(n); - LogObject::Swap swp(lg); - try { - if (n[0] == '_') throw std::runtime_error(std::string(n).append(": The trader's name can't begins with underscore '_'")); - MTrader::Config mcfg = MTrader::load(ini[n], force_dry_run); - logProgress("Started trader $1 (for $2)", n, mcfg.pairsymb); - traders.emplace_back(stockSelector, sf.create(n), - std::make_unique([aq](auto &&fn) { - aq->push(std::move(fn)); - }, n, rpt, spread_calc_interval, pool), - mcfg, n); - } catch (const std::exception &e) { - logFatal("Error: $1", e.what()); - throw std::runtime_error(std::string("Unable to initialize trader: ").append(n).append(" - ").append(e.what())); - } - } -} - -bool runTraders() { - stockSelector.forEachStock([](json::StrViewA, IStockApi&api) { - api.reset(); - }); - - bool hit = false; - for (auto &&t : traders) { - bool h = t.perform(); - hit |= h; - } - return hit; -} - +std::unique_ptr traders; template auto run_in_worker(Worker wrk, Fn &&fn) -> decltype(fn()) { @@ -215,64 +74,19 @@ auto run_in_worker(Worker wrk, Fn &&fn) -> decltype(fn()) { return *ret; } -class AuthMapper { -public: - - AuthMapper( std::string users, std::string realm):users(users),realm(realm) {} - AuthMapper &operator >>= (simpleServer::HTTPHandler &&hndl) { - handler = std::move(hndl); - return *this; - } - - void operator()(simpleServer::HTTPRequest req) const { - if (!users.empty()) { - auto hdr = req["Authorization"]; - auto hdr_splt = hdr.split(" "); - StrViewA type = hdr_splt(); - StrViewA cred = hdr_splt(); - if (type != "Basic") return genError(req); - auto u_splt = StrViewA(users).split(" "); - bool found = false; - while (!!u_splt && !found) { - StrViewA u = u_splt(); - found = u == cred; - } - if (!found) return genError(req); - } - handler(req); - } - - void genError(simpleServer::HTTPRequest req) const { - req.sendResponse(simpleServer::HTTPResponse(401) - .contentType("text/html") - ("WWW-Authenticate","Basic realm=\""+realm+"\""), - "

401 Unauthorized

" - ); - } - -protected: - AuthMapper( std::string users, std::string realm, simpleServer::HTTPHandler &&handler):users(users), handler(std::move(handler)) {} - std::string users; - std::string realm; - simpleServer::HTTPHandler handler; -}; - static int eraseTradeHandler(Worker &wrk, simpleServer::ArgList args, simpleServer::Stream stream, bool trunc) { if (args.length<2) { stream << "Needsd arguments: \n"; return 1; } else { - auto iter = std::find_if(traders.begin(), traders.end(),[&](const NamedMTrader &tr) { - return StrViewA(tr.ident) == args[0]; - }); - if (iter == traders.end()) { + NamedMTrader *trader = traders->find(args[0]); + if (trader == nullptr) { stream << "Trader idenitification is invalid: " << args[0] << "\n"; return 2; } else { - NamedMTrader &trader = *iter; try { bool res = run_in_worker(wrk, [&] { - return trader.eraseTrade(args[1],trunc); + return trader->eraseTrade(args[1],trunc); }); if (!res) { stream << "Trade not found: " << args[1] << "\n"; @@ -293,52 +107,14 @@ static int cmd_singlecmd(Worker &wrk, simpleServer::ArgList args, simpleServer:: if (args.empty()) { stream << "Need argument: \n"; return 1; } - StrViewA trader = args[0]; - auto iter = std::find_if(traders.begin(), traders.end(), [&](const NamedMTrader &dr){ - return StrViewA(dr.ident) == trader; - }); - if (iter == traders.end()) { - stream << "Trader idenitification is invalid: " << trader << "\n"; - return 1; - } - try { - MTrader &t = *iter; - run_in_worker(wrk, [&]{ - (t.*fn)();return true; - }); - stream << "OK\n"; - return 0; - } catch (std::exception &e) { - stream << e.what() << "\n"; - return 3; - } -} - - - -static int cmd_achieve(Worker &wrk, simpleServer::ArgList args, simpleServer::Stream stream) { - if (args.length != 3) { - stream << "Need arguments: \n"; return 1; - } - - double price = strtod(args[1].data,nullptr); - double balance = strtod(args[2].data,nullptr); - if (price<=0) { - stream << "second argument must be positive real numbers. Use dot (.) as decimal point\n";return 1; - } - - StrViewA trader = args[0]; - auto iter = std::find_if(traders.begin(), traders.end(), [&](const NamedMTrader &dr){ - return StrViewA(dr.ident) == trader; - }); - if (iter == traders.end()) { - stream << "Trader idenitification is invalid: " << trader << "\n"; + NamedMTrader *trader = traders->find(args[0]); + if (trader == nullptr) { + stream << "Trader idenitification is invalid: " << args[0] << "\n"; return 1; } try { - NamedMTrader &t = *iter; run_in_worker(wrk, [&]{ - t.achieve_balance(price,balance);return true; + (trader->*fn)();return true; }); stream << "OK\n"; return 0; @@ -348,120 +124,7 @@ static int cmd_achieve(Worker &wrk, simpleServer::ArgList args, simpleServer::St } } -static int cmd_backtest(Worker &wrk, simpleServer::ArgList args, simpleServer::Stream stream, const std::string &cfgfname, IStockSelector &stockSel, Report &rpt, Stats2Report::SharedPool pool) { - if (args.length < 1) { - stream << "Need arguments: [option=value ...]\n"; return 1; - } - StrViewA trader = args[0]; - auto iter = std::find_if(traders.begin(), traders.end(), [&](const NamedMTrader &dr){ - return StrViewA(dr.ident) == trader; - }); - if (iter == traders.end()) { - stream << "Trader idenitification is invalid: " << trader << "\n"; - return 1; - } - - stream << "Preparing chart\n"; - stream.flush(); - NamedMTrader &t = *iter; - try { - std::vector options; - for (std::size_t i = 1; i < args.length; i++) { - auto arg = args[i]; - auto splt = arg.split("=",2); - StrViewA key = splt(); - StrViewA value = splt(); - key = key.trim(isspace); - value = value.trim(isspace); - options.emplace_back(ondra_shared::IniItem::data, trader, key, value); - } - - std::optional backtest; - BacktestControl::BtReport *btrpt_cntr; - run_in_worker(wrk, [&] { - auto cfg = BacktestControl::loadConfig(cfgfname, trader, options,t.getLastSpread()); - auto btrpt = std::make_unique( - std::make_unique( - [=](CalcSpreadFn &&fn) {fn();}, - "backtest", - rpt, - cfg.calc_spread_minutes,pool)); - btrpt_cntr = btrpt.get(); - t.init(); - backtest.emplace(stockSel, std::move(btrpt), cfg, t.getChart(), t.getInternalBalance()); - return true; - }); - - stream << "Running ('.' - per hour, '+' - report)\n"; - - std::mutex wrlock; - auto wrout = [&](StrViewA x) { - std::lock_guard _(wrlock); - stream << x; - return stream.flush(); - }; - - Scheduler sch = Scheduler::create(); - sch.each(std::chrono::seconds(1)) >> [p = 0,&wrout]() mutable { - char c[2]; - p = (p + 1) % 4; - c[1] = '\b'; - c[0] = "\\|/-"[p]; - wrout(StrViewA(c,2)); - }; - - int mdv = 0; - auto tc = std::chrono::system_clock::now(); - while (backtest->step()) { - auto tn = std::chrono::system_clock::now(); - mdv++; - if (mdv >= 60) { - if (!wrout(".")) break; - mdv = 0; - } - if (std::chrono::duration_cast(tn-tc).count()>50) { - run_in_worker(wrk,[&] { - btrpt_cntr->flush(); - rpt.genReport(); - wrout("+"); - return true; - }); - tc = tn; - } - } - sch.clear(); - stream << "\nGenerating report\n"; - stream.flush(); - run_in_worker(wrk,[&] { - btrpt_cntr->flush(); - rpt.genReport(); - return true; - }); - stream << "Done\n"; - return 0; - } catch (std::exception &e) { - stream << e.what() << "\n"; - return 2; - } - -} -static int cmd_config(Worker &wrk, simpleServer::ArgList args, simpleServer::Stream stream, const ondra_shared::IniConfig &cfg) { - if (args.length < 1) { - stream << "Need argument: \n"; return 1; - } - auto sect = cfg[args[0]]; - std::stringstream buff; - MTrader::showConfig(sect, false, buff); - std::vector list; - buff.seekp(0); - std::string line; - while (std::getline(buff,line)) list.push_back(line); - std::sort(list.begin(),list.end()); - for (auto &&k : list) - stream << k << "\n"; - return 0; -} static ondra_shared::CrashHandler report_crash([](const char *line) { @@ -472,8 +135,19 @@ static ondra_shared::CrashHandler report_crash([](const char *line) { class App: public ondra_shared::DefaultApp { public: - using ondra_shared::DefaultApp::DefaultApp; + App(): ondra_shared::DefaultApp({ + App::Switch{'t',"dry_run",[this](auto &&){this->test = true;},"dry run"}, + App::Switch{'p',"port",[this](auto &&cmd){ + auto p = cmd.getUInt(); + if (p.has_value()) + this->port = *p; + else + throw std::runtime_error("Need port number after -p"); + }," Temporarily opens TCP port"}, + },std::cout) {} + bool test = false; + int port = -1; virtual void showHelp(const std::initializer_list &defsw) { const char *commands[] = { @@ -487,14 +161,10 @@ class App: public ondra_shared::DefaultApp { "status - print status", "pidof - print pid", "wait - wait until service exits", - "logrotate - close and reopen logfile", - "calc_range - calculate and print trading range for each pair", "get_all_pairs- print all tradable pairs - need broker name as argument", "erase_trade - erases trade. Need id of trader and id of trade", "reset - erases all trades expect the last one", - "achieve - achieve an internal state (achieve mode)", "repair - repair pair", - "backtest - backtest", "show_config - shows trader's complete configuration" }; @@ -513,18 +183,16 @@ class App: public ondra_shared::DefaultApp { for (const char *c : commands) wordwrap(c); } + }; + + int main(int argc, char **argv) { try { - bool test = false; -// auto refdir = std::experimental::filesystem::current_path(); - - App app({ - App::Switch{'t',"dry_run",[&](auto &&){test = true;},"dry run"}, - },std::cout); + App app; if (!app.init(argc, argv)) { std::cerr << "Invalid parameters at:" << app.args->getNext() << std::endl; @@ -539,7 +207,6 @@ int main(int argc, char **argv) { auto pidfile = servicesection.mandatory["inst_file"].getPath(); auto name = servicesection["name"].getString("mmbot"); auto user = servicesection["user"].getString(); - auto wrkcnt = servicesection["workers"].getUInt(std::thread::hardware_concurrency()); std::vector argList; while (!!*app.args) argList.push_back(app.args->getNext()); @@ -563,86 +230,97 @@ int main(int argc, char **argv) { cntr.enableRestart(); - cntr.addCommand("logrotate",[=](const simpleServer::ArgList &, simpleServer::Stream ) { - ondra_shared::logRotate(); - return 0; - }); - + Scheduler sch = ondra_shared::Scheduler::create(); - auto lstsect = app.config["traders"]; - auto names = lstsect.mandatory["list"].getString(); - auto storagePath = lstsect.mandatory["storage_path"].getPath(); - auto storageBinary = lstsect["storage_binary"].getBool(true); - auto spreadCalcInterval = lstsect["spread_calc_interval"].getUInt(10); + auto storagePath = servicesection.mandatory["storage_path"].getPath(); + auto storageBinary = servicesection["storage_binary"].getBool(true); + auto listen = servicesection["listen"].getString(); + auto socket = servicesection["socket"].getPath(); auto rptsect = app.config["report"]; auto rptpath = rptsect.mandatory["path"].getPath(); auto rptinterval = rptsect["interval"].getUInt(864000000); - auto a2np = rptsect["a2np"].getBool(false); + auto dr = rptsect["daily_report_service"]; - stockSelector.loadStockMarkets(app.config["brokers"], test); - auto web_bind = rptsect["http_bind"]; - std::unique_ptr srv; - if (web_bind.defined()) { - simpleServer::NetAddr addr = simpleServer::NetAddr::create(web_bind.getString(),11223); - srv = std::make_unique(addr, 1, 1); - (*srv) >>= AuthMapper(rptsect["http_auth"].getString(),name) - >>= simpleServer::HttpFileMapper(std::string(rptpath), "index.html"); - } StorageFactory sf(storagePath,5,storageBinary?Storage::binjson:Storage::json); StorageFactory rptf(rptpath,2,Storage::json); - Report rpt(rptf.create("report.json"), rptinterval, a2np); + Report rpt(rptf.create("report.json"), rptinterval, false); + + + std::unique_ptr perfmod; + if (dr.defined()) + { + std::string cmdline; + std::string workdir; + cmdline = dr.getPath(); + workdir = dr.getCurPath(); + perfmod = std::make_unique(workdir,"performance_module", cmdline); + } else { + perfmod = std::make_unique(sf.create("_performance_daily"), storagePath+"/_performance_current"); + } - Scheduler sch = ondra_shared::Scheduler::create(); Worker wrk = schedulerGetWorker(sch); - Stats2Report::SharedPool pool(wrkcnt); + traders = std::make_unique( + sch,app.config["brokers"], app.test,sf,rpt,*perfmod, rptpath + ); - loadTraders(app.config, names, sf,sch, rpt, test,spreadCalcInterval, pool); + RefCntPtr aul; - logNote("---- Starting service ----"); + RefCntPtr webcfgstate; + StrViewA webadmin_auth = servicesection["admin"].getString(); + webcfgstate = new WebCfg::State(sf.create("web_admin_conf"),new AuthUserList, new AuthUserList); + webcfgstate->setAdminAuth(webadmin_auth); + webcfgstate->applyConfig(*traders); + aul = webcfgstate->users; - cntr.addCommand("calc_range",[&](const simpleServer::ArgList &args, simpleServer::Stream out){ + std::unique_ptr srv; - ondra_shared::Countdown cnt(1); - wrk >> [&] { - try { - for(auto &&t:traders) { ; - std::ostringstream buff; - auto result = t.calc_min_max_range(); - auto ass = t.getMarketInfo().asset_symbol; - auto curs = t.getMarketInfo().currency_symbol; - buff << "Trader " << t.getConfig().title - << ":" << std::endl - << "\tAssets:\t\t\t" << result.assets << " " << ass << std::endl - << "\tAssets value:\t\t" << result.value << " " << curs << std::endl - << "\tAvailable assets:\t" << result.avail_assets << " " << ass << std::endl - << "\tAvailable money:\t" << result.avail_money << " " << curs << std::endl - << "\tMin price:\t\t" << result.min_price << " " << curs << std::endl; - if (result.min_price == 0) - buff << "\t - money left:\t\t" << (result.avail_money-result.value) << " " << curs << std::endl; - buff << "\tMax price:\t\t" << result.max_price << " " << curs << std::endl; - out << buff.str(); - out.flush(); + simpleServer::NetAddr addr(nullptr); + if (!socket.empty()) { + addr = simpleServer::NetAddr::create(std::string("unix:")+socket,11223); + } + if (!listen.empty()) { + simpleServer::NetAddr baddr = simpleServer::NetAddr::create(listen,11223); + if (addr.getHandle() == nullptr) addr = baddr; + else addr = addr + baddr; + } - } - } catch (std::exception &e) { - out << e.what(); - } - cnt.dec(); - }; - cnt.wait(); + if (app.port>0) { + simpleServer::NetAddr baddr = simpleServer::NetAddr::create("0",app.port); + if (addr.getHandle() == nullptr) addr = baddr; + else addr = addr + baddr; + } - return 0; - }); + if (addr.getHandle() != nullptr) { + srv = std::make_unique(addr, 1, 1); + + + std::vector paths; + paths.push_back(simpleServer::HttpStaticPathMapper::MapRecord{ + "/",AuthMapper(name,aul) >>= simpleServer::HttpFileMapper(std::string(rptpath), "index.html") + }); + + paths.push_back({ + "/admin",ondra_shared::shared_function(WebCfg(webcfgstate, + name, + *traders, + [=](WebCfg::Action &&a) mutable {sch.immediate() >> std::move(a);})) + }); + (*srv) >>= simpleServer::HttpStaticPathMapperHandler(paths); + } + + + + logNote("---- Starting service ----"); cntr.addCommand("get_all_pairs",[&](simpleServer::ArgList args, simpleServer::Stream stream){ if (args.length < 1) { @@ -674,18 +352,9 @@ int main(int argc, char **argv) { cntr.addCommand("reset", [&](simpleServer::ArgList args, simpleServer::Stream stream){ return cmd_singlecmd(wrk, args,stream,&MTrader::reset); }); - cntr.addCommand("achieve", [&](simpleServer::ArgList args, simpleServer::Stream stream){ - return cmd_achieve(wrk, args,stream); - }); cntr.addCommand("repair", [&](simpleServer::ArgList args, simpleServer::Stream stream){ return cmd_singlecmd(wrk, args,stream,&MTrader::repair); }); - cntr.addCommand("backtest", [&](simpleServer::ArgList args, simpleServer::Stream stream){ - return cmd_backtest(wrk, args, stream, app.configPath.string(), stockSelector, rpt, pool); - }); - cntr.addCommand("show_config", [&](simpleServer::ArgList args, simpleServer::Stream stream){ - return cmd_config(wrk, args, stream, app.config); - }); std::size_t id = 0; cntr.addCommand("run",[&](simpleServer::ArgList, simpleServer::Stream) { @@ -702,7 +371,8 @@ int main(int argc, char **argv) { try { - runTraders(); + traders->runTraders(false); + rpt.perfReport(perfmod->getReport()); rpt.genReport(); } catch (std::exception &e) { logError("Scheduler exception: $1", e.what()); @@ -717,17 +387,17 @@ int main(int argc, char **argv) { return 0; }); + cntr.dispatch(); sch.remove(id); sch.sync(); - traders.clear(); - stockSelector.clear(); - + traders->clear(); } logNote("---- Exit ----"); - return 0; + + return 0; }, simpleServer::ArgList(argList.data(), argList.size()), cmd == "calc_range" || cmd == "get_all_pairs" || cmd == "achieve" || cmd == "reset" || cmd=="repair" || cmd == "backtest" || cmd == "show_config"); @@ -736,8 +406,7 @@ int main(int argc, char **argv) { return 2; } } else { - std::cerr << "Missing arguments. Use -h to show help" << std::endl; - return 1; + std::cout << "use -h for help" << std::endl; } } catch (std::exception &e) { std::cerr << "Error:" << e.what() << std::endl; diff --git a/src/main/mtrader.cpp b/src/main/mtrader.cpp index dba4d2d3..0886ff27 100644 --- a/src/main/mtrader.cpp +++ b/src/main/mtrader.cpp @@ -2,6 +2,7 @@ #include #include "istockapi.h" #include "mtrader.h" +#include "strategy.h" #include #include @@ -9,11 +10,18 @@ #include #include #include +#include +#include +#include "../shared/stringview.h" #include "emulator.h" +#include "ibrokercontrol.h" #include "sgn.h" +using ondra_shared::logDebug; +using ondra_shared::logInfo; using ondra_shared::logNote; +using ondra_shared::StringView; using ondra_shared::StrViewA; json::NamedEnum strDynmult_mode ({ @@ -23,154 +31,93 @@ json::NamedEnum strDynmult_mode ({ {Dynmult_mode::half_alternate, "half_alternate"} }); -json::NamedEnum strNeutralPosType ({ - {MTrader::Config::assets, "assets"}, - {MTrader::Config::currency, "currency"}, - {MTrader::Config::center, "center"}, - {MTrader::Config::disabled, "disabled"} -}); - - std::string_view MTrader::vtradePrefix = "__vt__"; -MTrader::MTrader(IStockSelector &stock_selector, - StoragePtr &&storage, - PStatSvc &&statsvc, - Config config) -:stock(selectStock(stock_selector,config,ownedStock)) -,cfg(std::move(config)) -,storage(std::move(storage)) -,statsvc(std::move(statsvc)) -{ - //probe that broker is valid configured - stock.testBroker(); - magic = this->statsvc->getHash() & 0xFFFFFFFF; -} - - -void MTrader::Config::parse_neutral_pos(StrViewA txt) { - if (txt.empty()) { - neutral_pos = 0; - neutralPosType = disabled; - } else { - auto splt = txt.split(" ",2); - StrViewA type = splt(); - StrViewA value = splt(); - - if (value.empty()) { - neutralPosType = assets; - neutral_pos = strtod(type.data,nullptr); - } else { - neutralPosType = strNeutralPosType[type]; - neutral_pos =strtod(value.data,nullptr); - } - } +static double default_value(json::Value data, double defval) { + if (data.type() == json::number) return data.getNumber(); + else return defval; } - -template -void unsupported(Ini ini, - const std::initializer_list &options, - std::string_view desc) { - - for (auto &&x: options) { - if (ini[x].defined()) { - throw std::runtime_error(std::string(x).append(" - option is no longer supported. ").append(desc)); - } - } - +static StrViewA default_value(json::Value data, StrViewA defval) { + if (data.type() == json::string) return data.getString(); + else return defval; +} +/*static intptr_t default_value(json::Value data, intptr_t defval) { + if (data.type() == json::number) return data.getInt(); + else return defval; +}*/ +static uintptr_t default_value(json::Value data, uintptr_t defval) { + if (data.type() == json::number) return data.getUInt(); + else return defval; +} +static bool default_value(json::Value data, bool defval) { + if (data.type() == json::boolean) return data.getBool(); + else return defval; } -template -MTrader::Config load_internal(Ini ini, bool force_dry_run) { - - MTrader::Config cfg; - - - cfg.broker = ini.mandatory["broker"].getString(); - cfg.spread_calc_mins = ini["spread_calc_hours"].getUInt(24*5)*60; - cfg.spread_calc_min_trades = ini["spread_calc_min_trades"].getUInt(4); - cfg.spread_calc_max_trades = ini["spread_calc_max_trades"].getUInt(24); - cfg.pairsymb = ini.mandatory["pair_symbol"].getString(); - - cfg.buy_mult = ini["buy_mult"].getNumber(1.0); - cfg.sell_mult = ini["sell_mult"].getNumber(1.0); - - cfg.buy_step_mult = ini["buy_step_mult"].getNumber(1.0); - cfg.sell_step_mult = ini["sell_step_mult"].getNumber(1.0); - cfg.external_assets = ini["external_assets"].getNumber(0); - cfg.min_size = ini["min_size"].getNumber(0); - cfg.max_size = ini["max_size"].getNumber(0); - cfg.expected_trend = ini["expected_trend"].getNumber(0); - cfg.report_position_offset = ini["report_position_offset"].getNumber(0); - - - cfg.dry_run = force_dry_run?true:ini["dry_run"].getBool(false); - cfg.internal_balance = cfg.dry_run?true:ini["internal_balance"].getBool(false); - cfg.detect_manual_trades = ini["detect_manual_trades"].getBool(true); - cfg.enabled = ini["enable"].getBool(true); - - unsupported(ini, { - "sliding_pos.change", - "sliding_pos.acum", - "sliding_pos.assets", - "sliding_pos.currency", - "sliding_pos.center", - "sliding_pos.max_pos", - "sliding_pos.giveup", - "sliding_pos.recoil", - }, "Check manual for new sliding_pos options"); - - StrViewA neutral_pos_str = ini["neutral_pos"].getString(""); - cfg.parse_neutral_pos(neutral_pos_str); +void MTrader_Config::loadConfig(json::Value data, bool force_dry_run) { + pairsymb = data["pair_symbol"].getString(); + broker = data["broker"].getString(); + title = data["title"].getString(); - double default_accum = ini["acum_factor"].getNumber(0); - cfg.acm_factor_buy = ini["acum_factor_buy"].getNumber(default_accum); - cfg.acm_factor_sell = ini["acum_factor_sell"].getNumber(default_accum); + auto strdata = data["strategy"]; + auto strstr = strdata["type"].toString(); + strategy = Strategy::create(strstr.str(), strdata); - cfg.dynmult_raise = ini["dynmult_raise"].getNumber(250); - cfg.dynmult_fall = ini["dynmult_fall"].getNumber(0.5); - cfg.dynmult_mode = strDynmult_mode[ini["dynmult_mode"].getString(strDynmult_mode[Dynmult_mode::half_alternate])]; - cfg.emulated_currency = ini["emulated_currency"].getNumber(0); - cfg.force_spread = ini["force_spread"].getNumber(0); - cfg.force_margin = ini["force_margin"].getBool(); - cfg.dust_orders = ini["dust_orders"].getBool(true); + buy_mult = default_value(data["buy_mult"],1.0); + sell_mult = default_value(data["sell_mult"],1.0); + buy_step_mult = default_value(data["buy_step_mult"],1.0); + sell_step_mult = default_value(data["sell_step_mult"],1.0); + min_size = default_value(data["min_size"],0.0); + max_size = default_value(data["max_size"],0.0); - cfg.accept_loss = ini["accept_loss"].getUInt(0); - cfg.max_pos = ini["max_pos"].getNumber(0); + dynmult_raise = default_value(data["dynmult_raise"],0.0); + dynmult_fall = default_value(data["dynmult_fall"],0.0); + dynmult_mode = strDynmult_mode[default_value(data["dynmult_mode"], StrViewA("half_alternate"))]; - cfg.sliding_pos_hours = ini["sliding_pos.hours"].getNumber(0); - cfg.sliding_pos_weaken = ini["sliding_pos.weaken"].getNumber(0); + accept_loss = default_value(data["accept_loss"], static_cast(1)); - cfg.title = ini["title"].getString(); + force_spread = default_value(data["force_spread"], 0.0); + report_position_offset = default_value(data["report_position_offset"], 0.0); + spread_calc_sma_hours = default_value(data["spread_calc_sma_hours"], static_cast(2))*60; + spread_calc_stdev_hours = default_value(data["spread_calc_stdev_hours"], static_cast(8))*60; - cfg.start_time = ini["start_time"].getUInt(0); + dry_run = force_dry_run || default_value(data["dry_run"], false); + internal_balance = default_value(data["internal_balance"], false); + detect_manual_trades= default_value(data["detect_manual_trades"], false); + enabled= default_value(data["enabled"], true); + dust_orders= default_value(data["dust_orders"], true); + dynmult_scale = default_value(data["dynmult_scale"], true); - if (cfg.spread_calc_mins > 1000000) throw std::runtime_error("spread_calc_hours is too big"); - if (cfg.spread_calc_min_trades > cfg.spread_calc_max_trades) throw std::runtime_error("'spread_calc_min_trades' must bee less then 'spread_calc_max_trades'"); - if (cfg.spread_calc_max_trades > 24*60) throw std::runtime_error("'spread_calc_max_trades' is too big"); - if (cfg.acm_factor_buy > 50) throw std::runtime_error("'acum_factor_buy' is too big"); - if (cfg.acm_factor_buy < -50) throw std::runtime_error("'acum_factor_buy' is too small"); - if (cfg.acm_factor_sell > 50) throw std::runtime_error("'acum_factor_sell' is too big"); - if (cfg.acm_factor_sell < -50) throw std::runtime_error("'acum_factor_sell' is too small"); - if (cfg.dynmult_raise > 1e6) throw std::runtime_error("'dynmult_raise' is too big"); - if (cfg.dynmult_raise < 0) throw std::runtime_error("'dynmult_raise' is too small"); - if (cfg.dynmult_fall > 100) throw std::runtime_error("'dynmult_fall' must be below 100"); - if (cfg.dynmult_fall <= 0) throw std::runtime_error("'dynmult_fall' must not be negative or zero"); - if (cfg.max_pos <0) throw std::runtime_error("'max_pos' must not be negative"); - if ((cfg.max_pos || cfg.sliding_pos_hours) && cfg.neutralPosType == MTrader::Config::disabled) { - throw std::runtime_error("Some option needs to define neutral_pos"); - } + if (dynmult_raise > 1e6) throw std::runtime_error("'dynmult_raise' is too big"); + if (dynmult_raise < 0) throw std::runtime_error("'dynmult_raise' is too small"); + if (dynmult_fall > 100) throw std::runtime_error("'dynmult_fall' must be below 100"); + if (dynmult_fall <= 0) throw std::runtime_error("'dynmult_fall' must not be negative or zero"); - return cfg; } +MTrader::MTrader(IStockSelector &stock_selector, + StoragePtr &&storage, + PStatSvc &&statsvc, + Config config) +:stock(selectStock(stock_selector,config,ownedStock)) +,cfg(config) +,storage(std::move(storage)) +,statsvc(std::move(statsvc)) +,strategy(config.strategy) +{ + //probe that broker is valid configured + stock.testBroker(); + magic = this->statsvc->getHash() & 0xFFFFFFFF; + std::random_device rnd; + uid = 0; + while (!uid) { + uid = rnd(); + } -MTrader::Config MTrader::load(const ondra_shared::IniConfig::Section& ini, bool force_dry_run) { - return load_internal(ini, force_dry_run); } @@ -183,7 +130,7 @@ IStockApi &MTrader::selectStock(IStockSelector &stock_selector, const Config &co IStockApi *s = stock_selector.getStock(conf.broker); if (s == nullptr) throw std::runtime_error(std::string("Unknown stock market name: ")+std::string(conf.broker)); if (conf.dry_run) { - ownedStock = std::make_unique(*s, conf.emulated_currency); + ownedStock = std::make_unique(*s, 0); return *ownedStock; } else { return *s; @@ -192,20 +139,20 @@ IStockApi &MTrader::selectStock(IStockSelector &stock_selector, const Config &co double MTrader::raise_fall(double v, bool raise) const { if (raise) { - double rr = (1.0+cfg.dynmult_raise/100.0); - return v * rr; + double rr = cfg.dynmult_raise/100.0; + return v + rr; } else { - double ff = (1.0-cfg.dynmult_fall/100.0); - return std::max(1.0,v * ff); + double ff = cfg.dynmult_fall/100.0; + return std::max(1.0,v - ff); } } - +/* static auto calc_margin_range(double A, double D, double P) { double x1 = (A*P - 2*sqrt(A*D*P) + D)/A; double x2 = (A*P + 2*sqrt(A*D*P) + D)/A; return std::make_pair(x1,x2); } - +*/ void MTrader::init() { if (need_load){ @@ -214,249 +161,147 @@ void MTrader::init() { } } -double MTrader::calcWeakenMult(double neutral_pos, double balance) { - - if (neutral_pos && cfg.sliding_pos_weaken ) { - double maxpos = cfg.external_assets* cfg.sliding_pos_weaken * 0.01; - double curpos = balance - neutral_pos; - double mult = (maxpos - fabs(curpos))/maxpos; - if (mult < 1e-10) mult = 1e-10; - return mult; - } else { - return 1.0; - } +const MTrader::TradeHistory& MTrader::getTrades() const { + return trades; } -int MTrader::perform() { +void MTrader::perform(bool manually) { try { - init(); - double begbal = internal_balance + cfg.external_assets; - - //Get opened orders - auto orders = getOrders(); - //get current status - auto status = getMarketStatus(); - - std::string buy_order_error; - std::string sell_order_error; - - double neutral_pos=0; - - - switch (cfg.neutralPosType) { - case Config::center: { - double a = 1.0/(cfg.neutral_pos+1.0); - neutral_pos = ((status.assetBalance-cfg.external_assets) * status.curPrice + currency_balance_cache)*a/status.curPrice+cfg.external_assets; - }break; - case Config::currency: - neutral_pos = (currency_balance_cache-cfg.neutral_pos)/status.curPrice+status.assetBalance; - break; - case Config::assets: - neutral_pos = cfg.neutral_pos + cfg.external_assets; - break; - default: - neutral_pos = 0; - break; - } - ondra_shared::logDebug("Neutral pos: $1", neutral_pos); - - - //update market fees - minfo.fees = status.new_fees; - //process all new trades - auto ptres =processTrades(status, first_order); - //merge trades on same price - mergeTrades(trades.size() - status.new_trades.size()); - double lastTradePrice = trades.empty()?status.curPrice:trades.back().eff_price; + //Get opened orders + auto orders = getOrders(); + //get current status + auto status = getMarketStatus(); - bool calcadj = false; - double weakenMult = calcWeakenMult(neutral_pos, status.assetBalance); - - //if calculator is not valid, update it using current price and assets - if (!calculator.isValid()) { - calculator.update(lastTradePrice, status.assetBalance); - calcadj = true; - } - if (!calculator.isValid()) { - ondra_shared::logError("No asset balance is available. Buy some assets, use 'external_assets=' or use command achieve to invoke automatic initial trade"); - } else { + std::string buy_order_error; + std::string sell_order_error; + //update market fees + minfo.fees = status.new_fees; + //process all new trades + processTrades(status); + //merge trades on same price + mergeTrades(trades.size() - status.new_trades.trades.size()); + double lastTradePrice = !trades.empty()?trades.back().eff_price:strategy.isValid()?strategy.getEquilibrium():status.curPrice; + double lastTradeSize = trades.empty()?0:trades.back().eff_size; - double acm_buy, acm_sell; -/* if (cfg.sliding_pos_acm) { - double f = sgn(neutral_pos-status.assetBalance); - acm_buy = cfg.acm_factor_buy * f; - acm_sell = cfg.acm_factor_sell * f; - ondra_shared::logDebug("Sliding pos: acum_factor_buy=$1, acum_factor_sell=$2", acm_buy, acm_sell); - } else {*/ - acm_buy = cfg.acm_factor_buy; - acm_sell = cfg.acm_factor_sell; - /*}*/ //only create orders, if there are no trades from previous run - if (status.new_trades.empty()) { + if (status.new_trades.trades.empty()) { - ondra_shared::logDebug("internal_balance=$1, external_balance=$2",status.internalBalance,status.assetBalance); - if ( !similar(status.internalBalance ,status.assetBalance,1e-5)) { - //when balance changes, we need to update calculator - ondra_shared::logWarning("Detected balance change: $1 => $2", status.internalBalance, status.assetBalance); - calculator.update(lastTradePrice, status.assetBalance); - calcadj = true; - internal_balance=status.assetBalance - cfg.external_assets; + if (recalc) { + update_dynmult(lastTradeSize > 0, lastTradeSize < 0); } + if (!strategy.isValid()) { + strategy.init(status.curPrice, status.assetBalance, status.currencyBalance); + } + ondra_shared::logDebug("internal_balance=$1, external_balance=$2",status.internalBalance,status.assetBalance); //calculate buy order auto buyorder = calculateOrder(lastTradePrice, - -status.curStep*buy_dynmult*cfg.buy_step_mult, - status.curPrice, status.assetBalance, acm_buy, - cfg.buy_mult * weakenMult); + -status.curStep*cfg.buy_step_mult, buy_dynmult, + status.ticker.bid, status.assetBalance, + cfg.buy_mult); //calculate sell order auto sellorder = calculateOrder(lastTradePrice, - status.curStep*sell_dynmult*cfg.sell_step_mult, - status.curPrice, status.assetBalance, acm_sell, - cfg.sell_mult * weakenMult); - - try { - setOrderCheckMaxPos(orders.buy, buyorder,status.assetBalance, neutral_pos); - } catch (std::exception &e) { - buy_order_error = e.what(); - if (!acceptLoss(orders.buy, buyorder, status, neutral_pos)) { - orders.buy = buyorder; + status.curStep*cfg.sell_step_mult, sell_dynmult, + status.ticker.ask, status.assetBalance, + cfg.sell_mult); + + if (!cfg.enabled) { + if (orders.buy.has_value()) + stock.placeOrder(cfg.pairsymb,0,0,magic,orders.buy->id,0); + if (orders.sell.has_value()) + stock.placeOrder(cfg.pairsymb,0,0,magic,orders.sell->id,0); + statsvc->reportError(IStatSvc::ErrorObj("Automatic trading is disabled")); + } else { + try { + setOrder(orders.buy, buyorder); + } catch (std::exception &e) { + buy_order_error = e.what(); + if (!acceptLoss(orders.buy, buyorder, status)) { + orders.buy = buyorder; + } } - } - try { - setOrderCheckMaxPos(orders.sell, sellorder, status.assetBalance, neutral_pos); - } catch (std::exception &e) { - sell_order_error = e.what(); - if (!acceptLoss(orders.sell, sellorder, status, neutral_pos)) { - orders.sell = sellorder; + try { + setOrder(orders.sell, sellorder); + } catch (std::exception &e) { + sell_order_error = e.what(); + if (!acceptLoss(orders.sell, sellorder, status)) { + orders.sell = sellorder; + } + } + if (!recalc && !manually) { + update_dynmult(false,false); } - } - //replace order on stockmarket - //remember the orders (keep previous orders as well) - std::swap(lastOrders[0],lastOrders[1]); - lastOrders[0] = orders; - update_dynmult(false,false); + //report order errors to UI + statsvc->reportError(IStatSvc::ErrorObj(buy_order_error, sell_order_error)); - } else { - const auto &lastTrade = trades.back(); - //update after trade - if (!ptres.manual_trades) { - calculator.update_after_trade(lastTrade.eff_price, status.assetBalance, - begbal, lastTrade.eff_size<0?acm_sell:acm_buy); - calcadj = true; } - currency_balance_cache = stock.getBalance(minfo.currency_symbol); - //we need change trend_adv slower to avoid shocks - cur_trend_adv = cur_trend_adv + (cfg.expected_trend - cur_trend_adv)*0.05; - - - if (cfg.sliding_pos_hours && trades.size()>1) { - const auto & pt = trades[trades.size()-2]; - const auto & ct = lastTrade; - double tdf = ct.time - pt.time; - if (tdf > 0) { - double tot = cfg.sliding_pos_hours * 3600 * 1000; - double pos = pt.balance - neutral_pos; - double pldiff = pos * (ct.eff_price - pt.eff_price); - double eq = calculator.balance2price(neutral_pos); - double neq = (pldiff*tot)>0? eq + (ct.price - eq) * (tdf/fabs(tot)):eq; - calculator = Calculator(neq, neutral_pos, false); - ondra_shared::logDebug("sliding_pos.hours: tdf=$1 pos=$2 pldiff=$3 eq=$4 neq=$5", - tdf, pos, pldiff, eq, neq); - - } - } + recalc = false; + } else { - update_dynmult(!orders.buy.has_value() && lastTrade.size > 0, - !orders.sell.has_value() && lastTrade.size < 0); + recalc = true; } + //report orders to UI + statsvc->reportOrders(orders.buy,orders.sell); + //report trades to UI + statsvc->reportTrades(trades); + //report price to UI + statsvc->reportPrice(status.curPrice); + //report misc + { + auto minmax = strategy.calcSafeRange(status.assetBalance, status.currencyBalance); + + statsvc->reportMisc(IStatSvc::MiscData{ + status.new_trades.trades.empty()?0:sgn(status.new_trades.trades.back().size), + strategy.getEquilibrium(), + status.curPrice * (exp(status.curStep) - 1), + buy_dynmult, + sell_dynmult, + minmax.min, + minmax.max, + trades.size(), + trades.empty()?0:(trades.back().time-trades[0].time) + }); - - - if (calcadj) { - double c = calculator.balance2price(1.0); - ondra_shared::logNote("Calculator adjusted: $1 at $2, ref_price=$3 ($4)", calculator.getBalance(), calculator.getPrice(), c, c - prev_calc_ref); - prev_calc_ref = c; } - } - - //report orders to UI - statsvc->reportOrders(orders.buy,orders.sell); - //report order errors to UI - statsvc->reportError(IStatSvc::ErrorObj(buy_order_error, sell_order_error)); - //report trades to UI - statsvc->reportTrades(trades, minfo.leverage || cfg.force_margin); - //report price to UI - statsvc->reportPrice(status.curPrice); - //report misc - { - double value = status.assetBalance * status.curPrice; - double max_price = pow2((status.assetBalance * sqrt(status.curPrice))/cfg.external_assets); - double S = value - currency_balance_cache; - double min_price = S<=0?0:pow2(S/(status.assetBalance*sqrt(status.curPrice))); - double b1 = isfinite(max_price)?calculator.price2balance(sqrt(max_price*min_price)):1; - double boost = b1/(b1-cfg.external_assets); - - if (minfo.leverage && cfg.external_assets > 0) { - double start_price = calculator.balance2price(cfg.external_assets); - double cur_price = calculator.balance2price(status.assetBalance); - double colateral = (currency_balance_cache+(start_price-sqrt(start_price*cur_price))*internal_balance )* (1 - 1 / minfo.leverage); - auto range = calc_margin_range(cfg.external_assets, colateral, start_price); - max_price = range.second; - min_price = range.first; - boost = cfg.external_assets*start_price / colateral; + if (!manually) { + //store current price (to build chart) + chart.push_back(status.chartItem); + { + //delete very old data from chart + unsigned int max_count = std::max(cfg.spread_calc_sma_hours, cfg.spread_calc_stdev_hours); + if (chart.size() > max_count) + chart.erase(chart.begin(),chart.end()-max_count); + } } - statsvc->reportMisc(IStatSvc::MiscData{ - status.new_trades.empty()?0:sgn(status.new_trades.back().size), - calculator.isAchieveMode(), - calculator.balance2price(status.assetBalance), - status.curPrice * (exp(status.curStep) - 1), - buy_dynmult, - sell_dynmult, - weakenMult, - 2 * value, - boost, - min_price, - max_price, - trades.size(), - trades.empty()?0:(trades.back().time-trades[0].time) - }); - - } - - //store current price (to build chart) - chart.push_back(status.chartItem); - //delete very old data from chart - if (chart.size() > cfg.spread_calc_mins) - chart.erase(chart.begin(),chart.end()-cfg.spread_calc_mins); + internal_balance = status.internalBalance; + currency_balance_cache = status.currencyBalance; + lastTradeId = status.new_trades.lastId; - //if this was first order, the next will not first order - first_order = false; - //save state - saveState(); + //save state + saveState(); - return 0; } catch (std::exception &e) { statsvc->reportError(IStatSvc::ErrorObj(e.what())); throw; @@ -494,24 +339,6 @@ MTrader::OrderPair MTrader::getOrders() { return ret; } -void MTrader::setOrderCheckMaxPos(std::optional &orig, Order neworder, double balance, double neutral_pos) { - if (cfg.max_pos) { - double final_pos = balance + neworder.size; - if (final_pos > neutral_pos + cfg.max_pos) - throw std::runtime_error("Max position reached"); - if (final_pos < neutral_pos - cfg.max_pos) - throw std::runtime_error("Min position reached"); - } - if (cfg.enabled) { - setOrder(orig, neworder); - } else { - if (orig.has_value()) { - stock.placeOrder(cfg.pairsymb, 0, 0, 0, orig->id, 0); - } - throw std::runtime_error("Disabled (enable=off)"); - } - -} void MTrader::setOrder(std::optional &orig, Order neworder) { try { @@ -566,31 +393,39 @@ MTrader::Status MTrader::getMarketStatus() const { json::Value lastId; if (!trades.empty()) lastId = getTradeLastId(); - res.new_trades = stock.getTrades(lastId, cfg.start_time, cfg.pairsymb); + + res.new_trades = stock.syncTrades(lastTradeId, cfg.pairsymb); { - double balance = 0; - for (auto &&t:res.new_trades) balance+=t.eff_size; - res.internalBalance = internal_balance + balance + cfg.external_assets; + res.internalBalance = std::accumulate(res.new_trades.trades.begin(), + res.new_trades.trades.end(),0.0, + [&](auto &&a, auto &&b) {return a + b.eff_size;}); + if (internal_balance.has_value()) res.internalBalance += *internal_balance; } if (cfg.internal_balance) { res.assetBalance = res.internalBalance; } else{ - res.assetBalance = stock.getBalance(minfo.asset_symbol)+ cfg.external_assets; + res.assetBalance = stock.getBalance(minfo.asset_symbol); + } + + if (!res.new_trades.trades.empty()) { + res.currencyBalance = stock.getBalance(minfo.currency_symbol); + } else { + res.currencyBalance = *currency_balance_cache; } - auto step = cfg.force_spread>0?cfg.force_spread:statsvc->calcSpread(chart,cfg,minfo,res.assetBalance,prev_spread); + auto step = cfg.force_spread>0?cfg.force_spread:calcSpread(); res.curStep = step; - prev_spread = step; res.new_fees = stock.getFees(cfg.pairsymb); auto ticker = stock.getTicker(cfg.pairsymb); + res.ticker = ticker; res.curPrice = std::sqrt(ticker.ask*ticker.bid); res.chartItem.time = ticker.time; @@ -605,14 +440,15 @@ MTrader::Status MTrader::getMarketStatus() const { MTrader::Order MTrader::calculateOrderFeeLess( double prevPrice, double step, + double dynmult, double curPrice, double balance, - double acm, double mult) const { Order order; - double newPrice = prevPrice * exp(step); - double fact = acm; + + double newPrice = prevPrice * exp(step*dynmult); + double newPriceNoScale= prevPrice * exp(step); if (step < 0) { //if price is lower than old, check whether current price is above @@ -625,17 +461,9 @@ MTrader::Order MTrader::calculateOrderFeeLess( if (newPrice < curPrice) newPrice = curPrice; } - Calculator ccalc (calculator.getPrice() * (1+ cur_trend_adv * 0.01 * (minfo.invert_price?-1:1)), - calculator.getBalance(),false); + double size = strategy.calcOrderSize(cfg.dynmult_scale?newPrice:newPriceNoScale, balance); - double newBalance = ccalc.price2balance(newPrice); - double base = (newBalance - balance); - double extra = ccalc.calcExtra(prevPrice, newPrice); - double size = base +extra*fact; - - ondra_shared::logDebug("Set order: step=$1, base_price=$6, price=$2, base=$3, extra=$4, total=$5",step, newPrice, base, extra, size, prevPrice); - - if (size * step > 0) { + if (size * step >= 0) { if (cfg.dust_orders) { size = -sgn(step)*minfo.min_size; } else { @@ -654,13 +482,12 @@ MTrader::Order MTrader::calculateOrderFeeLess( MTrader::Order MTrader::calculateOrder( double lastTradePrice, double step, + double dynmult, double curPrice, double balance, - double acm, double mult) const { - Order order(calculateOrderFeeLess(lastTradePrice, step,curPrice,balance,acm,mult)); - + Order order(calculateOrderFeeLess(lastTradePrice, step,dynmult,curPrice,balance,mult)); if (cfg.max_size && std::fabs(order.size) > cfg.max_size) { order.size = cfg.max_size*sgn(order.size); @@ -690,48 +517,49 @@ MTrader::Order MTrader::calculateOrder( void MTrader::loadState() { minfo = stock.getMarketInfo(cfg.pairsymb); + std::string brokerImg; + const IBrokerIcon *bicon = dynamic_cast(&stock); + if (bicon) brokerImg = bicon->getIconName(); this->statsvc->setInfo( IStatSvc::Info { cfg.title, minfo.asset_symbol, minfo.currency_symbol, minfo.invert_price?minfo.inverted_symbol:minfo.currency_symbol, + brokerImg, cfg.report_position_offset, minfo.invert_price, - minfo.leverage || cfg.force_margin, - stock.isTest() + minfo.leverage != 0, + minfo.simulator }); currency_balance_cache = stock.getBalance(minfo.currency_symbol); + + if (storage == nullptr) return; auto st = storage->load(); need_load = false; - bool wastest = false; - auto curtest = stock.isTest(); - bool drop_calc = false; - - bool recalc_trades = false; + if (!cfg.dry_run) { + json::Value t = st["test_backup"]; + if (t.defined()) { + st = t.replace("chart",st["chart"]); + } + } if (st.defined()) { - json::Value tst = st["testStartTime"]; - wastest = tst.defined(); - drop_calc = drop_calc || (wastest != curtest); - testStartTime = tst.getUInt(); auto state = st["state"]; if (state.defined()) { - if (!curtest) { - buy_dynmult = state["buy_dynmult"].getNumber(); - sell_dynmult = state["sell_dynmult"].getNumber(); - } - prev_spread = state["lnspread"].getNumber(); + buy_dynmult = state["buy_dynmult"].getNumber(); + sell_dynmult = state["sell_dynmult"].getNumber(); internal_balance = state["internal_balance"].getNumber(); - double ext_ass = state["external_assets"].getNumber(); - if (ext_ass != cfg.external_assets) drop_calc = true; - cur_trend_adv = state["trend"].getNumber(); + recalc = state["recalc"].getBool(); + std::size_t nuid = state["uid"].getUInt(); + if (nuid) uid = nuid; + lastTradeId = state["lastTradeId"]; } auto chartSect = st["chart"]; if (chartSect.defined()) { @@ -739,12 +567,10 @@ void MTrader::loadState() { for (json::Value v: chartSect) { double ask = v["ask"].getNumber(); double bid = v["bid"].getNumber(); - json::Value vlast = v["last"]; - double last = vlast.defined()?vlast.getNumber():sqrt(ask*bid); + double last = v["last"].getNumber(); + std::uint64_t tm = v["time"].getUIntLong(); - chart.push_back({ - v["time"].getUInt(),ask,bid,last - }); + chart.push_back({tm,ask,bid,last}); } } { @@ -753,60 +579,38 @@ void MTrader::loadState() { trades.clear(); for (json::Value v: trSect) { TWBItem itm = TWBItem::fromJSON(v); - if (wastest && !curtest && itm.time > testStartTime ) { - continue; - } else { - trades.push_back(itm); - recalc_trades = recalc_trades || std::isnan(itm.balance); - } + trades.push_back(itm); } } mergeTrades(0); } - if (!drop_calc) { - calculator = Calculator::fromJSON(st["calc"]); - lastOrders[0] = OrderPair::fromJSON(st["orders"][0]); - lastOrders[1] = OrderPair::fromJSON(st["orders"][1]); - } - } - if (curtest && testStartTime == 0) { - testStartTime = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch() - ).count(); - } - if (recalc_trades) { - double endBal = stock.getBalance(minfo.asset_symbol) + cfg.external_assets; - double chng = std::accumulate(trades.begin(), trades.end(),0.0,[](auto &&a, auto &&b) { - return a + b.eff_size; - }); - double begBal = endBal-chng; - for (auto &&t:trades) { - if (t.balance < 0) t.balance = begBal+t.eff_size; - begBal = t.balance; + strategy.importState(st["strategy"]); + if (cfg.dry_run) { + test_backup = st["test_backup"]; + if (!test_backup.hasValue()) { + test_backup = st.replace("chart",json::Value()); + } } } - if (internal_balance == 0) { - if (!trades.empty()) internal_balance = trades.back().balance- cfg.external_assets; - } + tempPr.broker = cfg.broker; + tempPr.magic = magic; + tempPr.uid = uid; + tempPr.currency = minfo.currency_symbol; + } void MTrader::saveState() { if (storage == nullptr) return; json::Object obj; - obj.set("version",2); - if (stock.isTest()) { - obj.set("testStartTime", testStartTime);; - } - { auto st = obj.object("state"); st.set("buy_dynmult", buy_dynmult); st.set("sell_dynmult", sell_dynmult); - st.set("lnspread", prev_spread); - st.set("internal_balance", internal_balance); - st.set("external_assets", cfg.external_assets); - st.set("trend",cur_trend_adv); + st.set("internal_balance", *internal_balance); + st.set("recalc",recalc); + st.set("uid",uid); + st.set("lastTradeId",lastTradeId); } { auto ch = obj.array("chart"); @@ -823,29 +627,13 @@ void MTrader::saveState() { tr.push_back(itm.toJSON()); } } - obj.set("calc", calculator.toJSON()); - obj.set("orders", {lastOrders[0].toJSON(),lastOrders[1].toJSON()}); + obj.set("strategy",strategy.exportState()); + if (test_backup.hasValue()) { + obj.set("test_backup", test_backup); + } storage->store(obj); } -MTrader::CalcRes MTrader::calc_min_max_range() { - - CalcRes res {}; - loadState(); - - - res.avail_assets = stock.getBalance(minfo.asset_symbol); - res.avail_money = stock.getBalance(minfo.currency_symbol); - res.cur_price = stock.getTicker(cfg.pairsymb).last; - res.assets = res.avail_assets+cfg.external_assets; - res.value = res.assets * res.cur_price; - res.max_price = pow2((res.assets * sqrt(res.cur_price))/(res.assets -res.avail_assets)); - double S = res.value - res.avail_money; - res.min_price = S<=0?0:pow2(S/(res.assets*sqrt(res.cur_price))); - return res; - - -} void MTrader::mergeTrades(std::size_t fromPos) { if (fromPos) --fromPos; @@ -858,7 +646,8 @@ void MTrader::mergeTrades(std::size_t fromPos) { while (rd != end) { if (rd->price == wr->price && rd->size * wr->size > 0) { wr->size+=rd->size; - wr->balance = rd->balance; + wr->norm_accum = rd->norm_accum; + wr->norm_profit = rd->norm_profit; wr->eff_price = rd->eff_price; wr->eff_size+=rd->eff_size; wr->time = rd->time; @@ -908,57 +697,43 @@ json::Value MTrader::OrderPair::toJSON() const { ("sell",sell.has_value()?sell->toJSON():json::Value()); } -MTrader::PTResult MTrader::processTrades(Status &st,bool first_trade) { +void MTrader::processTrades(Status &st) { - bool buy_trade = false; - bool sell_trade = false; - bool was_manual = false; - for (auto &&t : st.new_trades) { - bool manual = false; - manual = true; - Order fkord(t.size, t.price); - for (auto &lo : lastOrders) { - if (t.eff_size < 0) { - if (lo.sell.has_value() && t.eff_size > lo.sell->size*1.1) { - manual = false; - } - } - if (t.eff_size > 0) { - if (lo.buy.has_value() && t.eff_size < lo.buy->size*1.1) { - manual = false; - } - } - if (!manual) break; - } - if (manual) { - for (auto &lo : lastOrders) { - ondra_shared::logNote("Detected manual trade: $1 $2 $3", - !lo.buy.has_value()?0.0:lo.buy->price, fkord.price, !lo.sell.has_value()?0.0:lo.sell->price); - } - } + if (st.new_trades.trades.empty()) return; - was_manual = was_manual || manual; - if (!cfg.detect_manual_trades || first_trade) - manual = false; + auto z = std::accumulate(st.new_trades.trades.begin(), st.new_trades.trades.end(),std::pair(st.assetBalance,st.currencyBalance), + [](const std::pair &x, const IStockApi::Trade &y) { + return std::pair(x.first - y.eff_size, x.second + y.eff_size*y.eff_price);} + ); + + if (!strategy.isValid()) { + strategy.init(st.new_trades.trades[0].eff_price, z.first, z.second); + } - if (!manual) { - internal_balance += t.eff_size; - } - buy_trade = buy_trade || t.eff_size > 0; - sell_trade = sell_trade || t.eff_size < 0; + double last_np = 0; + double last_ap = 0; + if (!trades.empty()) { + last_np = trades.back().norm_profit; + last_ap = trades.back().norm_accum; - trades.push_back(TWBItem(t, st.assetBalance, - manual || calculator.isAchieveMode())); } - st.internalBalance = internal_balance + cfg.external_assets; - prev_calc_ref = calculator.balance2price(1); - return {was_manual}; + for (auto &&t : st.new_trades.trades) { + + tempPr.tradeId = t.id.toString().str(); + tempPr.size = t.eff_size; + tempPr.price = t.eff_price; + if (!minfo.simulator) statsvc->reportPerformance(tempPr); + z.first += t.eff_size; + z.second -= t.eff_size * t.eff_price; + auto norm = strategy.onTrade(t.eff_price, t.eff_size, z.first, z.second); + trades.push_back(TWBItem(t, last_np+=norm.normProfit, last_ap+=norm.normAccum)); + } } void MTrader::update_dynmult(bool buy_trade,bool sell_trade) { @@ -988,35 +763,46 @@ void MTrader::reset() { if (trades.size() > 1) { trades.erase(trades.begin(), trades.end()-1); } + if (!trades.empty()) { + trades.back().norm_accum = 0; + trades.back().norm_profit = 0; + } saveState(); } -void MTrader::achieve_balance(double price, double balance) { - if (need_load) loadState(); - if (minfo.leverage>0) { - balance += cfg.external_assets; - } - if (balance > 0) { - calculator.achieve(price, balance); - saveState(); - } else { - throw std::runtime_error("can't set negative balance"); - } +void MTrader::stop() { + cfg.enabled = false; } + void MTrader::repair() { if (need_load) loadState(); buy_dynmult = 1; sell_dynmult = 1; if (cfg.internal_balance) { if (!trades.empty()) - internal_balance = trades.back().balance- cfg.external_assets; + internal_balance = std::accumulate(trades.begin(), trades.end(), 0.0, [](double v, const TWBItem &itm) {return v+itm.eff_size;}); } else { - internal_balance = 0; + internal_balance.reset(); + } + currency_balance_cache.reset(); + strategy.reset(); + + if (!trades.empty()) { + stock.reset(); + lastTradeId = nullptr; + } + double lastPrice = 0; + for (auto &&x : trades) { + if (!isfinite(x.norm_accum)) x.norm_accum = 0; + if (!isfinite(x.norm_profit)) x.norm_profit = 0; + if (x.price < 1e-8 || !isfinite(x.price)) { + x.price = lastPrice; + } else { + lastPrice = x.price; + } + } - prev_spread = 0; - currency_balance_cache =0; - prev_calc_ref = 0; saveState(); } @@ -1024,18 +810,8 @@ ondra_shared::StringView MTrader::getChart() const { return chart; } -double MTrader::getLastSpread() const { - return prev_spread; -} -double MTrader::getInternalBalance() const { - return internal_balance; -} -void MTrader::setInternalBalance(double v) { - internal_balance = v; -} - -bool MTrader::acceptLoss(std::optional &orig, const Order &order, const Status &st, double neutral_pos) { +bool MTrader::acceptLoss(std::optional &orig, const Order &order, const Status &st) { if (cfg.accept_loss && cfg.enabled && !trades.empty()) { std::size_t ttm = trades.back().time; @@ -1045,7 +821,7 @@ bool MTrader::acceptLoss(std::optional &orig, const Order &order, const S Order cpy (order); cpy.size = sgn(cpy.size)*minfo.min_size; try { - setOrderCheckMaxPos(orig,cpy,st.assetBalance, neutral_pos); + setOrder(orig,cpy); return true; } catch (...) { @@ -1054,11 +830,11 @@ bool MTrader::acceptLoss(std::optional &orig, const Order &order, const S std::size_t e = st.chartItem.time>ttm?(st.chartItem.time-ttm)/(3600000):0; double lastTradePrice = trades.back().eff_price; if (e > cfg.accept_loss) { - auto reford = calculateOrder(lastTradePrice, 2 * st.curStep * sgn(-order.size),lastTradePrice, st.assetBalance, 0, st.assetBalance); + auto reford = calculateOrder(lastTradePrice, 2 * st.curStep * sgn(-order.size),1,lastTradePrice, st.assetBalance, 1.0); double df = (st.curPrice - reford.price)* sgn(-order.size); if (df > 0) { ondra_shared::logWarning("Accept loss in effect: price=$1, balance=$2", st.curPrice, st.assetBalance); - trades.push_back(IStockApi::TradeWithBalance ( + trades.push_back(TWBItem ( IStockApi::Trade { json::Value(json::String({vtradePrefix,"loss_", std::to_string(st.chartItem.time)})), st.chartItem.time, @@ -1066,9 +842,8 @@ bool MTrader::acceptLoss(std::optional &orig, const Order &order, const S reford.price, 0, reford.price, - }, st.assetBalance, false)); - update_dynmult(order.size>0, order.size<0); - calculator.update(reford.price, st.assetBalance); + }, trades.back().norm_profit, trades.back().norm_accum)); + strategy.onTrade(reford.price, 0, st.assetBalance, st.currencyBalance); } } } @@ -1152,9 +927,84 @@ class ConfigOuput { std::ostream &out; }; -void MTrader::showConfig(const ondra_shared::IniConfig::Section &ini, bool force_dry_run, std::ostream &out) { +void MTrader::dropState() { + storage->erase(); + statsvc->clear(); +} + +class ConfigFromJSON { +public: + + class Mandatory:public ondra_shared::VirtualMember { + public: + using ondra_shared::VirtualMember::VirtualMember; + auto operator[](StrViewA name) const { + return getMaster()->getMandatory(name); + } + }; + + class Item { + public: + + json::Value v; + + Item(json::Value v):v(v) {} + + auto getString() const {return v.getString();} + auto getString(json::StrViewA d) const {return v.defined()?v.getString():d;} + auto getUInt() const {return v.getUInt();} + auto getUInt(std::size_t d) const {return v.defined()?v.getUInt():d;} + auto getNumber() const {return v.getNumber();} + auto getNumber(double d) const {return v.defined()?v.getNumber():d;} + auto getBool() const {return v.getBool();} + auto getBool(bool d) const {return v.defined()?v.getBool():d;} + bool defined() const {return v.defined();} + + }; + + Item operator[](ondra_shared::StrViewA name) const { + return Item(config[name]); + } + Item getMandatory(ondra_shared::StrViewA name) const { + json::Value v = config[name]; + if (v.defined()) return Item(v); + else throw std::runtime_error(std::string(name).append(" is mandatory")); + } + + Mandatory mandatory; + + ConfigFromJSON(json::Value config):mandatory(this),config(config) {} +protected: + json::Value config; +}; + +double MTrader::calcSpread() const { + if (chart.size() < 15) return 0.01; + std::queue sma; + std::vector mapped; + std::accumulate(chart.begin(), chart.end(), 0.0, [&](auto &&a, auto &&c) { + double h = 0.0; + if ( sma.size() >= cfg.spread_calc_sma_hours) { + h = sma.front(); + sma.pop(); + } + double d = a + c.last - h; + sma.push(c.last); + mapped.push_back(c.last - d/sma.size()); + return d; + }); + + std::size_t i = mapped.size() >= cfg.spread_calc_stdev_hours?mapped.size()-cfg.spread_calc_stdev_hours:0; + auto iter = mapped.begin()+i; + auto end = mapped.end(); + auto stdev = std::sqrt(std::accumulate(iter, end, 0.0, [&](auto &&v, auto &&c) { + return v + c*c; + })/std::distance(iter, end)); + + double lnspread = std::log((stdev+sma.back())/sma.back()); + logDebug("Spread calculated: stdev=$1, sma=$2, lnspread=$3", stdev, sma.back(), lnspread); + + return lnspread; - ConfigOuput cfg(ini, out); - load_internal(cfg, force_dry_run); } diff --git a/src/main/mtrader.h b/src/main/mtrader.h index 14d5ee6b..412f9ff8 100644 --- a/src/main/mtrader.h +++ b/src/main/mtrader.h @@ -13,10 +13,11 @@ #include #include -#include "calculator.h" +#include "idailyperfmod.h" #include "istatsvc.h" #include "storage.h" #include "report.h" +#include "strategy.h" class IStockApi; @@ -39,7 +40,6 @@ struct MTrader_Config { double sell_mult; double buy_step_mult; double sell_step_mult; - double external_assets; double min_size; double max_size; @@ -47,48 +47,24 @@ struct MTrader_Config { double dynmult_fall; Dynmult_mode dynmult_mode; - double acm_factor_buy; - double acm_factor_sell; - unsigned int accept_loss; - double sliding_pos_hours; - double sliding_pos_weaken; - double force_spread; - double emulated_currency; double report_position_offset; - - unsigned int spread_calc_mins; - unsigned int spread_calc_min_trades; - unsigned int spread_calc_max_trades; - - - enum NeutralPosType { - disabled, - assets, - currency, - center - }; - - NeutralPosType neutralPosType; - double neutral_pos; - double max_pos; - double expected_trend; - + unsigned int spread_calc_stdev_hours; + unsigned int spread_calc_sma_hours; bool dry_run; bool internal_balance; bool detect_manual_trades; bool enabled; - bool force_margin; bool dust_orders; + bool dynmult_scale; - std::size_t start_time; + Strategy strategy = Strategy(nullptr); - - void parse_neutral_pos(ondra_shared::StrViewA txt); + void loadConfig(json::Value data, bool force_dry_run); }; @@ -99,14 +75,6 @@ class MTrader { using StoragePtr = PStorage; using Config = MTrader_Config; - - - - - static Config load(const ondra_shared::IniConfig::Section &ini, bool force_dry_run); - static void showConfig(const ondra_shared::IniConfig::Section &ini, bool force_dry_run, std::ostream &out); - - struct Order: public IStockApi::Order { bool isSimilarTo(const Order &other, double step); Order(const IStockApi::Order& o):IStockApi::Order(o) {} @@ -130,82 +98,67 @@ class MTrader { Config config); - ///Returns true, if trade was detected, or false, if not - int perform(); + + void perform(bool manually); void init(); OrderPair getOrders(); void setOrder(std::optional &orig, Order neworder); - void setOrderCheckMaxPos(std::optional &orig, Order neworder, double balance, double neutral); using ChartItem = IStatSvc::ChartItem; struct Status { + IStockApi::Ticker ticker; double curPrice; double curStep; double assetBalance; double internalBalance; + double currencyBalance; double new_fees; - IStockApi::TradeHistory new_trades; + IStockApi::TradesSync new_trades; ChartItem chartItem; }; Status getMarketStatus() const; - - /// Calculate order - /** - * @param step precalculated step (spread), negative for sell, positive for buy - * @param oldPrice price of last trade (reference price) - * @param curPrice current price (center price) - * @param balance current balance (including external) - * @return order - */ Order calculateOrder(double lastTradePrice, double step, + double dynmult, double curPrice, double balance, - double acm, double mult) const; Order calculateOrderFeeLess( double lastTradePrice, double step, + double dynmult, double curPrice, double balance, - double acm, double mult) const; const Config &getConfig() {return cfg;} const IStockApi::MarketInfo getMarketInfo() const {return minfo;} - struct CalcRes { - double assets; - double avail_assets; - double value; - double avail_money; - double min_price; - double max_price; - double cur_price; - - }; - - CalcRes calc_min_max_range(); - bool eraseTrade(std::string_view id, bool trunc); void reset(); void repair(); - void achieve_balance(double price, double balance); ondra_shared::StringView getChart() const; - double getLastSpread() const; - double getInternalBalance() const; - void setInternalBalance(double v); + void dropState(); + void stop(); + + using TradeHistory = std::vector; + + const TradeHistory &getTrades() const; static std::string_view vtradePrefix; + Strategy &getStrategy() {return strategy;} + const Strategy &getStrategy() const {return strategy;} + IStockApi &getBroker() {return stock;} + protected: std::unique_ptr ownedStock; IStockApi &stock; @@ -213,53 +166,45 @@ class MTrader { IStockApi::MarketInfo minfo; StoragePtr storage; PStatSvc statsvc; - OrderPair lastOrders[2]; + Strategy strategy; bool need_load = true; - bool first_order = true; - Calculator calculator; + bool recalc = true; + json::Value test_backup; + json::Value lastTradeId = nullptr; using TradeItem = IStockApi::Trade; - using TWBItem = IStockApi::TradeWithBalance; + using TWBItem = IStatSvc::TradeRecord; std::vector chart; - IStockApi::TWBHistory trades; + TradeHistory trades; double buy_dynmult=1.0; double sell_dynmult=1.0; - double internal_balance = 0; - mutable double prev_spread=0.01; - double prev_calc_ref = 0; - double currency_balance_cache = 0; + std::optional internal_balance; + std::optional currency_balance_cache; size_t magic = 0; - double cur_trend_adv = 0; + size_t uid = 0; + PerformanceReport tempPr; void loadState(); void saveState(); - - double range_max_price(Status st, double &avail_assets); - double range_min_price(Status st, double &avail_money); - double raise_fall(double v, bool raise) const; - static IStockApi &selectStock(IStockSelector &stock_selector, const Config &conf, std::unique_ptr &ownedStock); - std::size_t testStartTime; - struct PTResult { - bool manual_trades; - }; - PTResult processTrades(Status &st,bool first_trade); + void processTrades(Status &st); void mergeTrades(std::size_t fromPos); void update_dynmult(bool buy_trade,bool sell_trade); - bool acceptLoss(std::optional &orig, const Order &order, const Status &st, double neutral_pos); + bool acceptLoss(std::optional &orig, const Order &order, const Status &st); json::Value getTradeLastId() const; + double calcSpread() const; + - double calcWeakenMult(double neutral_pos, double balance); }; diff --git a/src/main/ordergen.cpp b/src/main/ordergen.cpp deleted file mode 100644 index 595137a7..00000000 --- a/src/main/ordergen.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/* - * ordergen.cpp - * - * Created on: 11. 5. 2019 - * Author: ondra - */ - -#include -#include "ordergen.h" - -OrderGen::OrderGen(Config config):config(config) { -} - -OrderGen::Orders OrderGen::generate(double asset_balance, double buy_price, double middle_price) { - - - Orders orders; - generate_buy(orders, asset_balance, buy_price, middle_price); - generate_sell(orders, asset_balance, buy_price, middle_price); - - - return orders; - -} - -void OrderGen::generate_buy(Orders& out, double asset_balance, double buy_price, double middle_price) { - double bp = std::min(buy_price, middle_price); - double p = bp - config.min_currency; - double a = adj_amount(amount_from_price(asset_balance, buy_price, p)); - //double step = a; - double mp = bp /(1 + config.spread); - p = adj_price(price_from_amount(asset_balance, buy_price, a)); - while (p > mp) { - if (a > config.min_asset) { - out.push_back({p, a}); - } - asset_balance+=a; - buy_price = p; - a = 2*a; - p = adj_price(price_from_amount(asset_balance, buy_price, a)); - - } -} - -void OrderGen::generate_sell(Orders& out, double asset_balance, - double buy_price, double middle_price) { - - double bp = std::max(buy_price, middle_price); - double p = bp + config.min_currency; - double a = adj_amount(amount_from_price(asset_balance, buy_price, p)); - //double step = a; - double mp = bp * (1 + config.spread); - p = adj_price(price_from_amount(asset_balance, buy_price, a)); - while (p < mp && -2*a < asset_balance) { - if (-a > config.min_asset) { - out.push_back({p, a}); - } - asset_balance+=a; - buy_price = p; - a = 2*a; - p = adj_price(price_from_amount(asset_balance, buy_price, a)); - } - -} - -double OrderGen::amount_from_price(double asset_balance, double buy_price, - double new_price) { - - //(a*bp - a*np)/(2 * np) - //a*(bp-np)/(2*np) - //a*(bp/np-1)/2 - - return asset_balance*(buy_price/new_price-1)/2; -} - - -double OrderGen::price_from_amount(double asset_balance, double buy_price, double add_amount) { - - //na = (a*bp - a*np)/(2 * np) - //na = (a*bp/2*np) - a/2 - //na*np = (a*bp/2) - a*np/2 - //na*np + a*np/2 = a*bp/2 - //np(na + a/2) = a*bp/2 - //np = a*bp/(2*na + a) - - return asset_balance*buy_price/(2*add_amount + asset_balance); - -} - -double OrderGen::adj_price(double price) { - return std::round(price/config.currency_step)*config.currency_step; -} - -double OrderGen::adj_amount(double amount) { - return std::round(amount/config.asset_step)*config.asset_step; -} diff --git a/src/main/ordergen.h b/src/main/ordergen.h deleted file mode 100644 index 276907e9..00000000 --- a/src/main/ordergen.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * ordergen.h - * - * Created on: 11. 5. 2019 - * Author: ondra - */ - -#ifndef SRC_MAIN_ORDERGEN_H_ -#define SRC_MAIN_ORDERGEN_H_ -#include -#include - -class OrderGen { -public: - - struct Config { - double asset_step; - double currency_step; - double min_asset; - double min_currency; - double spread; - }; - - - - - struct Order { - double price; - double size; - std::string refId; - }; - - using Orders = std::vector; - - Config config; - - OrderGen(Config config); - - - Orders generate(double asset_balance, double buy_price, double middle_price); - - -protected: - void generate_buy(Orders &out, double asset_balance, double buy_price, double middle_price); - void generate_sell(Orders &out, double asset_balance, double buy_price, double middle_price); - - static double amount_from_price(double asset_balance, double buy_price, double new_price); - static double price_from_amount(double asset_balance, double buy_price, double add_amount); - - double adj_price(double price); - double adj_amount(double amount); -}; - -#endif /* SRC_MAIN_ORDERGEN_H_ */ diff --git a/src/main/report.cpp b/src/main/report.cpp index 85c29f83..834b82e4 100644 --- a/src/main/report.cpp +++ b/src/main/report.cpp @@ -24,7 +24,9 @@ using std::chrono::_V2::system_clock; using namespace json; - +void Report::setInterval(std::size_t interval) { + this->interval_in_ms = interval; +} void Report::genReport() { @@ -40,6 +42,7 @@ void Report::genReport() { std::chrono::system_clock::now().time_since_epoch() ).count()); st.set("log", logLines); + st.set("performance", perfRep); while (logLines.size()>30) logLines.erase(0); report->store(st); } @@ -79,7 +82,7 @@ void Report::setOrders(StrViewA symb, const std::optional &buy } -void Report::setTrades(StrViewA symb, StringView trades, bool margin) { +void Report::setTrades(StrViewA symb, StringView trades) { using ondra_shared::range; @@ -89,8 +92,6 @@ void Report::setTrades(StrViewA symb, StringView tr bool inverted = info["inverted"].getBool(); double pos = info["po"].getNumber(); - - if (!trades.empty()) { const auto &last = trades[trades.length-1]; @@ -102,23 +103,15 @@ void Report::setTrades(StrViewA symb, StringView tr auto iter = trades.begin(); auto &&t = *iter; - std::size_t invest_beg_time = t.time; - double invst_value = t.eff_price*t.balance; - //so the first trade doesn't change the value of portfolio -// double init_value = init_balance*t.eff_price+init_fiat; - // double init_price = t.eff_price; - double prev_balance = t.balance-t.eff_size; double prev_price = init_price; - double cur_sum = 0; double cur_fromPos = 0; - double norm_sum_ass = 0; - double norm_sum_cur = 0; - double potentialpl = 0; - double neutral_price = 0; + double pnp = 0; + double pap = 0; + while (iter != tend) { @@ -126,46 +119,17 @@ void Report::setTrades(StrViewA symb, StringView tr auto &&t = *iter; double gain = (t.eff_price - prev_price)*pos ; - double earn = -t.eff_price * t.eff_size; - double bal_chng = (t.balance - prev_balance) - t.eff_size; - invst_value += bal_chng * t.eff_price; - - - double calcbal = prev_balance * sqrt(prev_price/t.eff_price); - double asschg = (prev_balance+t.eff_size) - calcbal ; - double curchg = -(calcbal * t.eff_price - prev_balance * prev_price - earn); - double norm_chng = 0; - - if (iter != trades.begin() && !iter->manual_trade) { - cur_fromPos += gain; - cur_sum += earn; - - norm_sum_ass += asschg; - norm_sum_cur += curchg; - norm_chng = curchg+asschg * t.eff_price; - - pos += t.eff_size; - + // double earn = -t.eff_price * t.eff_size; - double np = t.balance-pos; - neutral_price = t.eff_price * pow2(t.balance/np); - potentialpl = cur_fromPos + pos*(neutral_price-sqrt(t.eff_price*neutral_price)); - - - } - if (iter->manual_trade) { - invst_value += earn; - } - double norm; - norm = norm_sum_cur; + prev_price = t.eff_price; + cur_fromPos += gain; + pos += t.eff_size; - prev_balance = t.balance; - prev_price = t.eff_price; + double normch = (t.norm_accum - pap) * t.eff_price + (t.norm_profit - pnp); + pap = t.norm_accum; + pnp = t.norm_profit; - double invst_time = t.time - invest_beg_time; - double invst_n = norm/invst_time; - if (!std::isfinite(invst_n)) invst_n = 0; if (t.time >= first) { @@ -174,18 +138,14 @@ void Report::setTrades(StrViewA symb, StringView tr ("time", t.time) ("achg", (inverted?-1:1)*t.eff_size) ("gain", gain) - ("norm", margin?Value():Value(norm)) - ("normch", norm_chng) - ("nacum", margin?Value():Value((inverted?-1:1)*norm_sum_ass)) + ("norm", t.norm_profit) + ("normch", normch) + ("nacum", Value((inverted?-1:1)*t.norm_accum)) ("pos", (inverted?-1:1)*pos) ("pl", cur_fromPos) - ("pln", potentialpl) ("price", (inverted?1.0/t.price:t.price)) - ("invst_v", invst_value) - ("invst_n", invst_n) ("volume", (inverted?1:-1)*t.eff_price*t.eff_size) - ("man",t.manual_trade) - ("np",(inverted?1.0/neutral_price:neutral_price)) + ("man",false) ); } @@ -220,6 +180,7 @@ void Report::setInfo(StrViewA symb, const InfoObj &infoObj) { ("currency", infoObj.currencySymb) ("asset", infoObj.assetSymb) ("price_symb", infoObj.priceSymb) + ("brokerIcon", infoObj.brokerIcon) ("inverted", infoObj.inverted) ("emulated",infoObj.emulated) ("po", infoObj.position_offset); @@ -329,14 +290,10 @@ void Report::setMisc(StrViewA symb, const MiscData &miscData) { miscMap[symb] = Object ("t",-miscData.trade_dir) - ("a", miscData.achieve) ("mcp", 1.0/miscData.calc_price) - ("mv", miscData.value) ("ms", spread) ("mdmb", miscData.dynmult_sell) ("mdms", miscData.dynmult_buy) - ("sm", miscData.size_mult) - ("mb",miscData.boost) ("ml",1.0/miscData.highest_price) ("mh",1.0/miscData.lowest_price) ("mt",miscData.total_trades) @@ -344,17 +301,26 @@ void Report::setMisc(StrViewA symb, const MiscData &miscData) { } else { miscMap[symb] = Object ("t",miscData.trade_dir) - ("a", miscData.achieve) ("mcp", miscData.calc_price) - ("mv", miscData.value) ("ms", spread) ("mdmb", miscData.dynmult_buy) ("mdms", miscData.dynmult_sell) - ("sm", miscData.size_mult) - ("mb",miscData.boost) ("ml",miscData.lowest_price) ("mh",miscData.highest_price) ("mt",miscData.total_trades) ("tt",miscData.total_time); } } + +void Report::clear(StrViewA symb) { + tradeMap.erase(symb); + infoMap.erase(symb); + priceMap.erase(symb); + miscMap.erase(symb); + errorMap.erase(symb); + orderMap.clear(); +} + +void Report::perfReport(json::Value report) { + perfRep = report; +} diff --git a/src/main/report.h b/src/main/report.h index 1e512a9b..057b999b 100644 --- a/src/main/report.h +++ b/src/main/report.h @@ -16,6 +16,7 @@ #include "../shared/stdLogOutput.h" #include "../shared/stringview.h" #include "istatsvc.h" +#include "strategy.h" namespace json { class Array; @@ -34,18 +35,22 @@ class Report { :report(std::move(report)),interval_in_ms(interval_in_ms),a2np(a2np) {} + void setInterval(std::size_t interval); void genReport(); using StrViewA = ondra_shared::StrViewA; template using StringView = ondra_shared::StringView; void setOrders(StrViewA symb, const std::optional &buy, const std::optional &sell); - void setTrades(StrViewA symb, StringView trades, bool margin); + void setTrades(StrViewA symb, StringView trades); void setInfo(StrViewA symb, const InfoObj &info); void setMisc(StrViewA symb, const MiscData &miscData); void setPrice(StrViewA symb, double price); void addLogLine(StrViewA ln); + void clear(StrViewA symb); + + void perfReport(json::Value report); virtual void setError(StrViewA symb, const ErrorObj &errorObj); @@ -82,6 +87,7 @@ class Report { MiscMap miscMap; MiscMap errorMap; json::Array logLines; + json::Value perfRep; StoragePtr report; diff --git a/src/main/sgn.h b/src/main/sgn.h index cdbbf3dc..7fe80eac 100644 --- a/src/main/sgn.h +++ b/src/main/sgn.h @@ -7,6 +7,7 @@ #ifndef SRC_MAIN_SGN_H_ #define SRC_MAIN_SGN_H_ +#include diff --git a/src/main/spread_calc.cpp b/src/main/spread_calc.cpp deleted file mode 100644 index 8c368a30..00000000 --- a/src/main/spread_calc.cpp +++ /dev/null @@ -1,217 +0,0 @@ -/* - * spread_calc.cpp - * - * Created on: 19. 5. 2019 - * Author: ondra - */ - - -#include "spread_calc.h" - -#include -#include - -#include "../shared/logOutput.h" -#include "../shared/worker.h" -#include "mtrader.h" -#include "backtest_broker.h" - -using ondra_shared::logInfo; -using ondra_shared::logDebug; - -using StockEmulator = BacktestBroker; - -class EmptyStorage: public IStorage { -public: - virtual void store(json::Value) {}; - virtual json::Value load() {return json::Value();} - -}; - -class EmulStatSvc: public IStatSvc { -public: - EmulStatSvc(double spread):spread(spread) {} - - virtual void reportTrades(ondra_shared::StringView , bool) override {} - virtual void reportOrders(const std::optional &, - const std::optional &)override {} - virtual void reportPrice(double ) override {} - virtual void reportError(const ErrorObj &) override {} - virtual void reportMisc(const MiscData &) override {} - virtual void setInfo(const Info &) override {} - virtual double calcSpread(ondra_shared::StringView , - const MTrader_Config &, - const IStockApi::MarketInfo &, - double, - double ) const override {return spread;} - virtual std::size_t getHash() const override { - return 0xABCDEF; - } - -protected: - double spread; -}; - - -struct EmulResult { - double score; - int trades; -}; - -static EmulResult emulateMarket(ondra_shared::StringView chart, - const MTrader_Config &config, - const IStockApi::MarketInfo &minfo, - double balance, - double spread) { - - - StockEmulator emul(chart, minfo, balance, true); - - MTrader_Config cfg(config); - cfg.dry_run = false; - cfg.spread_calc_mins=1; - cfg.internal_balance = false; - cfg.dynmult_fall = 100; - cfg.dynmult_raise = 0; - cfg.sell_step_mult = 1; - cfg.buy_step_mult = 1; - cfg.acm_factor_buy = 0; - cfg.acm_factor_sell = 0; - cfg.accept_loss = 0; - cfg.max_pos = 0; - cfg.neutralPosType = MTrader_Config::disabled; - cfg.sliding_pos_hours=0; - cfg.sliding_pos_weaken=0; - cfg.expected_trend = 0; - - class Selector: public IStockSelector { - public: - Selector(IStockApi &emul):emul(emul) {} - virtual IStockApi *getStock(const std::string_view &) const {return &emul;} - virtual void forEachStock(EnumFn fn) const {fn("emil", emul);} - - protected: - IStockApi &emul; - }; - - double initScore = emul.getScore(); - - Selector selector(emul); - std::size_t counter = 1; - { - ondra_shared::PLogProvider nullprovider (std::make_unique()); - ondra_shared::LogObject nullLog(*nullprovider,""); - ondra_shared::LogObject::Swap swp(nullLog); - - MTrader trader(selector, nullptr,std::make_unique(spread),cfg); - - trader.perform(); - while (emul.reset()) { - trader.perform(); - counter++; - } - - } - - double score = emul.getScore()-initScore; - std::intptr_t tcount = emul.getTradeCount(); - if (tcount == 0) return EmulResult{-1001,0}; - std::intptr_t min_count = std::max(counter*cfg.spread_calc_min_trades/1440,1); - std::intptr_t max_count = (counter*cfg.spread_calc_max_trades+1439)/1440; - if (tcount < min_count) score = tcount-min_count; - else if (tcount > max_count) score = max_count-tcount; - return EmulResult { - score, - static_cast(tcount) - }; -} - - - -std::pair glob_calcSpread2(ondra_shared::Worker wrk, - ondra_shared::StringView chart, - const MTrader_Config &config, - const IStockApi::MarketInfo &minfo, - double balance, - double prev_val) { - double curprice = sqrt(chart[chart.length-1].ask*chart[chart.length-1].bid); - - - using ResultItem = std::pair; - ResultItem bestResults[]={ - {-10,prev_val}, - {-10,prev_val}, - {-10,prev_val}, - {-10,prev_val} - }; - - double low_spread = curprice*(std::exp(prev_val)-1)/10; - const int steps = 200; - double hi_spread = curprice*(std::exp(prev_val)-1)*10; - double best_profit = 0; - auto resend = std::end(bestResults); - auto resbeg = std::begin(bestResults); - auto resiter = resbeg; - - using namespace ondra_shared; - std::mutex lock; - MTCounter cnt; - - for (int i = 0; i < steps; i++) { - - cnt++; - double curSpread = std::log(((low_spread+(hi_spread-low_spread)*i/(steps-1.0))+curprice)/curprice); - wrk >> [&, curSpread] { - { - auto res = emulateMarket(chart, config, minfo, balance, curSpread); - std::unique_lock _(lock); - auto profit = res.score; - ResultItem resitem(profit,curSpread); - if (resiter->first < resitem.first) { - *resiter = resitem; - resiter = std::min_element(resbeg, resend); - ondra_shared::logDebug("Found better spread= $1 (log=$2), score=$3, trades=$4", curprice*exp(curSpread)-curprice, curSpread, profit, res.trades); - best_profit = profit; - } - } - cnt--; - }; - } - - cnt.wait(); - - double sugg_spread = pow(std::accumulate( - std::begin(bestResults), - std::end(bestResults), ResultItem(1,1), - [](const ResultItem &a, const ResultItem &b) { - return ResultItem(0,a.second * b.second);}).second,1.0/std::distance(resbeg, resend)); - return {sugg_spread,best_profit}; -} - -double glob_calcSpread(ondra_shared::Worker wrk, - ondra_shared::StringView chart, - const MTrader_Config &config, - const IStockApi::MarketInfo &minfo, - double balance, - double prev_val) { - if (prev_val < 1e-10) prev_val = 0.01; - if (chart.empty() || balance == 0 || !config.enabled) return prev_val; - double curprice = sqrt(chart[chart.length-1].ask*chart[chart.length-1].bid); - auto sp1 = glob_calcSpread2(wrk,chart, config, minfo, balance, prev_val); - auto sp2 = sp1; - if (chart.length > 1000) { - sp2 = glob_calcSpread2(wrk,chart.substr(chart.length-1000), config, minfo, balance, prev_val); - } - double sp3 = (sp1.first + sp2.first)/2.0; - logInfo("Spread calculated: long=$1 (profit=$2), short=$3 (profit=$4), final=$5",curprice*(exp(sp1.first)-1), - sp1.second, - curprice*(exp(sp2.first)-1), - sp2.second, - curprice*(exp(sp3)-1)); - return sp3; - - -} - - - diff --git a/src/main/spread_calc.h b/src/main/spread_calc.h deleted file mode 100644 index 67f25894..00000000 --- a/src/main/spread_calc.h +++ /dev/null @@ -1,25 +0,0 @@ -/* - * spread_calc.h - * - * Created on: 19. 5. 2019 - * Author: ondra - */ - -#ifndef SRC_MAIN_SPREAD_CALC_H_ -#define SRC_MAIN_SPREAD_CALC_H_ - -#include "../shared/stringview.h" -#include "../shared/worker.h" -#include "istatsvc.h" -#include "istockapi.h" - - -double glob_calcSpread(ondra_shared::Worker wrk, - ondra_shared::StringView chart, - const MTrader_Config &config, - const IStockApi::MarketInfo &minfo, - double balance, - double prev_val); - - -#endif /* SRC_MAIN_SPREAD_CALC_H_ */ diff --git a/src/main/stats2report.h b/src/main/stats2report.h index 5f6393b0..0f837f9f 100644 --- a/src/main/stats2report.h +++ b/src/main/stats2report.h @@ -9,9 +9,10 @@ #define SRC_MAIN_STATS2REPORT_H_ #include + +#include "idailyperfmod.h" #include "istatsvc.h" #include "report.h" -#include "spread_calc.h" using CalcSpreadFn = std::function; using CalcSpreadQueue = std::function; @@ -20,41 +21,20 @@ using CalcSpreadQueue = std::function; class Stats2Report: public IStatSvc { public: - struct SpreadInfo { - double spread = 0; - bool pending = false; - }; - - class SharedPool: public std::shared_ptr { - public: - using Super = std::shared_ptr; - explicit SharedPool(int wrkcnt) - :Super(std::make_shared()) - ,wrkcnt(wrkcnt) {} - void init() { - if (!(*this)->defined()) { - *(this->get()) = ondra_shared::Worker::create(wrkcnt); - } - } - protected: - int wrkcnt; - }; + Stats2Report( - CalcSpreadQueue q, std::string name, Report &rpt, - int interval, - const SharedPool &pool) - :q(q),rpt(rpt),name(name),interval(interval) - ,spread(std::make_shared()),pool(pool) {} + IDailyPerfModule *perfmod + ) :rpt(rpt),name(name),perfmod(perfmod) {} virtual void reportOrders(const std::optional &buy, const std::optional &sell) override { rpt.setOrders(name, buy, sell); } - virtual void reportTrades(ondra_shared::StringView trades, bool margin) override { - rpt.setTrades(name,trades,margin); + virtual void reportTrades(ondra_shared::StringView trades) override { + rpt.setTrades(name,trades); } virtual void reportMisc(const MiscData &miscData) override{ rpt.setMisc(name, miscData); @@ -69,49 +49,20 @@ class Stats2Report: public IStatSvc { virtual void reportPrice(double price) override{ rpt.setPrice(name, price); } - virtual double calcSpread(ondra_shared::StringView chart, - const MTrader_Config &cfg, - const IStockApi::MarketInfo &minfo, - double balance, - double prev_value) const override{ - - if (spread->spread == 0) spread->spread = prev_value; - - if (cnt <= 0) { - if (spread->pending) return spread->spread; - cnt += interval; - spread->pending = true; - q([chart = std::vector(chart.begin(),chart.end()), - cfg = MTrader_Config(cfg), - minfo = IStockApi::MarketInfo(minfo), - balance, - spread = this->spread, - name = this->name, - pool = this->pool] () mutable { - ondra_shared::LogObject logObj(name); - ondra_shared::LogObject::Swap swap(logObj); - pool.init(); - spread->spread = glob_calcSpread( *(pool.get()),chart, cfg, minfo, balance, spread->spread); - spread->pending = false; - }); - return spread->spread; - } else { - --cnt; - return spread->spread; - } - } virtual std::size_t getHash() const override { std::hash h; return h(name); } + virtual void clear() override { + rpt.clear(name); + } + virtual void reportPerformance(const PerformanceReport &repItem) override { + if (perfmod) perfmod->sendItem(repItem); + } - CalcSpreadQueue q; Report &rpt; std::string name; - int interval; - mutable int cnt = 0; - std::shared_ptr spread; - SharedPool pool; + IDailyPerfModule *perfmod; }; diff --git a/src/main/storage.cpp b/src/main/storage.cpp index aca289e8..0703cdbf 100644 --- a/src/main/storage.cpp +++ b/src/main/storage.cpp @@ -12,6 +12,8 @@ #include #include +#include + #include "../shared/logOutput.h" using namespace std::experimental::filesystem; @@ -19,6 +21,17 @@ using namespace std::experimental::filesystem; Storage::Storage(std::string file, int versions, Format format):file(file),versions(versions),format(format) { } +std::stack Storage::generateNames() { + std::stack names; + names.push(file); + for (int i = 1; i < versions; i++) { + std::ostringstream buff; + buff << file << "~" << i; + names.push(buff.str()); + } + return names; +} + void Storage::store(json::Value data) { std::string tmpname = file+".tmp"; std::ofstream f(tmpname, std::ios::out|std::ios::trunc); @@ -42,15 +55,7 @@ void Storage::store(json::Value data) { f.close(); - std::stack names; - names.push(file); - - for (int i = 1; i < versions; i++) { - std::ostringstream buff; - buff << file << "~" << i; - names.push(buff.str()); - } - + std::stack names = generateNames(); std::string to = names.top(); names.pop(); while (!names.empty()) { @@ -97,3 +102,10 @@ PStorage StorageFactory::create(std::string name) const { return std::make_unique(path+"/"+ name, versions, format); } +void Storage::erase() { + auto stack = generateNames(); + while (!stack.empty()) { + std::remove(stack.top().c_str()); + stack.pop(); + } +} diff --git a/src/main/storage.h b/src/main/storage.h index 30277c93..4603e547 100644 --- a/src/main/storage.h +++ b/src/main/storage.h @@ -10,6 +10,8 @@ #include #include +#include + #include "istorage.h" @@ -27,6 +29,7 @@ class Storage: public IStorage { virtual void store(json::Value data) override; virtual json::Value load() override; + virtual void erase() override; protected: @@ -34,6 +37,7 @@ class Storage: public IStorage { int versions; Format format; + std::stack generateNames(); }; diff --git a/src/main/strategy.cpp b/src/main/strategy.cpp new file mode 100644 index 00000000..d70121a0 --- /dev/null +++ b/src/main/strategy.cpp @@ -0,0 +1,57 @@ +/* + * strategy.cpp + * + * Created on: 18. 10. 2019 + * Author: ondra + */ + +#include "strategy.h" + +#include +#include +#include "../shared/stringview.h" +#include "strategy_plfrompos.h" +#include "strategy_halfhalf.h" +#include "strategy_keepvalue.h" + +static json::NamedEnum strCloseMode ({ + {Strategy_PLFromPos::always_close,"always_close"}, + {Strategy_PLFromPos::prefer_close,"prefer_close"}, + {Strategy_PLFromPos::prefer_reverse,"prefer_reverse"} +}); + +using ondra_shared::StrViewA; +Strategy Strategy::create(std::string_view id, json::Value config) { + + if (id== Strategy_PLFromPos::id) { + Strategy_PLFromPos::Config cfg; + cfg.accum = config["accum"].getNumber(); + cfg.step = config["cstep"].getNumber(); + cfg.neutral_pos = config["neutral_pos"].getNumber(); + cfg.maxpos = config["maxpos"].getNumber(); + cfg.reduce_factor = config["reduce_factor"].getNumber(); + cfg.closeMode = strCloseMode[config["closepos"].getString()]; + return Strategy(new Strategy_PLFromPos(cfg)); + } else if (id == Strategy_HalfHalf::id) { + double ea = config["ea"].getNumber(); + double accum = config["accum"].getNumber(); + return Strategy(new Strategy_HalfHalf(ea, accum)); + } else if (id == Strategy_KeepValue::id) { + Strategy_KeepValue::Config cfg; + cfg.ea = config["ea"].getNumber(); + cfg.accum = config["accum"].getNumber(); + return Strategy(new Strategy_KeepValue(cfg)); + } else { + throw std::runtime_error(std::string("Unknown strategy: ").append(id)); + } + +} + +json::Value Strategy::exportState() const { + return json::Object(ptr->getID(), ptr->exportState()); +} + +void Strategy::importState(json::Value src) { + json::Value data = src[ptr->getID()]; + ptr = ptr->importState(data); +} diff --git a/src/main/strategy.h b/src/main/strategy.h new file mode 100644 index 00000000..f543f6e7 --- /dev/null +++ b/src/main/strategy.h @@ -0,0 +1,58 @@ +/* + * strategy.h + * + * Created on: 17. 10. 2019 + * Author: ondra + */ + +#ifndef SRC_MAIN_STRATEGY_H_ +#define SRC_MAIN_STRATEGY_H_ + +#include "istrategy.h" + +class Strategy { +public: + + using Ptr = ondra_shared::RefCntPtr; + using TradeResult = IStrategy::OnTradeResult; + using MinMax = IStrategy::MinMax; + + Strategy(const Ptr ptr):ptr(ptr) {} + bool isValid() const {return ptr!=nullptr && ptr->isValid();} + void init(double curPrice, double assets, double currency) { + ptr = ptr->init(curPrice, assets, currency); + } + TradeResult onTrade(double tradePrice, double tradeSize, + double assetsLeft, double currencyLeft) { + auto t = ptr->onTrade(tradePrice, tradeSize, assetsLeft, currencyLeft); + ptr = t.second; + return t.first; + } + json::Value exportState() const; + void importState(json::Value src); + + double calcOrderSize(double price, double assets) const { + return ptr->calcOrderSize(price, assets); + } + MinMax calcSafeRange(double assets, double currencies) const { + return ptr->calcSafeRange(assets, currencies); + } + double getEquilibrium() const { + return ptr->getEquilibrium(); + } + void reset() { + ptr = ptr->reset(); + } + + + static Strategy create(std::string_view id, json::Value config); + +protected: + Ptr ptr; + + + +}; + + +#endif /* SRC_MAIN_STRATEGY_H_ */ diff --git a/src/main/strategy_halfhalf.cpp b/src/main/strategy_halfhalf.cpp new file mode 100644 index 00000000..586d050f --- /dev/null +++ b/src/main/strategy_halfhalf.cpp @@ -0,0 +1,89 @@ +/* + * strategy_halfhalf.cpp + * + * Created on: 18. 10. 2019 + * Author: ondra + */ + +#include "strategy_halfhalf.h" + +#include + +#include +#include "sgn.h" + +std::string_view Strategy_HalfHalf::id = "halfhalf"; + + +Strategy_HalfHalf::Strategy_HalfHalf(double ea, double accu, double p, double a) + :ea(ea), accu(accu), p(p), a(a+ea) {} + + + +bool Strategy_HalfHalf::isValid() const { + return a * p > 0; +} + + +IStrategy* Strategy_HalfHalf::init(double curPrice, double assets, + double currency) const { + return new Strategy_HalfHalf(ea, accu, curPrice, assets); +} + +std::pair Strategy_HalfHalf::onTrade( + double tradePrice, double size , double assetsLeft, double currencyLeft ) const { + + if (size == 0) { + return std::make_pair( + OnTradeResult{0,0}, + new Strategy_HalfHalf(ea, accu, tradePrice, assetsLeft)); + } else { + + double n = tradePrice; + double na = a * sqrt(p/n); + double v = a * p + a * n - 2 * a * sqrt(p * n); + double ap = (v / n) * accu; + double np = v * (1-accu); + return std::make_pair( + OnTradeResult {np, ap}, + new Strategy_HalfHalf(ea,accu,n,na+ap-ea) + ); + } +} + +json::Value Strategy_HalfHalf::exportState() const { + return json::Object + ("p",p) + ("a",a-ea); +} + +IStrategy* Strategy_HalfHalf::importState(json::Value src) const { + double new_p = src["p"].getNumber(); + double new_a = src["a"].getNumber(); + return new Strategy_HalfHalf(ea, accu, new_p, new_a); +} + +double Strategy_HalfHalf::calcOrderSize(double n, double assets) const { + double ca = assets + ea; + return a * sqrt(p/n) - ca + (a * p + a * n - 2 * a * sqrt(p * n)) * accu / n; +} + +Strategy_HalfHalf::MinMax Strategy_HalfHalf::calcSafeRange(double assets, double currencies) const { + MinMax r; + double s = a * p - currencies; + r.max = ea<=0?std::numeric_limits::infinity():p*pow2((assets + ea) / ea); + r.min = s<=0?0:pow2(s/a)/p; + return r; +} + +double Strategy_HalfHalf::getEquilibrium() const { + return p; +} + +IStrategy* Strategy_HalfHalf::reset() const { + return new Strategy_HalfHalf(ea, accu); +} + +std::string_view Strategy_HalfHalf::getID() const { + return id; +} diff --git a/src/main/strategy_halfhalf.h b/src/main/strategy_halfhalf.h new file mode 100644 index 00000000..cabe0fab --- /dev/null +++ b/src/main/strategy_halfhalf.h @@ -0,0 +1,38 @@ +/* + * strategy_halfhalf.h + * + * Created on: 18. 10. 2019 + * Author: ondra + */ + +#ifndef SRC_MAIN_STRATEGY_HALFHALF_H_ +#define SRC_MAIN_STRATEGY_HALFHALF_H_ +#include "istrategy.h" + +class Strategy_HalfHalf: public IStrategy { +public: + Strategy_HalfHalf(double ea, double accu, double p = 0, double a = 0); + + virtual bool isValid() const override; + virtual IStrategy *init(double curPrice, double assets, double currency) const override; + virtual std::pair onTrade(double tradePrice, double tradeSize, + double assetsLeft, double currencyLeft) const override; + virtual json::Value exportState() const override; + virtual IStrategy *importState(json::Value src) const override; + virtual double calcOrderSize(double price, double assets) const override; + virtual MinMax calcSafeRange(double assets, double currencies) const override; + virtual double getEquilibrium() const override; + virtual IStrategy *reset() const override; + virtual std::string_view getID() const override; + + static std::string_view id; + + +protected: + double ea; + double accu; + double p; + double a; +}; + +#endif /* SRC_MAIN_STRATEGY_HALFHALF_H_ */ diff --git a/src/main/strategy_keepvalue.cpp b/src/main/strategy_keepvalue.cpp new file mode 100644 index 00000000..35125689 --- /dev/null +++ b/src/main/strategy_keepvalue.cpp @@ -0,0 +1,85 @@ +/* + * strategy_keepvalue.cpp + * + * Created on: 20. 10. 2019 + * Author: ondra + */ + +#include "strategy_keepvalue.h" + +#include +#include "../shared/logOutput.h" +#include + +using ondra_shared::logDebug; + +#include "../shared/logOutput.h" +std::string_view Strategy_KeepValue::id = "keepvalue"; + +Strategy_KeepValue::Strategy_KeepValue(const Config& cfg, double p, double a) +:cfg(cfg),p(p),a(a) +{ +} + +bool Strategy_KeepValue::isValid() const { + return p >0 && (a+cfg.ea) > 0; +} + +IStrategy* Strategy_KeepValue::init(double curPrice, double assets, double) const { + return new Strategy_KeepValue(cfg, curPrice , assets); +} + +std::pair Strategy_KeepValue::onTrade( + double tradePrice, double tradeSize, double assetsLeft, + double currencyLeft) const { + + double k = (a+cfg.ea) * p; + double cf = (assetsLeft-tradeSize+cfg.ea)*(tradePrice - p); + double nv = k * std::log(tradePrice/p); + double pf = cf - nv; + double ap = (pf / tradePrice) * cfg.accum; + double np = pf * (1.0 - cfg.accum); + double new_a = (k / tradePrice) - cfg.ea; + return { + OnTradeResult{np,ap}, new Strategy_KeepValue(cfg, tradePrice, new_a+ap) + }; + +} + +json::Value Strategy_KeepValue::exportState() const { + return json::Object + ("p", p) + ("a", a); +} + +IStrategy* Strategy_KeepValue::importState(json::Value src) const { + return init(src["p"].getNumber(), src["a"].getNumber(),src["acm"].getNumber()); +} + +double Strategy_KeepValue::calcOrderSize(double price, double assets) const { + double k = (a+cfg.ea) * p; + double na = k / price; + return (na - cfg.ea) - assets; +} + +Strategy_KeepValue::MinMax Strategy_KeepValue::calcSafeRange(double assets, + double currencies) const { + double k = p*(a+cfg.ea); + double n = p*std::exp(-currencies/k); + double m = cfg.ea > 0?(k/cfg.ea):std::numeric_limits::infinity(); + return MinMax {n,m}; +} + +double Strategy_KeepValue::getEquilibrium() const { + return p; +} + +std::string_view Strategy_KeepValue::getID() const { + return id; + +} + +IStrategy* Strategy_KeepValue::reset() const { + return new Strategy_KeepValue(cfg); +} + diff --git a/src/main/strategy_keepvalue.h b/src/main/strategy_keepvalue.h new file mode 100644 index 00000000..da39c596 --- /dev/null +++ b/src/main/strategy_keepvalue.h @@ -0,0 +1,44 @@ +/* + * strategy_keepvalue.h + * + * Created on: 20. 10. 2019 + * Author: ondra + */ + +#ifndef SRC_MAIN_STRATEGY_KEEPVALUE_H_ +#define SRC_MAIN_STRATEGY_KEEPVALUE_H_ +#include "istrategy.h" + +class Strategy_KeepValue: public IStrategy { +public: + + struct Config { + double ea; + double accum; + }; + + Strategy_KeepValue(const Config &cfg, double p = 0, double a = 0); + + virtual bool isValid() const override; + virtual IStrategy *init(double curPrice, double assets, double currency) const override; + virtual std::pair onTrade(double tradePrice, double tradeSize, + double assetsLeft, double currencyLeft) const override; + virtual json::Value exportState() const override; + virtual IStrategy *importState(json::Value src) const override; + virtual double calcOrderSize(double price, double assets) const override; + virtual MinMax calcSafeRange(double assets, double currencies) const override; + virtual double getEquilibrium() const override; + virtual std::string_view getID() const override; + virtual IStrategy *reset() const override; + + static std::string_view id; + + +protected: + Config cfg; + double p; + double a; +}; + + +#endif /* SRC_MAIN_STRATEGY_KEEPVALUE_H_ */ diff --git a/src/main/strategy_plfrompos.cpp b/src/main/strategy_plfrompos.cpp new file mode 100644 index 00000000..3deb8678 --- /dev/null +++ b/src/main/strategy_plfrompos.cpp @@ -0,0 +1,130 @@ +/* + * strategy_plfrompos.cpp + * + * Created on: 18. 10. 2019 + * Author: ondra + */ + +#include "strategy_plfrompos.h" +#include +#include + +#include "../shared/logOutput.h" +#include "sgn.h" + +using ondra_shared::logDebug; + +std::string_view Strategy_PLFromPos::id = "plfrompos"; + +Strategy_PLFromPos::Strategy_PLFromPos(const Config &cfg, double p, double pos, double acm) + :cfg(cfg),p(p),pos(pos), acm(acm) +{ +} + + +bool Strategy_PLFromPos::isValid() const { + return p > 0; +} + +IStrategy* Strategy_PLFromPos::init(double curPrice, double assets, double ) const { + return new Strategy_PLFromPos(cfg, curPrice, assets - cfg.neutral_pos); +} + + + +double Strategy_PLFromPos::calcK() const { + return cfg.step / (pow2(p) * 0.01); +} + +double Strategy_PLFromPos::calcNewPos(double tradePrice, bool reducepos) const { + double k = calcK(); + double lin_pos = pos + (p - tradePrice) * k; + double red_pos_inr = 1 + 2*k*(p - tradePrice)/pos; + double red_pos = red_pos_inr > 0? pos * sqrt(red_pos_inr):0; + bool gaining = (pos == 0 || (p - tradePrice) / pos > 0 ); + double new_pos = gaining?lin_pos + :(lin_pos + (red_pos - lin_pos)*cfg.reduce_factor); + + switch (cfg.closeMode) { + case always_close: if (new_pos * pos <= 0) new_pos = 0;break; + case prefer_close: if (!gaining && red_pos == 0) new_pos = 0;break; + case prefer_reverse: if (!gaining && red_pos == 0) new_pos = lin_pos;break; + } + + if (fabs(new_pos) > cfg.maxpos) { + new_pos = (new_pos + cfg.maxpos)/2; + } + return new_pos; +} + +std::pair Strategy_PLFromPos::onTrade( + double tradePrice, double tradeSize, double assetsLeft, + double currencyLeft) const { + + double k = calcK(); + double new_pos = calcNewPos(tradePrice,true); + double act_pos = assetsLeft-acm-cfg.neutral_pos; + double prev_pos = act_pos - tradeSize; +/* if (cfg.maxpos && std::fabs(act_pos) >=cfg.maxpos) { + new_pos = sgn(act_pos) * cfg.maxpos; + }*/ + double ef = (1/ (2*k)) *(pow2(act_pos) - pow2(prev_pos)) + prev_pos * (tradePrice - p); + double np = ef * (1 - cfg.accum); + double ap = ef * cfg.accum; + return { + OnTradeResult{np,ap}, + new Strategy_PLFromPos(cfg,tradePrice, new_pos, acm+ap) + }; +} + +json::Value Strategy_PLFromPos::exportState() const { + return json::Object + ("p",p) + ("pos",pos) + ("acm",acm); + +} + +IStrategy* Strategy_PLFromPos::importState(json::Value src) const { + double new_p = src["p"].getNumber(); + double new_pos = src["pos"].getNumber(); + double new_acm = src["acm"].getNumber(); + return new Strategy_PLFromPos(cfg, new_p, new_pos, new_acm); +} + +double Strategy_PLFromPos::calcOrderSize(double price, double assets) const { + bool reducepos = cfg.maxpos == 0 || std::fabs(assets-acm-cfg.neutral_pos) < cfg.maxpos; + double new_pos = calcNewPos(price, reducepos); + return new_pos + cfg.neutral_pos + acm - assets; + +} + +Strategy_PLFromPos::MinMax Strategy_PLFromPos::calcSafeRange(double assets, + double currencies) const { + double pos = assets-cfg.neutral_pos-acm; + double k = calcK(); + double mp = pos / k + p; + if (cfg.maxpos) { + return MinMax { + -cfg.maxpos /k + mp, + cfg.maxpos /k + mp + }; + } else { + return MinMax { + p - sqrt(2*currencies/calcK()), + assets / calcK() + p + }; + } +} + +double Strategy_PLFromPos::getEquilibrium() const { + return p; +} + +std::string_view Strategy_PLFromPos::getID() const { + return id; +} + +IStrategy* Strategy_PLFromPos::reset() const { + return new Strategy_PLFromPos(cfg); +} diff --git a/src/main/strategy_plfrompos.h b/src/main/strategy_plfrompos.h new file mode 100644 index 00000000..a0eabb10 --- /dev/null +++ b/src/main/strategy_plfrompos.h @@ -0,0 +1,63 @@ +/* + * strategy_plfrompos.h + * + * Created on: 18. 10. 2019 + * Author: ondra + */ + +#ifndef SRC_MAIN_STRATEGY_PLFROMPOS_H_ +#define SRC_MAIN_STRATEGY_PLFROMPOS_H_ + +#include "istrategy.h" + +class Strategy_PLFromPos: public IStrategy { +public: + enum CloseMode { + always_close, + prefer_close, + prefer_reverse + }; + + + + struct Config { + double step; + double accum; + double neutral_pos; + double maxpos; + double reduce_factor; + CloseMode closeMode; + + }; + + Strategy_PLFromPos(const Config &cfg, double p = 0,double pos = 0, double acm = 0); + + virtual bool isValid() const override; + virtual IStrategy *init(double curPrice, double assets, double currency) const override; + virtual std::pair onTrade(double tradePrice, double tradeSize, + double assetsLeft, double currencyLeft) const override; + virtual json::Value exportState() const override; + virtual IStrategy *importState(json::Value src) const override; + virtual double calcOrderSize(double price, double assets) const override; + virtual MinMax calcSafeRange(double assets, double currencies) const override; + virtual double getEquilibrium() const override; + virtual std::string_view getID() const override; + virtual IStrategy *reset() const override; + + static std::string_view id; + + + + double calcK() const; + +protected: + Config cfg; + double p; + double pos; + double acm; + +private: + double calcNewPos(double tradePrice, bool reducepos) const; +}; + +#endif /* SRC_MAIN_STRATEGY_PLFROMPOS_H_ */ diff --git a/src/main/traders.cpp b/src/main/traders.cpp new file mode 100644 index 00000000..e6df1879 --- /dev/null +++ b/src/main/traders.cpp @@ -0,0 +1,160 @@ +/* + * traders.cpp + * + * Created on: 17. 9. 2019 + * Author: ondra + */ + + + + +#include "traders.h" + +#include "ext_stockapi.h" +NamedMTrader::NamedMTrader(IStockSelector &sel, StoragePtr &&storage, PStatSvc statsvc, Config cfg, std::string &&name) + :MTrader(sel, std::move(storage), std::move(statsvc), cfg), ident(std::move(name)) { +} + +void NamedMTrader::perform(bool manually) { + using namespace ondra_shared; + LogObject lg(ident); + LogObject::Swap swap(lg); + try { + MTrader::perform(manually); + } catch (std::exception &e) { + logError("$1", e.what()); + } +} + + + + +void StockSelector::loadStockMarkets(const ondra_shared::IniConfig::Section &ini, bool test) { + std::vector data; + for (auto &&def: ini) { + ondra_shared::StrViewA name = def.first; + ondra_shared::StrViewA cmdline = def.second.getString(); + ondra_shared::StrViewA workDir = def.second.getCurPath(); + data.push_back(StockMarketMap::value_type(name,std::make_unique(workDir, name, cmdline))); + } + StockMarketMap map(std::move(data)); + stock_markets.swap(map); +} +IStockApi *StockSelector::getStock(const std::string_view &stockName) const { + auto f = stock_markets.find(stockName); + if (f == stock_markets.cend()) return nullptr; + return f->second.get(); +} +void StockSelector::addStockMarket(ondra_shared::StrViewA name, PStockApi &&market) { + stock_markets.insert(std::pair(name,std::move(market))); +} + +void StockSelector::forEachStock(EnumFn fn) const { + for(auto &&x: stock_markets) { + fn(x.first, *x.second); + } +} +void StockSelector::clear() { + stock_markets.clear(); +} + +Traders::Traders(ondra_shared::Scheduler sch, + const ondra_shared::IniConfig::Section &ini, + bool test, + StorageFactory &sf, + Report &rpt, + IDailyPerfModule &perfMod, + std::string iconPath) + +: +test(test) +,sf(sf) +,rpt(rpt) +,perfMod(perfMod) +,iconPath(iconPath) +{ + stockSelector.loadStockMarkets(ini, test); +} + +void Traders::clear() { + traders.clear(); + stockSelector.clear(); +} + +void Traders::loadIcon(MTrader &t) { + IStockApi &api = t.getBroker(); + const IBrokerIcon *bicon = dynamic_cast(&api); + if (bicon) + bicon->saveIconToDisk(iconPath); +} + +void Traders::addTrader(const MTrader::Config &mcfg ,ondra_shared::StrViewA n) { + using namespace ondra_shared; + + LogObject lg(n); + LogObject::Swap swp(lg); + try { + logProgress("Started trader $1 (for $2)", n, mcfg.pairsymb); + auto t = std::make_unique(stockSelector, sf.create(n), + std::make_unique(n, rpt, &perfMod), mcfg, n); + loadIcon(*t); + traders.insert(std::pair(StrViewA(t->ident), std::move(t))); + } catch (const std::exception &e) { + logFatal("Error: $1", e.what()); + throw std::runtime_error(std::string("Unable to initialize trader: ").append(n).append(" - ").append(e.what())); + } + +} + + +void Traders::removeTrader(ondra_shared::StrViewA n, bool including_state) { + NamedMTrader *t = find(n); + if (t) { + if (including_state) { + //stop trader + t->stop(); + //perform while stop cancels all orders + t->perform(true); + //drop state + t->dropState(); + //now we can erase + } + traders.erase(n); + } +} + +void Traders::resetBrokers() { + stockSelector.forEachStock([](json::StrViewA, IStockApi&api) { + api.reset(); + }); +} + +void Traders::runTraders(bool manually) { + resetBrokers(); + + for (auto &&t : traders) { + t.second->perform(manually); + } +} + +Traders::TMap::const_iterator Traders::begin() const { + return traders.begin(); +} + +Traders::TMap::const_iterator Traders::end() const { + return traders.end(); +} + +NamedMTrader *Traders::find(json::StrViewA id) const { + auto iter = traders.find(id); + if (iter == traders.end()) return nullptr; + else return iter->second.get(); +} + +void Traders::loadIcons(const std::string &path) { + for (auto &&t: traders) { + IStockApi &api = t.second->getBroker(); + const IBrokerIcon *bicon = dynamic_cast(&api); + if (bicon) bicon->saveIconToDisk(path); + } +} diff --git a/src/main/traders.h b/src/main/traders.h new file mode 100644 index 00000000..331081a4 --- /dev/null +++ b/src/main/traders.h @@ -0,0 +1,84 @@ +/* + * trades.h + * + * Created on: 17. 9. 2019 + * Author: ondra + */ + +#ifndef SRC_MAIN_TRADERS_H_ +#define SRC_MAIN_TRADERS_H_ +#include "../shared/scheduler.h" +#include "istockapi.h" +#include "mtrader.h" +#include "stats2report.h" + + +using StatsSvc = Stats2Report; + +class NamedMTrader: public MTrader { +public: + NamedMTrader(IStockSelector &sel, StoragePtr &&storage, PStatSvc statsvc, Config cfg, std::string &&name); + void perform(bool manually); + const std::string ident; + +}; + +class StockSelector: public IStockSelector{ +public: + using PStockApi = std::unique_ptr; + using StockMarketMap = ondra_shared::linear_map>; + + StockMarketMap stock_markets; + + void loadStockMarkets(const ondra_shared::IniConfig::Section &ini, bool test); + virtual IStockApi *getStock(const std::string_view &stockName) const override; + void addStockMarket(ondra_shared::StrViewA name, PStockApi &&market); + virtual void forEachStock(EnumFn fn) const override; + void clear(); +}; + + +class Traders { +public: + + using TMap = ondra_shared::linear_map >; + TMap traders; + StockSelector stockSelector; + bool test; + StorageFactory &sf; + Report &rpt; + IDailyPerfModule &perfMod; + std::string iconPath; + + Traders(ondra_shared::Scheduler sch, + const ondra_shared::IniConfig::Section &ini, + bool test, + StorageFactory &sf, + Report &rpt, + IDailyPerfModule &perfMod, + std::string iconPath); + Traders(const Traders &&other) = delete; + void clear(); + + TMap::const_iterator begin() const; + TMap::const_iterator end() const; + + + + void addTrader(const MTrader::Config &mcfg, ondra_shared::StrViewA n); + void removeTrader(ondra_shared::StrViewA n, bool including_state); + void loadIcons(const std::string &path); + + + void runTraders(bool manually); + void resetBrokers(); + NamedMTrader *find(json::StrViewA id) const; + +private: + void loadIcon(MTrader &t); +}; + + + + +#endif /* SRC_MAIN_TRADERS_H_ */ diff --git a/src/main/webcfg.cpp b/src/main/webcfg.cpp new file mode 100644 index 00000000..5d71f8be --- /dev/null +++ b/src/main/webcfg.cpp @@ -0,0 +1,735 @@ +/* + * webcfg.cpp + * + * Created on: 21. 7. 2019 + * Author: ondra + */ + + +#include "webcfg.h" + +#include +#include +#include +#include +#include "../imtjson/src/imtjson/binary.h" +#include "../imtjson/src/imtjson/ivalue.h" +#include "../imtjson/src/imtjson/parser.h" +#include "../imtjson/src/imtjson/operations.h" +#include "../imtjson/src/imtjson/serializer.h" +#include "../server/src/simpleServer/query_parser.h" +#include "../server/src/simpleServer/urlencode.h" +#include "../shared/ini_config.h" +#include "../shared/logOutput.h" +#include "apikeys.h" +#include "ext_stockapi.h" + +using namespace json; +using ondra_shared::StrViewA; +using ondra_shared::IniConfig; +using ondra_shared::logError; +using namespace simpleServer; + +NamedEnum WebCfg::strCommand({ + {WebCfg::config, "config"}, + {WebCfg::serialnr, "serial"}, + {WebCfg::brokers, "brokers"}, + {WebCfg::traders, "traders"}, + {WebCfg::stop, "stop"}, + {WebCfg::logout, "logout"}, + {WebCfg::logout_commit, "logout_commit"}, + {WebCfg::editor, "editor"} +}); + +WebCfg::WebCfg( + ondra_shared::RefCntPtr state, + const std::string &realm, + Traders &traders, + Dispatch &&dispatch) + :auth(realm, state->admins) + ,trlist(traders) + ,dispatch(std::move(dispatch)) + ,state(state) +{ + +} + +WebCfg::~WebCfg() { +} + +bool WebCfg::operator ()(const simpleServer::HTTPRequest &req, + const ondra_shared::StrViewA &vpath) const { + + QueryParser qp(vpath); + StrViewA path = qp.getPath(); + auto splt = path.split("/",3); + splt(); + StrViewA pfx = splt(); + if (pfx != "api") return false; + StrViewA c = splt(); + StrViewA rest = splt(); + auto cmd = strCommand.find(c); + if (cmd == nullptr) { + return false; + } else { + if (!auth.checkAuth(req)) return true; + switch (*cmd) { + case config: return reqConfig(req); + case serialnr: return reqSerial(req); + case brokers: return reqBrokers(req, rest); + case stop: return reqStop(req); + case traders: return reqTraders(req, rest); + case logout: return reqLogout(req,false); + case logout_commit: return reqLogout(req,true); + case editor: return reqEditor(req); + } + } + return false; +} + +static Value hashPswds(Value data) { + Value list = data["users"]; + Array o; + for (Value v: list) { + Value p = v["password"]; + if (p.defined()) { + Value u = v["username"]; + Value pwdhash = AuthUserList::hashPwd(u.toString().str(),p.toString().str()); + v = v.merge(Value(json::object,{ + Value(p.getKey(), json::undefined), + Value("pwdhash", pwdhash) + },false)); + } + o.push_back(v); + } + return data.replace("users", o); +} + +bool WebCfg::reqConfig(simpleServer::HTTPRequest req) const { + + if (!req.allowMethods({"GET","PUT"})) return true; + if (req.getMethod() == "GET") { + + Sync _(state->lock); + json::Value data = state->config->load(); + if (!data.defined()) data = Object("revision",0); + req.sendResponse("application/json",data.stringify()); + + } else { + Traders &traders = this->trlist; + RefCntPtr state = this->state; + + + state->lock.lock(); + req.readBodyAsync(1024*1024, [state,&traders,dispatch = this->dispatch](simpleServer::HTTPRequest req) mutable { + try { + Sync _(state->lock); + state->lock.unlock(); + + Value data = Value::fromString(StrViewA(BinaryView(req.getUserBuffer()))); + unsigned int serial = data["revision"].getUInt(); + if (serial != state->write_serial) { + req.sendErrorPage(409); + return ; + } + data = hashPswds(data); + Value trs = data["traders"]; + for (Value v: trs) { + StrViewA name = v.getKey(); + try { + MTrader_Config().loadConfig(v,false); + } catch (std::exception &e) { + std::string msg(name.data,name.length); + msg.append(" - "); + msg.append(e.what()); + req.sendErrorPage(406, StrViewA(), msg); + return; + } + } + data = data.replace("revision", state->write_serial+1); + Value apikeys = data["apikeys"]; + state->config->store(data.replace("apikeys", Value())); + state->write_serial = serial+1;; + dispatch([&traders, state, req, data, apikeys] { + try { + state->applyConfig(traders); + if (apikeys.type() == json::object) { + for (Value v: apikeys) { + StrViewA broker = v.getKey(); + IStockApi *b = traders.stockSelector.getStock(broker); + if (b) { + IApiKey *apik = dynamic_cast(b); + apik->setApiKey(v); + } + } + } + traders.runTraders(true); + traders.rpt.genReport(); + req.sendResponse(HTTPResponse(202).contentType("application/json"),data.stringify()); + } catch (std::exception &e) { + req.sendErrorPage(500,StrViewA(),e.what()); + } + }); + + + } catch (std::exception &e) { + req.sendErrorPage(500,StrViewA(),e.what()); + } + + }); + + + } + return true; + + +} + + +bool WebCfg::reqSerial(simpleServer::HTTPRequest req) const { + if (!req.allowMethods({"GET"})) return true; + req.sendResponse("text/plain") << serial << "\r\n"; + return true; +} + + +static double getSafeBalance(IStockApi *api, std::string_view symb) { + try { + return api->getBalance(symb); + } catch (...) { + return 0; + } +} + +static json::Value brokerToJSON(const IStockApi::BrokerInfo &binfo) { + Value res = Object("name", binfo.name)("trading_enabled", + binfo.trading_enabled)("exchangeName", binfo.exchangeName)( + "exchangeUrl", binfo.exchangeUrl)("version", binfo.version); + return res; +} + + +bool WebCfg::reqBrokers(simpleServer::HTTPRequest req, ondra_shared::StrViewA rest) const { + if (rest.empty()) { + if (!req.allowMethods({"GET"})) return true; + Array brokers; + trlist.stockSelector.forEachStock([&](const std::string_view &name,IStockApi &){ + brokers.push_back(name); + }); + Object obj("entries", brokers); + req.sendResponse("application/json",Value(obj).stringify()); + return true; + } else { + json::String vpath = rest; + auto splt = StrViewA(vpath).split("/",2); + StrViewA urlbroker = splt(); + if (urlbroker == "_reload") { + if (!req.allowMethods({"POST"})) return true; + dispatch([=] { + trlist.stockSelector.forEachStock([&](const std::string_view &,IStockApi &x){ + ExtStockApi *ex = dynamic_cast(&x); + ex->stop(); + }); + req.sendResponse("application/json","true"); + }); + return true; + } else if (urlbroker == "_all") { + if (!req.allowMethods({"GET"})) return true; + Array res; + trlist.stockSelector.forEachStock([&](std::string_view, IStockApi &api) { + res.push_back(brokerToJSON(api.getBrokerInfo())); + }); + req.sendResponse("application/json",Value(res).stringify()); + return true; + } + std::string broker = urlDecode(urlbroker); + IStockApi *api = trlist.stockSelector.getStock(broker); + + return reqBrokerSpec(req, splt, api); + } +} + +static Value getOpenOrders(IStockApi &api, const std::string_view &pair) { + Value orders = json::array; + try { + auto ords = api.getOpenOrders(pair); + orders = Value(json::array, ords.begin(), + ords.end(), + [&](const IStockApi::Order &ord) { + return Object("price", ord.price)( + "size", ord.size)("clientId", + ord.client_id)("id", ord.id); + }); + } catch (...) { + + } + return orders; + +} + +static Value getPairInfo(IStockApi &api, const std::string_view &pair) { + IStockApi::MarketInfo nfo = api.getMarketInfo(pair); + double ab = getSafeBalance(&api, nfo.asset_symbol); + double cb = getSafeBalance(&api, nfo.currency_symbol); + Value resp = Object("symbol",pair)("asset_symbol", nfo.asset_symbol)( + "currency_symbol", nfo.currency_symbol)("fees", + nfo.fees)("leverage", nfo.leverage)( + "invert_price", nfo.invert_price)( + "asset_balance", ab)("currency_balance", cb)( + "min_size", nfo.min_size)("price", + api.getTicker(pair).last); + return resp; + +} + +bool WebCfg::reqBrokerSpec(simpleServer::HTTPRequest req, + ondra_shared::StrViewA vpath, IStockApi *api) const { + + if (api == nullptr) { + req.sendErrorPage(404); + return true; + } + + HTTPResponse hdr(200); + hdr.cacheFor(30); + hdr.contentType("application/json"); + + try { + auto splt = StrViewA(vpath).split("/"); + StrViewA entry = splt(); + StrViewA pair = splt(); + StrViewA orders = splt(); + + if (req.getPath().indexOf("reset=1") != StrViewA::npos) { + api->reset(); + } + + if (entry.empty()) { + if (!req.allowMethods( { "GET" })) + return true; + auto binfo = api->getBrokerInfo(); + Value res = brokerToJSON(binfo).replace("entries", { "icon.png", "pairs","apikey","settings","licence"}); + req.sendResponse(std::move(hdr),res.stringify()); + return true; + } else if (entry == "licence") { + if (!req.allowMethods( { "GET" })) + return true; + auto binfo = api->getBrokerInfo(); + req.sendResponse(std::move(hdr),Value(binfo.licence).stringify()); + } else if (entry == "icon.png") { + if (!req.allowMethods( { "GET" })) + return true; + auto binfo = api->getBrokerInfo(); + Value v = base64->decodeBinaryValue(binfo.favicon); + Binary b = v.getBinary(base64); + req.sendResponse( + HTTPResponse(200).contentType("image/png").cacheFor(600), + StrViewA(b)); + return true; + } else if (entry == "apikey") { + IApiKey *kk = dynamic_cast(api); + if (kk == nullptr) { + req.sendErrorPage(403); + return true; + } + if (!req.allowMethods( { "GET" })) + return true; + req.sendResponse(std::move(hdr), + kk->getApiKeyFields().toString().str()); + return true; + } else if (entry == "settings") { + + IBrokerControl *bc = dynamic_cast(api); + if (bc == nullptr) { + req.sendErrorPage(403);return true; + } + if (!req.allowMethods( { "GET", "PUT" })) return true; + if (req.getMethod() == "GET") { + req.sendResponse(std::move(hdr), Value(bc->getSettings("")).stringify()); + } else { + Stream s = req.getBodyStream(); + Value v = Value::parse(s); + bc->setSettings(v); + req.sendResponse("application/json","true",202); + return true; + } + } else if (entry == "pairs") { + if (pair.empty()) { + if (!req.allowMethods( { "GET" })) + return true; + Array p; + auto pairs = api->getAllPairs(); + for (auto &&k : pairs) + p.push_back(k); + Object obj("entries", p); + req.sendResponse(std::move(hdr), Value(obj).stringify()); + return true; + } else { + std::string p = urlDecode(pair); + + try { + if (orders.empty()) { + if (!req.allowMethods( { "GET" })) + return true; + Value resp = getPairInfo(*api, p).replace("entries",{"orders", "ticker" }); + req.sendResponse(std::move(hdr), resp.stringify()); + return true; + } else if (orders == "ticker") { + if (!req.allowMethods( { "GET" })) + return true; + auto t = api->getTicker(p); + Value ticker = Object("ask", t.ask)("bid", t.bid)( + "last", t.last)("time", t.time); + req.sendResponse(std::move(hdr), ticker.stringify()); + return true; + } else if (orders == "settings") { + + IBrokerControl *bc = dynamic_cast(api); + if (bc == nullptr) { + req.sendErrorPage(403);return true; + } + if (!req.allowMethods( { "GET", "PUT" })) return true; + if (req.getMethod() == "GET") { + req.sendResponse(std::move(hdr), Value(bc->getSettings(pair)).stringify()); + } else { + Stream s = req.getBodyStream(); + Value v = Value::parse(s); + bc->setSettings(v); + req.sendResponse("application/json","true",202); + return true; + } + }else if (orders == "orders") { + if (!req.allowMethods( { "GET", "POST", "DELETE" })) + return true; + if (req.getMethod() == "GET") { + Value orders = getOpenOrders(*api, p); + req.sendResponse(std::move(hdr), + orders.stringify()); + return true; + } else if (req.getMethod() == "DELETE") { + api->reset(); + auto ords = api->getOpenOrders(p); + for (auto &&x : ords) { + api->placeOrder(p, 0, 0, Value(), x.id, 0); + } + api->reset(); + req.sendResponse(std::move(hdr), "true"); + return true; + } else { + api->reset(); + Stream s = req.getBodyStream(); + Value parsed = Value::parse(s); + Value price = parsed["price"]; + if (price.type() == json::string) { + auto ticker = api->getTicker(pair); + if (price.getString() == "ask") + price = ticker.ask; + else if (price.getString() == "bid") + price = ticker.bid; + else { + req.sendErrorPage(400, "", "Invalid price"); + return true; + } + } + Value res = api->placeOrder(pair, + parsed["size"].getNumber(), + price.getNumber(), parsed["clientId"], + parsed["replaceId"], + parsed["replaceSize"].getNumber()); + req.sendResponse(std::move(hdr), res.stringify()); + return true; + } + + } + + } catch (...) { + auto pp = api->getAllPairs(); + auto f = std::find(pp.begin(), pp.end(), p); + if (f == pp.end()) { + req.sendErrorPage(404); + return true; + } else { + throw; + } + + } + } + } + req.sendErrorPage(404); + return true; + } catch (std::exception &e) { + req.sendErrorPage(500, StrViewA(), e.what()); + return true; + } + return true; + +} + +bool WebCfg::reqTraders(simpleServer::HTTPRequest req, ondra_shared::StrViewA vpath) const { + std::string path = vpath; + dispatch([path,req, this]() mutable { + HTTPResponse hdr(200); + hdr.contentType("application/json"); + + try { + if (path.empty()) { + if (!req.allowMethods({"GET"})) return ; + Value res (json::array, trlist.begin(), trlist.end(), [&](auto &&x) { + return x.first; + }); + res = Object("entries", res); + req.sendResponse(std::move(hdr), res.stringify()); + } else { + auto splt = StrViewA(path).split("/"); + std::string trid = urlDecode(StrViewA(splt())); + auto tr = trlist.find(trid); + if (tr == nullptr) { + req.sendErrorPage(404); + } else if (!splt) { + if (!req.allowMethods({"GET","DELETE"})) return ; + if (req.getMethod() == "DELETE") { + trlist.removeTrader(trid, false); + req.sendResponse(std::move(hdr), "true"); + } else { + req.sendResponse(std::move(hdr), + Value(Object("entries",{"stop","reset","repair","broker","trading"})).stringify()); + } + } else { + tr->init(); + auto cmd = urlDecode(StrViewA(splt())); + if (cmd == "reset") { + if (!req.allowMethods({"POST"})) return ; + tr->reset(); + req.sendResponse(std::move(hdr), "true"); + } else if (cmd == "stop") { + if (!req.allowMethods({"POST"})) return ; + tr->stop(); + req.sendResponse(std::move(hdr), "true"); + } else if (cmd == "repair") { + if (!req.allowMethods({"POST"})) return ; + tr->repair(); + req.sendResponse(std::move(hdr), "true"); + } else if (cmd == "broker") { + StrViewA nx = splt(); + StrViewA vpath = path; + StrViewA restpath = vpath.substr(nx.data - vpath.data); + reqBrokerSpec(req, restpath, &(tr->getBroker())); + } else if (cmd == "trading") { + Object out; + auto chart = tr->getChart(); + auto &&broker = tr->getBroker(); + broker.reset(); + if (chart.length>600) chart.substr(chart.length-600); + out.set("chart", Value(json::array,chart.begin(), chart.end(),[&](auto &&item) { + return Object("time", item.time)("last",item.last); + })); + std::size_t start = chart.empty()?0:chart[0].time; + auto trades = tr->getTrades(); + out.set("trades", Value(json::array, trades.begin(), trades.end(),[&](auto &&item) { + if (item.time >= start) return item.toJSON(); else return Value(); + })); + out.set("ticker", ([&](auto &&t) { + return Object("ask", t.ask)("bid", t.bid)("last", t.last)("time", t.time); + })(broker.getTicker(tr->getConfig().pairsymb))); + out.set("orders", getOpenOrders(broker, tr->getConfig().pairsymb)); + out.set("broker", tr->getConfig().broker); + out.set("pair", getPairInfo(broker, tr->getConfig().pairsymb)); + req.sendResponse(std::move(hdr), Value(out).stringify()); + } + + } + } + } catch (std::exception &e) { + req.sendErrorPage(500, StrViewA(), e.what()); + } + + + }); + return true; +} + + + +static void AULFromJSON(json::Value js, AuthUserList &aul, bool admin) { + using UserVector = std::vector; + using LoginPwd = AuthUserList::LoginPwd; + + UserVector ulist = js.reduce([&]( + UserVector &curVal, Value r){ + Value username = r["username"]; + Value password = r["pwdhash"]; + Value isadmin = r["admin"]; + + if (!admin || isadmin.getBool()) { + curVal.push_back(LoginPwd(username.toString().str(), password.toString().str())); + } + return curVal; + },UserVector()); + + aul.setUsers(std::move(ulist)); +} + + +void WebCfg::State::init(json::Value data) { + if (data.defined()) { + this->write_serial = data["revision"].getUInt(); + if (data["guest"].getBool() == false) { + AULFromJSON(data["users"],*users, false); + }else { + users->setUsers({}); + } + AULFromJSON(data["users"],*admins, true); + } + +} +void WebCfg::State::applyConfig(Traders &t) { + auto data = config->load(); + init(data); + for (auto &&n :traderNames) { + t.removeTrader(n, !data["traders"][n].defined()); + } + + traderNames.clear(); + + for (Value v: data["traders"]) { + try { + MTrader_Config cfg; + cfg.loadConfig(v, t.test); + t.addTrader(cfg,v.getKey()); + traderNames.push_back(v.getKey()); + } catch (std::exception &e) { + logError("Failed to initialized trader $1 - $2", v.getKey(), e.what()); + } + } + Value newInterval = data["report_interval"]; + if (newInterval.defined()) { + t.rpt.setInterval(newInterval.getUInt()); + } +} + +void WebCfg::State::setAdminAuth(StrViewA auth) { + auto ulist = AuthUserList::decodeMultipleBasicAuth(auth); + auto ulist2 = ulist; + users->setCfgUsers(std::move(ulist)); + admins->setCfgUsers(std::move(ulist2)); +} + +void WebCfg::State::init() { + init(config->load()); +} + +bool WebCfg::reqLogout(simpleServer::HTTPRequest req, bool commit) const { + + auto hdr = req["Authorization"]; + auto hdr_splt = hdr.split(" "); + hdr_splt(); + StrViewA cred = hdr_splt(); + auto credobj = AuthUserList::decodeBasicAuth(cred); + if (commit) { + if (state->logout_commit(std::move(credobj.first))) + auth.genError(req); + else + req.sendResponse("text/plain",""); + } else { + state->logout_user(std::move(credobj.first)); + std::string rndstr; + std::time_t t = std::time(nullptr); + rndstr = "?"; + ondra_shared::unsignedToString(t,[&](char c){rndstr.push_back(c);},16,8); + req.redirect(strCommand[logout_commit].data+rndstr,Redirect::temporary_GET); + } + + return true; +} + +bool WebCfg::reqStop(simpleServer::HTTPRequest req) const { + if (!req.allowMethods({"POST"})) return true; + + dispatch([=]{ + + HTTPResponse hdr(200); + hdr.contentType("application/json"); + + for (auto &&x: trlist) { + x.second->stop(); + }; + trlist.resetBrokers(); + trlist.runTraders(true); + trlist.rpt.genReport(); + trlist.resetBrokers(); + req.sendResponse(std::move(hdr), "true"); + + }); + + return true; +} + +void WebCfg::State::logout_user(std::string &&user) { + Sync _(lock); + logout_users.insert(std::move(user)); +} + +bool WebCfg::State::logout_commit(std::string &&user) { + Sync _(lock); + if (logout_users.find(user) == logout_users.end()) { + return false; + } else { + logout_users.erase(user); + return true; + } +} + +bool WebCfg::reqEditor(simpleServer::HTTPRequest req) const { + if (!req.allowMethods({"POST"})) return true; + req.readBodyAsync(10000,[&trlist = this->trlist, state = this->state](simpleServer::HTTPRequest req) { + + Value data = Value::fromString(StrViewA(BinaryView(req.getUserBuffer()))); + Value broker = data["broker"]; + Value trader = data["trader"]; + Value pair = data["pair"]; + std::string p; + + Sync _(state->lock); + NamedMTrader *tr = trlist.find(trader.toString().str()); + IStockApi *api = nullptr; + if (tr == nullptr) { + api = trlist.stockSelector.getStock(broker.toString().str()); + } else { + api = &tr->getBroker(); + } + if (api == nullptr) { + return req.sendErrorPage(404); + } + if (tr && !pair.hasValue()) { + p = tr->getConfig().pairsymb; + } else { + p = pair.toString().str(); + } + + + api->reset(); + auto binfo = api->getBrokerInfo(); + auto pairinfo = api->getMarketInfo(p); + + + Value strategy; + if (tr) { + strategy = tr->getStrategy().exportState(); + } + + Object result; + result.set("broker",Object + ("name", binfo.name) + ("exchangeName", binfo.exchangeName) + ("version", binfo.version) + ("settings", binfo.settings) + ("trading_enabled", binfo.trading_enabled)); + result.set("pair", getPairInfo(*api, p)); + result.set("orders", getOpenOrders(*api, p)); + result.set("strategy", strategy); + + req.sendResponse("application/json", Value(result).stringify()); + }); + return true; +} + diff --git a/src/main/webcfg.h b/src/main/webcfg.h new file mode 100644 index 00000000..d3b31150 --- /dev/null +++ b/src/main/webcfg.h @@ -0,0 +1,110 @@ +/* + * webcfg.h + * + * Created on: 21. 7. 2019 + * Author: ondra + */ + +#ifndef SRC_MAIN_WEBCFG_H_ +#define SRC_MAIN_WEBCFG_H_ + +#include +#include +#include +#include +#include + +#include +#include "../server/src/rpc/rpcServer.h" +#include "istockapi.h" +#include "authmapper.h" +#include "traders.h" + + +class WebCfg { +public: + + using Action = std::function; + using Dispatch = ondra_shared::shared_function; + + + class State : public ondra_shared::RefCntObj{ + public: + std::recursive_mutex lock; + unsigned int write_serial = 0; + PStorage config; + ondra_shared::RefCntPtr users, admins; + std::vector traderNames; + + State( PStorage &&config, + ondra_shared::RefCntPtr users, + ondra_shared::RefCntPtr admins): + config(std::move(config)), + users(users), + admins(admins) {} + + ~State() { + lock.lock(); + lock.unlock(); + } + + void init(); + void init(json::Value v); + void applyConfig(Traders &t); + void setAdminAuth(json::StrViewA auth); + ondra_shared::linear_set logout_users; + + void logout_user(std::string &&user); + bool logout_commit(std::string &&user); + }; + + + WebCfg( ondra_shared::RefCntPtr state, + const std::string &realm, + Traders &traders, + Dispatch &&dispatch); + + ~WebCfg(); + + bool operator()(const simpleServer::HTTPRequest &req, const ondra_shared::StrViewA &vpath) const; + + + enum Command { + config, + serialnr, + brokers, + traders, + stop, + logout, + logout_commit, + editor, + }; + + AuthMapper auth; + Traders &trlist; + Dispatch dispatch; + unsigned int serial; + + static json::NamedEnum strCommand; + + +protected: + bool reqConfig(simpleServer::HTTPRequest req) const; + bool reqSerial(simpleServer::HTTPRequest req) const; + bool reqBrokers(simpleServer::HTTPRequest req, ondra_shared::StrViewA rest) const; + bool reqTraders(simpleServer::HTTPRequest req, ondra_shared::StrViewA rest) const; + bool reqLogout(simpleServer::HTTPRequest req, bool commit) const; + bool reqStop(simpleServer::HTTPRequest req) const; + bool reqBrokerSpec(simpleServer::HTTPRequest req, ondra_shared::StrViewA rest, IStockApi *api) const; + bool reqEditor(simpleServer::HTTPRequest req) const; + + using Sync = std::unique_lock; + + + + ondra_shared::RefCntPtr state; +}; + + + +#endif /* SRC_MAIN_WEBCFG_H_ */ diff --git a/src/poloniex/CMakeLists.txt b/src/poloniex/CMakeLists.txt index 82389ee5..f45b87db 100644 --- a/src/poloniex/CMakeLists.txt +++ b/src/poloniex/CMakeLists.txt @@ -4,6 +4,6 @@ add_compile_options(-std=c++17) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin/brokers/) -add_executable (poloniex main.cpp orderdatadb.cpp config.cpp proxy.cpp ../brokers/api.cpp ) +add_executable (poloniex main.cpp orderdatadb.cpp proxy.cpp ../brokers/api.cpp ) target_link_libraries (poloniex LINK_PUBLIC imtjson curlpp ssl crypto curl stdc++fs pthread) install(TARGETS poloniex DESTINATION "bin/brokers") diff --git a/src/poloniex/config.cpp b/src/poloniex/config.cpp deleted file mode 100644 index 5b32b0db..00000000 --- a/src/poloniex/config.cpp +++ /dev/null @@ -1,22 +0,0 @@ -/* - * config.cpp - * - * Created on: 21. 5. 2019 - * Author: ondra - */ - - - -#include "config.h" - -Config load(const ondra_shared::IniConfig::Section& cfg) { - Config r; - - r.privKey = cfg.mandatory["secret"].getString(); - r.pubKey = cfg.mandatory["key"].getString(); - r.apiPrivUrl = cfg.mandatory["private_url"].getString(); - r.apiPublicUrl = cfg.mandatory["public_url"].getString(); - return r; -} - - diff --git a/src/poloniex/config.h b/src/poloniex/config.h deleted file mode 100644 index 55f136ff..00000000 --- a/src/poloniex/config.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * config.h - * - * Created on: 21. 5. 2019 - * Author: ondra - */ - -#ifndef SRC_COINMATE_CONFIG_H_ -#define SRC_COINMATE_CONFIG_H_ -#include "../shared/ini_config.h" - - - -struct Config { - std::string apiPrivUrl; - std::string apiPublicUrl; - std::string privKey; - std::string pubKey; - - }; - -Config load(const ondra_shared::IniConfig::Section &cfg); - -#endif /* SRC_COINMATE_CONFIG_H_ */ diff --git a/src/poloniex/main.cpp b/src/poloniex/main.cpp index 5f59ed4b..495f6686 100644 --- a/src/poloniex/main.cpp +++ b/src/poloniex/main.cpp @@ -9,7 +9,6 @@ #include #include "../imtjson/src/imtjson/operations.h" -#include "config.h" #include "proxy.h" #include "../main/istockapi.h" #include @@ -24,13 +23,18 @@ using namespace json; class Interface: public AbstractBrokerAPI { public: - Proxy &px; + Proxy px; - Interface(Proxy &cm, std::string dbpath):px(cm),orderdb(dbpath) {} + Interface(const std::string &path):AbstractBrokerAPI( + path, + { + Object("name","key")("type","string")("label","Key"), + Object("name","secret")("type","string")("label","Secret") + }),orderdb(path+".db") {} virtual double getBalance(const std::string_view & symb) override; - virtual TradeHistory getTrades(json::Value lastId, std::uintptr_t fromTime, const std::string_view & pair) override; + virtual TradesSync syncTrades(json::Value lastId, const std::string_view & pair) override; virtual Orders getOpenOrders(const std::string_view & par)override; virtual Ticker getTicker(const std::string_view & piar)override; virtual json::Value placeOrder(const std::string_view & pair, @@ -44,6 +48,9 @@ class Interface: public AbstractBrokerAPI { virtual double getFees(const std::string_view &pair)override; virtual std::vector getAllPairs()override; virtual void enable_debug(bool enable) override {px.debug = enable;} + virtual BrokerInfo getBrokerInfo() override; + virtual void onLoadApiKey(json::Value keyData) override; + virtual void onInit() override; Value balanceCache; Value tickerCache; @@ -98,32 +105,58 @@ class Interface: public AbstractBrokerAPI { } - Interface::TradeHistory Interface::getTrades(json::Value lastId, std::uintptr_t fromTime, const std::string_view & pair) { + Interface::TradesSync Interface::syncTrades(json::Value lastId, const std::string_view & pair) { - if (fromTime < lastFromTime) { - tradeMap.clear(); - needSyncTrades = true; - lastFromTime = fromTime; - } + if (!lastId.hasValue()) { - if (needSyncTrades) { - syncTrades(fromTime); - needSyncTrades = false; - } + return TradesSync{ {}, Value(json::array,{time(nullptr), nullptr})}; - auto trs = tradeMap[pair]; + } else { + time_t startTime = lastId[0].getUInt(); + Value id = lastId[1]; - auto iter = trs.begin(); - auto end = trs.end(); - if (lastId.defined()) { - iter = std::find_if(iter, end, [&](const Trade &x){ - return x.id == lastId; - }); - if (iter != end) ++iter; - } + Value trs = px.private_request("returnTradeHistory", Object + ("start", startTime) + ("currencyPair",pair) + ("limit", 10000)); + + TradeHistory loaded; + for (Value t: trs) { + auto time = parseTime(String(t["date"])); + auto id = t["tradeID"]; + auto size = t["amount"].getNumber(); + auto price = t["rate"].getNumber(); + auto fee = t["fee"].getNumber(); + if (t["type"].getString() == "sell") size = -size; + double eff_size = size >= 0? size*(1-fee):size; + double eff_price = size < 0? price*(1-fee):price; + loaded.push_back(Trade { + id, + time, + size, + price, + eff_size, + eff_price, + }); + } + + std::sort(loaded.begin(),loaded.end(), tradeOrder); + auto iter = std::find_if(loaded.begin(), loaded.end(), [&](auto &&x) { + return x.id == id; + }); + if (iter != loaded.end()) { + ++iter; + loaded.erase(loaded.begin(),iter); + } - return TradeHistory(iter, end); + if (!loaded.empty()) { + lastId = {loaded.back().time/1000, loaded.back().id}; + } + + return TradesSync{ loaded, lastId}; + + } } @@ -157,7 +190,7 @@ Interface::Orders Interface::getOpenOrders(const std::string_view & pair) { }, Orders()); } -static std::uintptr_t now() { +static std::uint64_t now() { return std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ).count(); @@ -275,7 +308,7 @@ inline Interface::MarketInfo Interface::getMarketInfo(const std::string_view &pa } inline double Interface::getFees(const std::string_view &pair) { - if (px.hasKey) { + if (px.hasKey()) { auto now = std::chrono::system_clock::now(); if (!feeInfo.defined() || feeInfoExpiration < now) { feeInfo = px.private_request("returnFeeInfo", Value()); @@ -359,6 +392,14 @@ inline bool Interface::syncTradeCheckTime(const std::vector &cont, return false; } +inline void Interface::onLoadApiKey(json::Value keyData) { + px.privKey = keyData["secret"].getString(); + px.pubKey = keyData["key"].getString(); +} + +inline void Interface::onInit() { + //empty +} bool Interface::tradeOrder(const Trade &a, const Trade &b) { std::size_t ta = a.time; @@ -369,35 +410,71 @@ bool Interface::tradeOrder(const Trade &a, const Trade &b) { } +Interface::BrokerInfo Interface::getBrokerInfo() { + return BrokerInfo{ + px.hasKey(), + "poloniex", + "Poloniex", + "https://www.poloniex.com/", + "1.0", + R"mit(Copyright (c) 2019 Ondřej Novák + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE.)mit", +"iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAABwlBMVEUAAAAqNTsqNjwrNjwsNz0t" +"Nz0uOD4vOT8wOkAxO0ExPEEyPEIzPUM0PkQ1PkQ0P0Q1P0U2P0U2QEY3QEY3QUc4QUc4Qkg5QkgA" +"UlwAU1w6Q0kBU10BVF0BVF47REoCVV47RUsCVV88RUsCVl47RksCVl8DVl88Rkw9RkwDV18DV2A9" +"R00+R00EWGAEWWFASU8FWmFASlAFW2JBSlAGW2IGW2NBS1EGXGNCS1FCTFIHXWMHXWRDTFIHXmRD" +"TVNDTlMIX2QIX2VETlQIYGUJYGUJYGZFT1VGT1UJYWYJYWdGUFZHUFYJYmcJYmgJY2gJY2lIUVcJ" +"ZGkJZGpIUlhJUlgJZWoJZWtKU1kJZmsJZmxKVFpLVFoJZ20KZ21MVVpMVVsKaG4KaG8KaW8KaXAK" +"anAKanEKa3EKa3IJbHMKbHIKbHMKbXMKbXQLbXQMbnUMb3YNb3YOb3YPcHcQcHcRcXgScngTcnkU" +"c3kUc3oVc3oWdHsXdHsXdXwYdXwZdnwZdn0adn0bdn4cd34deH8eeH8feYAgeYAheoEieoEie4Ij" +"e4Ije4MkfIMlfIMlfYQmfYQnfYQofYUofoUpfoUpf4Yqf4Zvf8klAAAAAXRSTlMAQObYZgAAApVJ" +"REFUeAHt0YOaHUEUReEd27bHsW3btm3bdvK8sfpO37Oqu6qi6f8F9vrOUaFQKBR+9R7I0U4Qu2AH" +"7RsB72xyssW2VYa3tjdi2ghkemN7LbR2nU22V0BkFZFfwUuBpUDope2FTAuX2MSePbfJsoDIpcD2" +"VIY5tply8sT2WGXNAHLzCMC+QY4eAv8DkAcg9r7uA/8HkHtAKSZPsymTuyBlHyibO4T3KQDcvmXz" +"PwC5CWLv68Z12zUlhA/QVZAlQLlcAe4FyucycS5QTpdsF5UwqSzldgG4FSi/8+eAEiakk4ezwCVA" +"XgFnABfIz2mCBfJ0CpyEAHk7AWQWyN9xcNQsUADHiH41NkFBHAFKgH3vAC4Y/ZP+cIFCOQT7h9MD" +"FM5hcDC1QAEdJCkFCip7gMI6QOoVKLD9RAkjFRwGxLaPqCjYq8j2EsW2h/z/BbuJYttF4geQBn+C" +"7YprO1Fc2+LvQwBQZJtJsf9/B2wiimsDUWTZ9uvqFNh6W2lAbeiCNaRk/zOFlGO/9s8doOobBbMa" +"JPdVFbpgNSndDx2wEpTuhy5YkT+gKsg+MfbjB8B+iILlQAlDq0pUytPiZbbFSqisT36WAdyv/J37" +"g1PJw2L/AK+CRQT2owfwvl/BfMD7fgXz5oLYAbMB7/sVTJ9l432/gqnTgRIGguwFU4FK9AfKaOIU" +"mxS3YCIQB3gVjBlnG68UQQNs46S4BaOAyugN5GhEzn31sHV3DgAqq5utq5wMBzJ0sXUWU00N7Fs6" +"2Tp2EKs21dTI1L6dTagCCLQFAkOG2YTa2FrLNGiATaxNK5tMA4CYWgIZ+vazyUkLYOwDuWkOVFYf" +"IEfNmjazNFUZvWx95KwJUKqetl7KoHEjk/4ihUKhUPgI3TlJWgiZwUMAAAAASUVORK5CYII=" + + }; +} + int main(int argc, char **argv) { using namespace json; if (argc < 2) { - std::cerr << "No config given, terminated" << std::endl; + std::cerr << "Argument needed" << std::endl; return 1; } - try { + Interface ifc(argv[1]); + ifc.dispatch(); - ondra_shared::IniConfig ini; - - ini.load(argv[1]); - - Config cfg = load(ini["api"]); - Proxy proxy(cfg); - - std::string dbpath = ini["order_db"].mandatory["path"].getPath(); - - Interface ifc(proxy, dbpath); - - - ifc.dispatch(); - - - } catch (std::exception &e) { - std::cerr << "Error: " << e.what() << std::endl; - return 2; - } } diff --git a/src/poloniex/proxy.cpp b/src/poloniex/proxy.cpp index 5b2fde1c..b5acd754 100644 --- a/src/poloniex/proxy.cpp +++ b/src/poloniex/proxy.cpp @@ -23,12 +23,18 @@ using ondra_shared::logDebug; static constexpr std::uint64_t start_time = 1557858896532; -Proxy::Proxy(Config config):config(config) { +Proxy::Proxy() { + + apiPrivUrl="https://poloniex.com/tradingApi"; + apiPublicUrl="https://poloniex.com/public"; + auto now = std::chrono::system_clock::now(); - std::size_t init_time = std::chrono::duration_cast(now.time_since_epoch()).count() - start_time; + auto init_time = std::chrono::duration_cast(now.time_since_epoch()).count() - start_time; nonce = init_time * 100; - hasKey = !config.privKey.empty() && !config.pubKey.empty(); +} +bool Proxy::hasKey() const { + return !privKey.empty() && !pubKey.empty(); } @@ -52,7 +58,7 @@ void Proxy::buildParams(const json::Value& params, std::ostream& data) { json::Value Proxy::public_request(std::string method, json::Value data) { std::ostringstream urlbuilder; - urlbuilder << config.apiPublicUrl << "?command=" << method; + urlbuilder << apiPublicUrl << "?command=" << method; buildParams(data, urlbuilder); std::ostringstream response; @@ -92,7 +98,7 @@ static std::string signData(std::string_view key, std::string_view data) { } json::Value Proxy::private_request(std::string method, json::Value data) { - if (!hasKey) + if (!hasKey()) throw std::runtime_error("Function requires valid API keys"); std::ostringstream databld; @@ -116,12 +122,12 @@ json::Value Proxy::private_request(std::string method, json::Value data) { curl_handle.setOpt(new cURLpp::Options::PostFieldSize(request.length())); std::list headers; - headers.push_back("Key: "+config.pubKey); - headers.push_back("Sign: "+signData(config.privKey, request));; + headers.push_back("Key: "+pubKey); + headers.push_back("Sign: "+signData(privKey, request));; curl_handle.setOpt(new cURLpp::Options::HttpHeader(headers)); - curl_handle.setOpt(new cURLpp::Options::Url(config.apiPrivUrl)); + curl_handle.setOpt(new cURLpp::Options::Url(apiPrivUrl)); curl_handle.setOpt(new cURLpp::Options::WriteStream(&response)); curl_handle.perform(); diff --git a/src/poloniex/proxy.h b/src/poloniex/proxy.h index 00fbc85f..f70a6510 100644 --- a/src/poloniex/proxy.h +++ b/src/poloniex/proxy.h @@ -10,14 +10,16 @@ #include #include -#include "config.h" class Proxy { public: - Proxy(Config config); + Proxy(); - Config config; + std::string apiPrivUrl; + std::string apiPublicUrl; + std::string privKey; + std::string pubKey; cURLpp::Easy curl_handle; std::uint64_t nonce; @@ -28,7 +30,7 @@ class Proxy { json::Value public_request(std::string method, json::Value data); json::Value private_request(std::string method, json::Value data); - bool hasKey; + bool hasKey() const; bool debug = false; private: diff --git a/src/server b/src/server index 1d31cf13..01140821 160000 --- a/src/server +++ b/src/server @@ -1 +1 @@ -Subproject commit 1d31cf13b5adec8c12b5c2a1bb73710c9e315d19 +Subproject commit 011408214894b6366582884ab8005409e177ea79 diff --git a/src/shared b/src/shared index 1bf824d8..86deba41 160000 --- a/src/shared +++ b/src/shared @@ -1 +1 @@ -Subproject commit 1bf824d8f5a7a2196ee380e9daa4e65435094008 +Subproject commit 86deba41326bec3290c49aefb4fde4db05e6771e diff --git a/src/trainer/CMakeLists.txt b/src/trainer/CMakeLists.txt new file mode 100644 index 00000000..9ed658f4 --- /dev/null +++ b/src/trainer/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 2.8) +add_compile_options(-std=c++17) + + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin/brokers/) + +add_executable (trainer main.cpp ../brokers/api.cpp ) +target_link_libraries (trainer LINK_PUBLIC imtjson curlpp ssl crypto curl stdc++fs pthread) +install(TARGETS trainer DESTINATION "bin/trainer") diff --git a/src/trainer/main.cpp b/src/trainer/main.cpp new file mode 100644 index 00000000..d8ebfccd --- /dev/null +++ b/src/trainer/main.cpp @@ -0,0 +1,614 @@ +/* + * proxy.cpp + * + * Created on: 19. 10. 2019 + * Author: ondra + */ + +#include +#include +#include +#include +#include +#include +#include +#include "../brokers/api.h" +#include +#include +#include + +#include "../shared/stringview.h" + +using json::Object; +using json::Value; +using json::String; +using ondra_shared::StrViewA; + + +static Value setupForm = {}; + + +static Value settingsForm = { + Object + ("name","prices") + ("type","textarea") + ("label","Prices one per line\nor\nURL to JSON source,\nand name of the field at second line") + ("default",""), + Object + ("name","timeframe") + ("type","number") + ("label","Time frame in minutes") + ("default",1), + Object + ("name","asset") + ("type","string") + ("label","Asset symbol") + ("default","TEST"), + Object + ("name","asset_balance") + ("type","number") + ("label","Asset Balance") + ("default","0"), + Object + ("name","asset_step") + ("type","number") + ("label","Asset Step") + ("default","0"), + Object + ("name","currency") + ("type","string") + ("label","Currency symbol") + ("default","FIAT"), + Object + ("name","currency_balance") + ("type","number") + ("label","Currency Balance") + ("default","0"), + Object + ("name","currency_step") + ("type","number") + ("label","Currency Step") + ("default","0"), + Object + ("name","type") + ("type","enum") + ("options",Object + ("normal","Standard exchange") + ("inverted","Inverted futures")) + ("label","Market type") + ("default","normal"), + Object + ("name","restart") + ("type","enum") + ("options",Object + ("cont","Continue in chart") + ("restart","Restart from beginning")) + ("label","Restart or continue in chart") + ("default","cont"), +}; + + +static std::size_t genIDCnt() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); +} + +class Interface: public AbstractBrokerAPI { +public: + + Interface(const std::string &path):AbstractBrokerAPI(path, setupForm),fname(path+".jconf"),idcnt(genIDCnt()) {} + + virtual BrokerInfo getBrokerInfo() override; + virtual void onLoadApiKey(json::Value) override {} + + virtual double getBalance(const std::string_view & symb) override; + virtual TradesSync syncTrades(json::Value lastId, const std::string_view & pair) override; + virtual Orders getOpenOrders(const std::string_view & par)override; + virtual Ticker getTicker(const std::string_view & piar)override; + virtual json::Value placeOrder(const std::string_view & pair, + double size, + double price, + json::Value clientId, + json::Value replaceId, + double replaceSize)override; + virtual bool reset()override; + virtual MarketInfo getMarketInfo(const std::string_view & pair)override; + virtual double getFees(const std::string_view &pair)override; + virtual std::vector getAllPairs()override; + virtual void enable_debug(bool enable) override; + virtual void onInit() override; + virtual void setSettings(json::Value v) override; + virtual json::Value getSettings(const std::string_view &) const override ; + + json::Value collectSettings() const; + void saveSettings(); + void loadSettings(); + + void unsuppError() { + throw std::runtime_error("Unsupported operation - The trainer must be run with 'dry_run' flag enabled"); + } + + bool inited; + time_t startTime = 0; + std::vector prices = {100,101,99}; + long timeDivisor = 120; + std::string asset = "TEST"; + double asset_balance = 0; + double asset_step = 0; + std::string currency = "FIAT"; + double currency_balance = 0; + double currency_step = 0; + bool inverted = false; + double prev_price = 0; + + + Orders orders; + TradeHistory trades; + + std::string fname; + std::size_t idcnt; + + mutable double last_price = 0; + double getCurPrice() const; + std::string price_url; + std::string price_path; + +}; + + +int main(int argc, char **argv) { + using namespace json; + + if (argc < 2) { + std::cerr << "Required storage path" << std::endl; + return 1; + } + + Interface ifc(argv[1]); + ifc.dispatch(); +} + +inline Interface::BrokerInfo Interface::getBrokerInfo() { + return BrokerInfo { + true, + "Trainer", + "Trainer", + "", + "1.0", + + "Trainer(c) 2019 Ondřej Novák\n\n" + "Permission is hereby granted, free of charge, to any person " + "obtaining a copy of this software and associated documentation " + "files (the \"Software\"), to deal in the Software without " + "restriction, including without limitation the rights to use, " + "copy, modify, merge, publish, distribute, sublicense, and/or sell " + "copies of the Software, and to permit persons to whom the " + "Software is furnished to do so, subject to the following " + "conditions: " + "\n\n" + "The above copyright notice and this permission notice shall be " + "included in all copies or substantial portions of the Software. " + "\n\n" + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, " + "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES " + "OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND " + "NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT " + "HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, " + "WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING " + "FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR " + "OTHER DEALINGS IN THE SOFTWARE.", + + "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAQAAABpN6lAAAAdqElEQVR42u2deZhmVX3nP+fc5V1r" + "r7equ2mWpqHpVtYGWQYcWZygiRBQghrCqFGfDDpxe0ziPmo0mmQg8xB0JnFJVAiQwTzooOIMYIYI" + "ZmAA2RttZG3o7qrq2t793nvO/HHOXd633qqu7q7C5hnu+zTd9XLvPb/zO7/99z2n4JXrleuV65Xr" + "/+NLHPCz+iCgX7/UDFj4lD4Ilk6/VAxY7Bn9a5dbvfoMWPp+fRCo7T7SIA8ai/JrGkMedBN8iRkk" + "VvxefRBMT6+GBLwcVn8/aBX7dd/SLki/JBPbmxvUK8eA3sOKHoPpVWWBWOTvxcfWK8GA7imLzJ90" + "mOyf1WLBwvF705AdXR8YA7rXXiQf7H/jYdPPQiL2z4boRSffSQV2fHrQsAwmiGWvfTyoRCCRGQZo" + "NAqFRmVIWA0JSGmIKUhpUAkVC5mwBD3uPkzfDOsgcXASFmgUEYqIiAiFWiUVSKceU2BoIFmAlAIF" + "aERChVicHneZ05cIHBxcXFw83GR4M3BASECYYYJe8embBUgpMDTESxBaCkIi+92yWOAuS/PjoT18" + "fHL4+Hg4CCAiJKBNixZtAgQhrKgcpDQ4uPgZGlwkAk1EQECLNi0CAgQRLGBBT3rcZRk+M7RHjoL9" + "5O3wZvAWDRrUadJEgGWBzgwq9okdnc+l0/fwySc05PBxECgiWpaGBo3MDDpZ0FMO3GVNX+Lik6NI" + "iT7KlCmQx8NBE9KiQY0qPtXELMUs6BU56H2OOISdfo4CRcqUKVOiSA4XgSKgSYMqVTyrmFgrQMcy" + "9GCBu+Twnatfoo9+BhmgnzKFjbnDvKp6tF1vUmOeWbseqTtabNXFfmm/i0+eEn0MMsAgfZT8/LH+" + "oLMjfMIswRwzXQwgsUaLqoK7l4AnnX6RPgYZZpjhQ4a+uPGszWNr/HwU1ea2P/XfHv72BHlchLXH" + "sSVWBxQWiQXrX6SPIYYZYfii0Q8du/nIvkHHC5qTE/ds+8wvn5i2EkGHM9aJT+gpB2KRQDP2uA6O" + "Ff5+Bhmhwtg7133+vPVHisw6Nms/vvPyB+d3s5tJppilSpM2oVUCfcDW38UjT5kBhhllzKt849Vv" + "Pqc0kIl19O7n/+K2q55lkt1MMc08NZrWL3Wyoys2cHpMPw03YqtbpI9+Rhhl/J2H/tXF4+tFhxi7" + "/qaNp4fX7tFR4ogiO/nOyG1/Psb8GfE3DBi/aeslb8wVO/gkygNnbQx23R0kcYlaEJCJXurn9Jh+" + "LPoOnhW8Ev0MMcrYoePfOX98nTWxuq7AEYaAww6t7PzhPG3atAmIiKzupR58Xz8iY/1TFax85sj3" + "XOD6hoZANZUjpADwcyeO3/7ciyphv+6auu5ggVjIgO6IL3Y6ZQYYYpQK41dtPnurWf3Ha1/a/fmZ" + "78+67Y15T4KUh/Vfv32+SZO2FT0SVhpZcvbhE0d83QwY9Stfe/3oGoC2+sHkJ6aunv15dZ1ckxNA" + "qbymecOctUC6y5Hq3tmk2zPodazw5yhQoo8BRhhlTFbOOsZM/7Hqb008HRLBre0vNP9kvSth7JDL" + "K38+TZE8Ph6e9cFx/iAWiKBeNLXWSXSvExtgqCleMnToEUb+vvni+2pawX3yvwc/1Kf1A5x8dP75" + "ppG+WIobSNoE0DtMd3sYPydZfSP8RvMqjI1X1oyawb8283TLvtb9T8Gb57cMgHTOPIRnydsoLQCi" + "jASITPKydBZoPpHVZY3EswzIUzhv3MsB7Kh/fEYbE+fuyV05c23ZlzA6vHXsbo3OBM1x1hIQJiZZ" + "JNKh3R4xXxxylCgzwCBDjFChwthYJZcDaKrvt2hRpw14Ue7Z2pYBgKE+O3kfnxAJVoBd3I4Mcu8M" + "MLG9MagiEwDnxvrNbTtqMy2aBICHvtWdi0Yl+O7YONjJGzlMsxbskmQTJeEuCDji6Zfpt5MfYZQK" + "FUbLw0ICRHo+okmNFuCjwiD2BimhaHxLeg7fErJ3GchOv2UNqsIhT868JzZ/UUiDBi00OeR8PrIT" + "yo8iEokxI2dDo2xwlAmERMbvm4irTD/DjFBh9JDKpeuPWaMHtpUfLmU4aWJvCJEitO+XycAFJODa" + "7CFPDs9KwVKRoLaaavKLJg0aNAkhYaOnreMWES3qNAGFb8UbEENX+K/u84efHPhuYXsuSZjoUSfQ" + "qRHs9Psm4BxilLHy+F9v+c2TR0ekBM0kHgkDApo0AY0vVBcDCihySHKUKFOiZFmQFcbFWGBS2xZN" + "atSoUqNJhBuzUds6tlC0adFEAwXrcxD8F3/Ul0OgXvXHM7c/8IGHd8okPk0ZkIlQ3Y7pezbqMzFX" + "ZXTN98887WQZD0klJTROgkESJAxwrOMsIdF4xoOcNPjv1x1VCd0HW98KnpKI85y3FA/J1aI7a98K" + "Grhc5r6+NODuan+vdkuEXqve6b4m70fPTl23464ZZpmjShsR53+xBKAIadNG4xCiYhs+Ftf7xfDQ" + "JeccU3nLT7brTKHEhGdRzDC0mwl53cz0hxil8vcnnX6KED3jU2ULEOAmMR/ascYzIoegQJ8z9PWj" + "LzptYFgI+G393vrXZo/zzh8seQCXqvdXr6m+p3hCvycBLgvvnLm99f7+Q8tSAPr35v7nve94vDbN" + "HHUUPiUK5JW1WsJMK0RbU9kj4Bbi+GP/rvFvf6pjc6oyd1mn6HY4vpwNegcZYeSiNefY6bfZRotN" + "DKSCGpefyEZcyqVAiTaSEI+SGLxxy0VnOLHWinWlT5VSHXDksf3X9KdWuOCeP3pemp2JvoGLzx0Y" + "eOODYZEqAQ4lyhSU16UuMS3JVWMbks0UAMSpJ17x9FcDgiQziSXelm1kh+MrW68/wiiVtx9dLAHU" + "9CeiEzmVy3gmHSXsEXCiPOs7RhhjnLV/cMSbTo2nHweeYslyROfPUp699ZNHsZZxKowwSD9lZb2A" + "TiuSykoCALt4L6ewlQ+oaQ3g+xcfwyijjDJs3kAhY5KFm3F8sd8fpcIYlSPXmlfeFl2p0MgfyOP4" + "ouml6czkBTI2TFGOfjQeLSBP3+9uyvlGgh7A4YTEhE7zEOs4KmHG0zzNqxLtjXiEGidRAFzn4s2f" + "U5RooshRpj/KJ3ooE2GOiIQ2hP0D14NCfV2dIX7fAzh0DWNWWkUmzzDLqN3M6sf5foUxxhkr9pn3" + "b4sIUQjcu93AzZnRwyRKyzBA5RnCo0iAJO/0Hz5qvv8O70VwHW8zyTMf5Dus4TZeDcAzvIEneCM3" + "YGKc27iQNl/iTxDA+uE1a3eWaKBwKdIXFRKPYwIrEzRbCdDca/RdIx50tSeAQokKOlPUdbIFfcOA" + "vM33RqgwyhgVxhhVltdK0SJE44Qe/QhrgGKDYlTIrF2BEYo0iXDIy3I+b9j8IzSaO7gUCUzyj8BO" + "HrEMeIIngB+xg35Aczdt4Pt8kAKQ84rjFGnYcKgYFRMGOFYGYltgqK3TJgK8yC6L9hlLctu0kGo7" + "CSZkLFBmkFHGGWeMMSqMMKRyibEJzKSET5/lXpTJ9p1EAnwGKROgTQAci3gTowi649EwEflYkm2K" + "C0ArHkAwSJ4WERKfXOwFbI4Rr6VKUpqGXQCNn1imsUwl2bF15MjYsbjW28dQrnLja044kbLKRTnl" + "KfcQkYQcAS1CpEiGSTyqQKYq8BoelcI3A2uwIn1gV45bJEUKQoOWMEiXCqR9IfN10wZOMubouHis" + "LPLOsDzcaYnaLx69/Ke7wqRuEcYS0Mfgl4+64CLp9ozOQgICpBAZB5Qpd8QMqGRDpQPEL+jEL2wh" + "04nMBiPZGF8lXwe0UCg8YV9RZAu4uBQBDjviqtnLGtSp06BFIC0DSvRvPlz27hNl+y46CUNIvIBY" + "iQnva+lQd7ZGE3cswqRHpXoWGeWRhzNAmQI+Lk7qBUqxfe3sSYAwVjZEimgBvV11N70KoJLe7xSL" + "tT9NWCSzDOicj8pTSmMBN+35xEbvPm4C4F1sSiWgu9+nOxJYe93N/+ho1n+EcUCwxiqI0ZQCfezJ" + "WIiyFfWiJXMEgDXWtczzFygtFAqpHc3Z/EZWDRd0o0XcqlVxhrKDawB4A68zHPJtV8lFIt2k8u/F" + "9nWWLwPw1vSV5iO6uu7xJxlq2j4JHoMILmMccHgfT+HzDrsGo1zNn3EOZ9p7T+Lj/BMf4DD785v5" + "GTv4GGY9JrlK1Ru0UTjkyG2RHeP3XhBlF836ky9bE51J2ZJCiZu2HbVcIkVdqu+/4NuL+VPWIYiD" + "tpP5IYJc4r9+l4vxk7C3zJ/yKfJJYLeBfyAi1sf7Vb1GlTYalxAt7I1CZxLcnkU1oRaZjkzKrpYB" + "MehhrwBUsXgFr+P6kA1x0ivfpcfFrtp8589+8q9dXNWy9ho8BK7wkvBHZ+2Q6EXZYs2WDLxiJZCi" + "C8auLPNBxRM27DFXnSc7qN7Fp/XdcSK7SugTN9Xkvb9aLw5b6cmRdk9PJPDtHS0+yHG8jY3kqPE4" + "X+cOPsl5rEUyzX1cyZ3YMp1GGFSCdjpGXtzZiCUkOlMfcm0kHREtpjMZgRG9B9KLDPVFbutR8vd4" + "A+9jAChwKe/mSkap8CLTAFyBzzhFdlA1b8+hkIQIfPLkM828jm6DXrAoi9k0ke0daTfJp9syjPX1" + "7eiMJupsq6oXakzGilSwnsNNRPruniTcyVH8DgBHJ77m+K57TooH84Qj8iLSAke7WpTJ1APEomgC" + "mVLl8LYOuyPSDqYyDAhNeVG2zQ1ncHrnO7uz6M5GY4aQczh7mQFQTMxZiTNc4pIZq9hJlVgomVrG" + "/YhYAg7nuo4nZWCCYMMC1xY3G9ScRm/lsW5DIbXTG7mnl1I7TeSLC53DMm2ZEzh3r4q6jFC4twSY" + "LpSbSkC3pXea1BIWKJfQTJ+5p3doJWTPFXBs+TxppGmx7Eg3Eup694JFoSgrECN3SIF28YhwM02/" + "rqde2MEcVRq0CYliBswz8+kntty65URRUJ7ytKuc4VhQTX9HIrXflYnZwkIsArPMAzBCJq1Qm/V5" + "wutBScguUCISSiiROjihpZZaakdLyVqLudqZPFViKM4FdIYFycJpnwiVDexaTCAiGcpQBjSfeuzj" + "DzJDlQatmAEtGsyTm/TO/hd+wThjjDLC0M+LJwirAq6JnLWvOz2DTpwKAD/lTQA8zuZ4pRVRSfo9" + "1mKOb6iPNtQs81YgTUNd2lZ4mX4G/l3+FnxghtPZYZ/7NpdnMxSdmL04R8qhUUjc+LsX9JENZphi" + "kt3sYhe7MaX2FgGRS4BDAydB/RnMX5vAWWsDOOO4NY720pp2WotJc4GelYRIJCv+lOVYjW18O/rx" + "LFNMM8M8dZoEKLBYkDKDNAjlcCpKcfItREeGEid6MnbFqoBjSmIxA2TABJNMMMFuJpiwEBqjAsol" + "op24OGVNYpuQSA4ZBgiJj4vAlVavtOiAyi4aQQhlagjCghMmuYMbedhMp8oUE0ywh1lqNAiIAMfi" + "kRoEaClFHgECEdAmBJy44iRUpiwn03q7KNpOoYztmWizi93sYjeTTDLNjB3R2oAIQaujL2dbCM31" + "pv50pMRH4CC2JrqsU5iqXlwCRECbQEtdQoDLGZzO+7mfv+XGNlXmmWWaKfYwT50WkW2nlmgQIfCc" + "POMIEErM0yRE4DqlDANUAsCwSyM4zrvBM0XSLVZYmnUm2GkZME2sdAbIo0wckKaRUVJBd57duXU9" + "wPnuO9S3HDiVK0jahA5pWToJosVCBrRo/4oXyhsL8R2DnMvpnB99uDXbokGdeeaYoWq7wC55mrYC" + "XJJ9sSQlDPCktyDZkTg42jLgcn7CbRL529FbLLEv7sZ8JtjDjG232umbQAjCDtC7jb9v2v7GY3N5" + "GBBfcd5Nk+MZz+YQDtmyZBxjKUAIK5AioE1zT/TR5z48fGz/UGILi7yjuKn/d158USVA2xp122ls" + "Az4lGjRFK86aRI0GIRJfljptQoIhti8/lBt5AMlWxzTywvAHv2CSSSaZSuxNDONSaLezWZy81MW9" + "zn/PA2efjoASr+2O/2MwEwiSWqkIaaGRUcG4/VJkQNQ3q5vnjvJ/r+/1A6/qG/LMov2b0evnLtg1" + "n4Kd27TQuAhatA06RITGwQol6pYBKsYiJLqvE0rsNcx5GVIfeuTKZ5lkKhH+pn27TYhkRvRNV75O" + "1ermxHvufeRRrXsWAWLgvIubWmAR0qRBfd7muBsFEW2aVJnePvHZ58/afva2a5+vBYb612746zVJ" + "q4KkipPBFw4K05oPw5kGdRo0aSUMiPEMFgnUOyF7cvt/uFtNWN2vJqsfpjgy2QVLCSwLZtjDxJM7" + "z7/jljvn5mIeVNP0NgXPe6n4yZAGderP1c3PpxQGIiIbaE0zwe6Hdl7+zMe2zTRNB/8tx7+tzyI/" + "nKQoE7/Zw31dn0EAVpszdYsWacmUAW6CR3JTCZhPTEOtdvvPLrj13het7i8QfiPxbkfHPLKTi2sm" + "+gV1YXDqs5cevmGN6nuqeFfxRi8XxwE+eTSQw09QGwF1QvhRePGwI+DI4heL/7FqEeUtFA4+hWvq" + "A8Gnj8+5UC5/ZPMNuyyuzCNCJWiwPPk1/tkWkvnMJHUaKDycuDKtDYRXoy1s3k7hg+HG+tF1r/r8" + "ru8+8793JX5/LnV8neBZtyOuNva/lUTWiojgntY9DaaoMHLmMGP2/zj4FBAIcuTiJEmG1AjQ/9j4" + "oz2bR0CKd63fXf18DWGNncalSfvPgteWzj8G4PjNl913Xc7CmRQanyIlA8i/+hDTXI2iHzxnGeDj" + "yBSPlCdCAjlyaRzQmP7UHiaZZMIavmmmmUvQy1F3JdFNkPUxfirKNBwimypbvFYjYhQHpCi45Inw" + "AI+8Y12TalKliaq639z+hQHfhaL3x8fI1mcbdmdBALQIdPTp7aetG+yDXOFdG657wa67BvKUKNNH" + "+ROVC4811vVXz18zwTxNFHmcyHoG6ZIHfDQe+bwbd0KDSev0TNCT+v0kAe6ESrmZzQXaTtr0J2O7" + "EMRdNNRO3Toq50BevsH/25ztb7rCP8R6+dl5ZmkQ4fzn+ikPX3KClFDy/+g4GXymRRNhGRqh79X/" + "55fnbwU4bmPpZzWj9aZq0Uc//ecNfPgUgy1oNP/qwcY88xaR5kyZfIs1xUK+4djYwT/XM1CTINy1" + "Owl69jDDnDV9rQQj0oUUczqws507AdOdWDbmqot3VYb7QYp14p/adYsl/2jxrRVHgFLXPfTTSatt" + "wa3V0/WGMSHAc19TKU7fUU+DbBTab/7WJulALr/tnofnaREhyFFmgJGh4RtPP2K9ac3fdM/Hn7U6" + "3EIhh8WbNksH+j2/dRvGEOf9rwwdVQCYmP70I61dGQbM25py6vjoBMo59NqKqrsaDHb/j5ZnuMce" + "LgSszb3OmYuec452P1X8w/GSCzAx8YcPzO5hlioNgra6ee5UfUTMgvHS1O1V25MNiVBPtv/g0GI/" + "SHf+oZt30SLCIUeZQYavP/a1Jxrxv/+XlzzS3sM0s9RooxDbxDvXDgyCFFvLG9XzzDq/4X9l8HUD" + "5v67H/7GdqsARvjrGd3v2Udwk90UnTvtooQBaRvcwf3ctnM2rVkHgtP6ry3PRb4oWxSAUrf832dN" + "ntVGkqc9p9782Hflua+WAgr5D52p65+sJWoQzLWmnh9dD7Cpz0IaLMbss2t+81QznZ1TH3i4Os8s" + "s8zSQtBGNL0b7/nwIa4HRef3x98aNnXZyVkDMDP1pYcS0zdrc/6gw/QtAEo6PfeLpL3fzlq8mBJq" + "z1kb/ByAI4pOTtoH9D33vv3nwZTdL9IgQCFazvcap4gNo0KA523q/5tfths0aRqj+u7R8U1o9Nxj" + "X33Mdn7y9HmDf3fOcAWg0frcPTdNMMWeJH8P0cg7gjOjDYcbFvmy6MQ4jGb96h998zkmmEjCnpZd" + "fZ3Zu6KX3jHSa092GiID/xoEL5yU4IcspqP9L3dd+q97TMQ1R40mIQqJ0/K/1z5ZbhwSAqS85fEX" + "anFER1M+98Jd99/+4I9/8uhdcwRoPAr0jQ9+5JR8EbS+4dGPvcA0exJdbpsEWHPzzPH1I9Y5HWWm" + "mcmrb/nkr6zuZ6ffKfqLbpvTCzabZZtPItNO0n8Z3HHj5161dfPwmOcr1Zh/5plrf/6XL6rpZKVC" + "ZLKipfnSF3acfUTOTZIkTWAk5L/OssN6GmVA17YJIgwk/449NKhTpUqNGk2gZVRzPrrwniueee+J" + "GzaU+qUTBjOTDz/xhUf+eQ9TTLHH6n529ZfYVe5mvspuNxOZ0ChbdtSEtO6rvWm68MBp+XX+nHqy" + "9XjDbpubtQFH3MjIUaVGQ7QSJKnM7PJLnZKydBRsOVtkkumG3ZLZoInCta3vQDe/Wv3qzqMKm3JD" + "zs7g/uZ0nSpzzDKTmX6UEf5F95O7HV+Lrn+b4EgkeCYDlWhSpdjI/7Nvd47GGyerNtxUuGg86tSp" + "0xTtDvVStgrZSrCbJq50bRya0CDjICz+REi7h8BkF4Xtue3pxkkjK/NWWjp1f4nt9O4CjV8oB6qj" + "qxbQpEae3IKtsw0bbWOrTMbctUQg6Ni0GD/RtuVUiQfkuneaCoNMMo7TgF0NQtUwILt1NqRt8lC7" + "hyAud6i9nyXg9sh1O+VAL2BAi3qSwWlbR4zD5dBqnEMQf0S4AL4Qr21k9V7hLMD0pLtG4r+DBN5m" + "FiHG/KlMyN5Ktmwta/q9d493ykHKAp0M1bSbYITN40O7RkbrpPX2UdKB6/Qp8ZaIwCJQHURHJSCL" + "TImSMl2UOaUgoIVnYQ4xXUGyfT7MxC97PUnCXbSg3c0CnZROHNpJQYy0u5zgNTUyE0OqLmhVtgBj" + "GMAiGx11ZgNcivZXVgZMIV8mWUs2bFfLP0PCXaLxJLoSJZWsXvYAi+7jK2L8bmckuXBLXMwi2XFX" + "bxRSFsOgkJaCbHdKZxi0QkdodDtGksmprp5sN1arm+ze4BWdWdue9wl6QGHijdBqEQr0gohvLwBE" + "d6/tx0450B0bIMUCxGCvLfO99FBnwu3FCdU9sEgpDd3js6+T3zsDegdIumfu0Bk/6CWZ2gmy3Nvd" + "vaBPqZNe6hCnZeCJ9s6AbsfYucr7e1qQ7vHuvROsu1RyaXYtC07lLptcQW/k6VLwtNU7U2jvkrLM" + "Sx7AEHpfEc9i/7DRe5eL/Z7+vuEE9xHOvej9ep/G0/v53Cow4EBZsrxp7uuIB0iDXLEJH/iKswzP" + "sTevseqHqup9tMsrL0F6Uae3X29094MAsd9DLsdNHYim69VWgeUOs1LOb98MoF59G3DgE1w9JdlP" + "ilb2YOVejNL7Lb56la3JATJA76cg7y1L0PvhIg9A4eSKaOjiaqLRYrGcrneO2OvuVYxG5KorQLbY" + "sTD31wvLJDHDhFqiprBil1zV6SsUkUjO9xDRIsWvTAFMRBnMWdR16sNBxwC9lOjb+m+YlMW1CHr0" + "aXVHpdDUfrHo0DA5+2PV4ky5iqtvyp5tGXeGtOhdsc+WSduiZVVAOwkwo6fdOEhVQHcxoE1TNuya" + "apltWugO/VcWK9gUjRh46zS6evsHpQ3QS2i/6SDUnWoMd3SqFhcedpSu47sDmjRkNbYZbi0Dau11" + "YvSKJFruCqy56Gn74wZY1ZuLJcCdS0ALUUf5WluYZoOqk5OGAdqfs03u9ACcFZ/+SjCg42iyjEU3" + "DbManpOL5oggmnXSvQFh5nC7GIzVoo4vnGg2HAAdODNdEBfdVZJbEaVY6V+bkJ5I5sWQt1L53H7h" + "gWr/r7lWlXmyihCf+xcf4dQny+f3ezkQwc/md5u7s91ezQqfWb3yDCBzLFeeAkWK5PDQhEkHN7uq" + "2SPc0rsFIS17zkMzUYOVP7J7RVRgoUXQtgkeQyo8HCBKOrjdrjA2meZu37ZLe9294pe7Cu+MMQXC" + "HmQZJHhwA7vsRGyaJ6LEdgQW+qxsx7nbZxz0DNBJzzhMDJy0DIhsuNO9ntkGfJg0ve1BN8kJtavy" + "eytWQwKy0GsDacj2kRdCFrN3q46tOGpxgOPBagSzplB0nSibPSs2K9CdvzhD7uXulwUDskc0p93k" + "LPSuc0K97mbRdvfLggHdvxNmYU1IH8DdK3g5q5hqiyVyB32Ad78MJGCpt+sVuPtlwYCl1/VA735Z" + "MGDhCHoF7z6oa4IrV194mTNAH8AEXwJ2vDS/RVDs04TEr0saDqZL8Mr1yvWSXP8P78XWqfvv6HgA" + "AAAASUVORK5CYII=", + true + }; +} + + +inline double Interface::getBalance(const std::string_view &x) { + if (inverted) { + if (x == "CONTRACT") return asset_balance; + else return currency_balance; + } else { + if (x == currency) return currency_balance; + else return asset_balance; + } +} + +inline Interface::TradesSync Interface::syncTrades(json::Value lastId, const std::string_view &pair) { + using namespace json; + if (lastId.hasValue()) { + TradeHistory ret; + std::copy_if(trades.begin(), trades.end(), std::back_inserter(ret), [&](const Trade &x) { + return Value::compare(x.id, lastId) > 0; + }); + return TradesSync{ret, trades.empty()? lastId: trades.back().id}; + } else { + return TradesSync { {}, trades.empty()? Value(nullptr): trades.back().id}; + } +} + +inline Interface::Orders Interface::getOpenOrders(const std::string_view &par) { + return orders; +} + +double Interface::getCurPrice() const { + if (last_price) return last_price; + double price = 0; + + if (!price_url.empty()) { + cURLpp::Easy curl_handle; + std::ostringstream response; + + if (this->debug_mode) { + std::cerr << "Send: " << price_url << std::endl; + } + + curl_handle.reset(); + curl_handle.setOpt(cURLpp::Options::Url(price_url)); + curl_handle.setOpt(cURLpp::Options::WriteStream(&response)); + curl_handle.perform(); + + if (debug_mode) { + std::cerr << "Recv: " << response.str() << std::endl; + } + + + json::Value resp =json::Value::fromString(response.str()); + bool found = false; + resp.walk([&](Value x) { + if (!found && ((price_path.empty() && x.type() == json::number) || + (!price_path.empty() && x.getKey() == StrViewA(price_path)))) { + price = x.getNumber(); + found = true; + } + return true; + }); + if (price == 0) throw std::runtime_error("Failed to download price"); + } else { + if (prices.empty()) return 100; + + time_t t = time(nullptr) - startTime; + std::size_t index = (t/timeDivisor) % prices.size(); + price = prices[index]; + } + if (inverted) price = 1/price; + last_price = price; + return price; + +} + +inline Interface::Ticker Interface::getTicker(const std::string_view &piar) { + double price = getCurPrice(); + return Ticker{price,price,price,uintptr_t(time(nullptr))*1000}; +} + +inline json::Value Interface::placeOrder(const std::string_view &, + double size, double price, json::Value clientId, json::Value replaceId, + double replaceSize) { + + double p = getCurPrice(); + auto iter = std::find_if(orders.begin(), orders.end(),[&](const Order &o) { + return o.id == replaceId; + }); + if (iter != orders.end()) { + if (replaceSize > fabs(iter->size)) return iter->id; + else orders.erase(iter); + } + + + + Value id = idcnt++; + if (size) { + + if (((p - price) / size) < 0) price = p; + + orders.push_back(Order{ + id,clientId, + size,price + }); + } + + return id; +} + +inline bool Interface::reset() { + + last_price = 0; + double p = getCurPrice(); + + Orders newOrders; + for (auto o : orders) { + double dp = p - o.price; + if (dp / o.size <= 0) { + double pprice = trades.empty()?p:trades.back().price; + Value id = ++idcnt; + double s = o.size * (dp == 0?0.5:1); + Trade tr { + id, + std::size_t(time(nullptr)*1000), + s, + o.price, + s, + o.price + }; + trades.push_back(tr); + if (inverted) { + currency_balance += asset_balance*(o.price - pprice); + } + asset_balance += s; + if (!inverted) { + currency_balance -= s * o.price; + } + double remain = (o.size - s); + if (std::fabs(remain) > (asset_step+1e-20)) { + newOrders.push_back(Order { + o.id, + o.client_id, + remain, + o.price + }); + } + } else { + newOrders.push_back(o); + } + } + + newOrders.swap(orders); + prev_price = p; + saveSettings(); + return true; + +} + +inline Interface::MarketInfo Interface::getMarketInfo(const std::string_view &pair) { + return MarketInfo{ + inverted?"CONTRACT":asset, + inverted?asset:currency, + asset_step, + currency_step, + asset_step, + 0, + 0, + AbstractBrokerAPI::currency, + 0, + inverted, + currency, + true + }; +} + + +inline double Interface::getFees(const std::string_view &pair) { + return 0; +} + +inline std::vector Interface::getAllPairs() { + return {"TRAINER_PAIR"}; +} + +inline void Interface::enable_debug(bool ) { + +} + + +inline void Interface::onInit() { + loadSettings(); +} + +inline void Interface::setSettings(json::Value keyData) { + timeDivisor = keyData["timeframe"].getInt()*60; + prices.clear(); + price_url.clear(); + price_path.clear(); + StrViewA textPrices = keyData["prices"].getString().trim(isspace); + if (textPrices.begins("https://") || textPrices.begins("http://")) { + auto splt=textPrices.split("\n"); + StrViewA url = splt(); + StrViewA path = splt(); + price_url = url; + price_path = path; + } else { + auto splt = textPrices.split("\n"); + while (!!splt) { + StrViewA line = splt(); + line = line.trim(isspace); + if (!line.empty()) { + double d = strtod(line.data,0); + if (std::isfinite(d) && d > 0) { + prices.push_back(d); + } + } + } + } + Value st = keyData["startTime"]; + if (st.hasValue()) startTime = st.getUInt(); + else { + if (keyData["restart"].getString() == "restart") { + startTime = time(nullptr); + } + } + + asset = keyData["asset"].getString(); + currency = keyData["currency"].getString(); + asset_balance = keyData["asset_balance"].getNumber(); + currency_balance = keyData["currency_balance"].getNumber(); + asset_step = keyData["asset_step"].getNumber(); + currency_step = keyData["currency_step"].getNumber(); + inverted = keyData["type"].getString() == "inverted"; + prev_price = keyData["prev_price"].getNumber(); + saveSettings(); +} + +json::Value Interface::collectSettings() const { + json::Object kv; + kv.set("prices",price_url.empty() + ?json::Value(json::array, prices.begin(), prices.end(), [](double v){return v;}).join("\n") + :json::Value(json::String({price_url,"\n", price_path}))) + ("asset",asset) + ("currency",currency) + ("asset_balance",asset_balance) + ("currency_balance",currency_balance) + ("asset_step",asset_step) + ("currency_step",currency_step) + ("type",inverted?"inverted":"normal") + ("startTime",startTime) + ("restart","cont") + ("timeframe",timeDivisor/60) + ("prev_price", prev_price); + return kv; +} + +inline json::Value Interface::getSettings(const std::string_view & ) const { + Value kv = collectSettings(); + return settingsForm.map([&](Value v) { + StrViewA n = v["name"].getString(); + return v.replace("default", kv[n]); + }); +} + +inline void Interface::saveSettings() { + std::ofstream f(fname, std::ios::out|std::ios::trunc); + collectSettings().toStream(f); +} + +inline void Interface::loadSettings() { + std::ifstream f(fname); + if (!f) return; + Value v = Value::fromStream(f); + if (!f) return; + setSettings(v); +} diff --git a/update b/update index 4fb2e8b8..e5940533 100755 --- a/update +++ b/update @@ -1,11 +1,10 @@ #!/bin/bash + git submodule foreach git reset --hard git submodule update --init git -c user.name="Update" -c user.email="update@localhost.localdomain" commit -a -m "" --allow-empty-message set -e git -c user.name="Update" -c user.email="update@localhost.localdomain" pull --no-commit git submodule update --init -mkdir -p ./data -mkdir -p ./log -cmake -DCMAKE_BUILD_TYPE=RELWITHDEBINFO . -make all -j `nproc` +./build + diff --git a/www/admin/code.js b/www/admin/code.js new file mode 100644 index 00000000..db916191 --- /dev/null +++ b/www/admin/code.js @@ -0,0 +1,1538 @@ +"use strict"; + +function app_start() { + TemplateJS.View.lightbox_class = "lightbox"; + window.app = new App(); + window.app.init(); + +} + +function fetch_error(e) { + if (!fetch_error.shown) { + fetch_error.shown = true; + var txt; + if (e.headers) { + var ct = e.headers.get("Content-Type"); + if (ct == "text/html" || ct == "application/xhtml+xml") { + txt = e.text().then(function(text) { + var parser = new DOMParser(); + var htmlDocument = parser.parseFromString(text, ct); + var el = htmlDocument.body.querySelector("p"); + if (!el) el = htmlDocument.body; + return el.innerText; + }); + } else { + txt = e.text() || ""; + } + } else { + txt = Promise.resolve(e.toString()); + } + txt.then(function(t) { + var msg = (e.status || "")+" "+(e.statusText || ""); + if (t == msg) t = ""; + app.dlgbox({text:msg, desc:t},"network_error").then(function() { + fetch_error.shown = false; + }); + }); + } + throw e; +} + +function fetch_with_error(url, opt) { + return fetch_json(url, opt).catch(fetch_error); +} + +function App() { + this.traders={}; + this.config={}; + this.curForm = null; +} + +TemplateJS.View.prototype.showWithAnim = function(item, state) { + var elem = this.findElements(item)[0]; + var h = elem.classList.contains("hidden") || elem.hidden; + if (state) { + if (h) { + elem.hidden = false; + TemplateJS.waitForDOMUpdate().then(function(x) { + elem.classList.remove("hidden"); + }); + } + } else { + if (!h) { + elem.classList.add("hidden"); + var anim = new TemplateJS.Animation(elem); + anim.wait().then(function(x) { + if (elem.classList.contains("hidden")) + elem.hidden = true; + }); + } + } +} + + +App.prototype.createTraderForm = function() { + var form = TemplateJS.View.fromTemplate("trader_form"); + var norm=form.findElements("goal_norm")[0]; + var pl = form.findElements("goal_pl")[0]; + form.dlgRules = function() { + var state = this.readData(["strategy","advanced","check_unsupp"]); + this.showWithAnim("strategy_halfhalf",state.strategy == "halfhalf" || state.strategy == "keepvalue"); + this.showWithAnim("strategy_pl",state.strategy == "plfrompos"); + form.setData({"help_goal":{"class":state.strategy}}); + form.getRoot().classList.toggle("no_adv", !state["advanced"]); + form.getRoot().classList.toggle("no_experimental", !state["check_unsupp"]); + + }; + form.setItemEvent("strategy","change", form.dlgRules.bind(form)); + form.setItemEvent("advanced","change", form.dlgRules.bind(form)); + form.setItemEvent("check_unsupp","change", form.dlgRules.bind(form)); + return form; +} + +App.prototype.createTraderList = function(form) { + if (!form) form = TemplateJS.View.fromTemplate("trader_list"); + var items = Object.keys(this.traders).map(function(x) { + return { + image:this.brokerImgURL(this.traders[x].broker), + caption: this.traders[x].title, + broker: this.traders[x].broker, + id: this.traders[x].id, + }; + },this); + items.sort(function(a,b) { + return a.broker.localeCompare(b.broker); + }); + items.unshift({ + "image":"../res/options.png", + "caption":this.strtable.options, + "id":"$" + + }) + items.unshift({ + "image":"../res/security.png", + "caption":this.strtable.access_control, + "id":"!" + + }) + items.push({ + "image":"../res/add_icon.png", + "caption":"", + "id":"+" + }); + items.forEach(function(x) { + x[""] = {"!click":function(id) { + form.select(id); + }.bind(null,x.id)} + }); + form.setData({"item": items}); + form.select = function(id) { + var update = items.map(function(x) { + return {"":{"classList":{"selected": x.id == id}}}; + }); + form.setData({"item": update}); + + var nf; + if (id == "!") { + if (this.curForm) { + this.curForm.save(); + this.curForm = null; + } + nf = this.securityForm(); + this.desktop.setItemValue("content", nf); + nf.save = function() {}; + + } else if (id == '$') { + if (this.curForm) { + this.curForm.save(); + this.curForm = null; + } + nf = this.optionsForm(); + this.desktop.setItemValue("content", nf); + this.curForm = nf; + + } else if (id == "+") { + this.brokerSelect().then(this.pairSelect.bind(this)).then(function(res) { + var broker = res[0]; + var pair = res[1]; + var name = res[2]; + if (!this.traders[name]) this.traders[name] = {}; + var t = this.traders[name]; + t.broker = broker; + t.pair_symbol = pair; + t.id = name; + t.enable = true; + t.dry_run = false; + if (!t.title) t.title = pair; + this.updateTopMenu(name); + }.bind(this)) + } else { + if (this.curForm) { + this.curForm.save(); + this.curForm = null; + } + + nf = this.openTraderForm(id); + + this.desktop.setItemValue("content", TemplateJS.View.fromTemplate("main_form_wait")); + + nf.then(function (nf) { + this.desktop.setItemValue("content", nf); + this.curForm = nf; + this.curForm.save = function() { + this.traders[id] = this.saveForm(nf, this.traders[id]); + this.updateTopMenu(); + }.bind(this); + this.curTrader = this.traders[id]; + }.bind(this)); + } + }.bind(this); + + return form; +} + +App.prototype.processConfig = function(x) { + this.config = x; + this.users = x["users"] || []; + this.traders = x["traders"] || {} + for (var id in this.traders) this.traders[id].id = id; + return x; +} + +App.prototype.loadConfig = function() { + return fetch_with_error("api/config").then(this.processConfig.bind(this)); +} + +App.prototype.brokerURL = function(broker) { + return "api/brokers/"+encodeURIComponent(broker); +} +App.prototype.pairURL = function(broker, pair) { + return this.brokerURL(broker) + "/pairs/" + encodeURIComponent(pair); +} +App.prototype.brokerImgURL = function(broker) { + return this.brokerURL(broker) + "/icon.png"; +} +App.prototype.traderURL = function(trader) { + return "api/traders/"+encodeURIComponent(trader); +} +App.prototype.traderPairURL = function(trader, pair) { + return "api/traders/"+encodeURIComponent(trader)+"/broker/pairs/" + encodeURIComponent(pair); +} + +function defval(v,w) { + if (v === undefined) return w; + else return v; +} + +function filledval(v,w) { + if (v === undefined) return {value:w}; + else if (v == w) return {value:v}; + else { + return { + "value":v, + "classList":{ + "changed":true + } + } + } +} + + + +function calc_range(a, ea, c, p, inv, leverage) { + a = a+ea; + var value = a * p; + var max_price = pow2((a * Math.sqrt(p))/ea); + var S = value - c + var min_price = S<=0?0:pow2(S/(a*Math.sqrt(p))); + if (leverage) { + var colateral = c* (1 - 1 / leverage); + min_price = (ea*p - 2*Math.sqrt(ea*colateral*p) + colateral)/ea; + max_price = (ea*p + 2*Math.sqrt(ea*colateral*p) + colateral)/ea; + + } + if (!isFinite(max_price)) max_price = "∞"; + if (inv) { + var k = 1/min_price; + min_price = 1/max_price; + max_price =k; + } + return { + range_min_price: adjNumN(min_price), + range_max_price: adjNumN(max_price) + } + + +} + +App.prototype.fillForm = function (src, trg) { + var data = {}; + data.id = src.id; + data.title = src.title; + data.symbol = src.pair_symbol; + if (!this.fillFormCache) this.fillFormCache = {} + + + var apikey = this.config.apikeys && this.config.apikeys[src.broker]; + + var updateHdr = function(){ + var state = fetch_json("api/editor",{ + method:"POST", + body: JSON.stringify({ + broker: src.broker, + trader: src.id, + pair: src.pair_symbol + }) + }); + state.then(function(st) { + var data = {}; + fillHeader(st,data); + trg.setData(data); + }); + }.bind(this); + + var fillHeader = function(state, data) { + + + var broker = state.broker; + var pair = state.pair; + var orders = state.orders; + this.fillFormCache[src.id] = state; + + data.broker = broker.exchangeName; + data.no_api_key = {".hidden": !!(apikey === undefined?broker.trading_enabled:apikey)}; + data.api_key_not_saved = {".hidden": apikey === undefined || !(!broker.trading_enabled && apikey)}; + data.broker_id = broker.name; + data.broker_ver = broker.version; + data.asset = pair.asset_symbol; + data.currency = pair.currency_symbol; + data.balance_asset= adjNum(invSize(pair.asset_balance,pair.invert_price)); + data.balance_currency = adjNum(pair.currency_balance); + data.price= adjNum(invPrice(pair.price,pair.invert_price)); + data.leverage=pair.leverage?pair.leverage+"x":"n/a"; + + var mp = orders?orders.map(function(z) { + return { + id: z.id, + dir: invSize(z.size,pair.invert_price)>0?"BUY":"SELL", + price:adjNum(invPrice(z.price, pair.invert_price)), + size: adjNum(Math.abs(z.size)) + };}):[]; + + if (mp.length) { + var butt = document.createElement("button"); + butt.innerText="X"; + mp[0].action = { + "rowspan":mp.length, + "value": butt + }; + butt.addEventListener("click", this.cancelAllOrders.bind(this, src.id)); + } + + data.orders = mp + + var fields = [] + + function fill_recursive(path, node) { + if (typeof node == "object") { + Object.keys(node).forEach(function(k) { + fill_recursive(path.concat(k), node[k]); + }); + } else { + fields.push({ + key: path.join("."), + value: node.toString() + }); + } + } + if (state.strategy) fill_recursive([],state.strategy[Object.keys(state.strategy)[0]]); + + data.strategy_fields = fields; + data.icon_broker = { + ".hidden":!broker.settings, + "!click":this.brokerConfig.bind(this, src.broker, src.pair_symbol) + } + data.open_orders_sect = {".hidden":!mp.length}; + + function linStrategy_recalc() { + var fdata = {}; + var inputs = trg.readData(["max_pos","cstep","neutral_pos"]); + var pos = pair.asset_balance - inputs.neutral_pos; + var k = inputs.cstep / (pair.price*pair.price * 0.01); + var mp = pos/k + pair.price; + var lp = mp-inputs.max_pos/k; + fdata.linear_max_in_pos = adjNum(0.5*k*(mp - lp)*(mp - lp)); + var minp = -inputs.max_pos/k+mp; + var maxp = +inputs.max_pos/k+mp; + if (pair.invert_price) { + fdata.linear_max_price = adjNum(1/minp); + fdata.linear_min_price = adjNum(1/maxp); + } else { + fdata.linear_min_price = adjNum(minp); + fdata.linear_max_price = adjNum(maxp); + } + fdata.err_toolargepos = {classList:{mark:pos > safe_pos}}; + fdata.err_invalid_values = {classList:{mark:inputs.cstep<=0 || inputs.max_pos<0}}; + + trg.setData(fdata); + var safe_pos = pair.currency_balance / pair.price; + trg.forEachElement("err_toolargepos", function(b,x) + {x.classList.toggle("mark",b);}.bind(null, pos > safe_pos) ); + } + function linStrategy_recomended() { + var inputs = trg.readData(["cstep","neutral_pos"]); + var value = pair.currency_balance; + var invest = value / 10; + var k = invest / (pair.price*pair.price * 0.01); + var max_pos = Math.sqrt(k * value); + trg.setData({ + cstep : adjNumN(invest), + max_pos: adjNumN(max_pos), + pl_redfact: 100, + pl_closepos: "prefer_close" + }); + + linStrategy_recalc(); + } + function linStrategy_recomended_maxpos() { + var inputs = trg.readData(["cstep","neutral_pos"]); + var value = pair.currency_balance; + var invest = inputs.cstep; + var k = invest / (pair.price*pair.price * 0.01); + var max_pos = Math.sqrt(k * value); + trg.setData({ + max_pos: adjNumN(max_pos) + }); + linStrategy_recalc(); + } + + function halfHalf_recalc() { + var inputs = trg.readData(["strategy","acum_factor","external_assets"]); + var p = pair.price; + var ea = inputs.external_assets; + var data = {}; + var a = pair.asset_balance+ea; + if (inputs.strategy == "halfhalf") { + var s = a * p - pair.currency_balance; + data.halfhalf_max_price = ea <= 0?"∞":adjNum(p*a*a/(ea*ea)); + data.halfhalf_min_price = adjNum(s <0?0:(s/a)*(s/a)/p); + } else { + var k = p*a; + data.halfhalf_min_price = adjNum(p*Math.exp(-pair.currency_balance/k)); + data.halfhalf_max_price = ea > 0?adjNum(k/ea):"∞"; + } + + data.err_external_assets = {classList:{mark:!pair.leverage && !pair.invert_price && a <= 0}}; + data.err_external_assets_margin = {classList:{mark:pair.leverage && !pair.invert_price && a <= 0}}; + data.err_external_assets_inverse = {classList:{mark:pair.invert_price && a <= 0}}; + data.external_assets_hint = -pair.asset_balance; + trg.setData(data); + + } + + data.max_pos = data.cstep = data.neutral_pos = {"!input": linStrategy_recalc}; + data.linear_suggest = {"!click":linStrategy_recomended}; + data.linear_suggest_maxpos = {"!click":linStrategy_recomended_maxpos}; + data.external_assets = {"!input": halfHalf_recalc}; + linStrategy_recalc(); + halfHalf_recalc(); + var tmp = trg.readData(["cstep","max_pos"]); + if (!tmp.max_pos && !tmp.cstep) linStrategy_recomended(); + + + }.bind(this); + + if (this.fillFormCache[src.id]) fillHeader(this.fillFormCache[src.id], data) + data.broker_img = this.brokerImgURL(src.broker); + data.advanced = src.advanced; + + + data.strategy = (src.strategy && src.strategy.type) || ""; + data.cstep = 0; + data.neutral_pos = 0; + data.pl_acum = 0; + data.acum_factor = 0; + data.external_assets = 0; + + if (data.strategy == "halfhalf" || data.strategy == "keepvalue") { + data.acum_factor = filledval(defval(src.strategy.accum,0)*100,0); + data.external_assets = filledval(src.strategy.ea,0); + } else if (data.strategy == "plfrompos") { + data.pl_acum = filledval(defval(src.strategy.accum,0)*100,0); + data.neutral_pos = filledval(src.strategy.neutral_pos,0); + data.cstep = filledval(src.strategy.cstep,0); + data.max_pos = filledval(src.strategy.maxpos,0); + data.pl_closepos = filledval(src.strategy.closepos,"prefer_close"); + data.pl_redfact = filledval(defval(src.strategy.reduce_factor,1)*100,100); + } + data.enabled = src.enable; + data.dry_run = src.dry_run; + data.accept_loss = filledval(src.accept_loss,1); + data.sliding_pos_hours = filledval(src["sliding_pos.hours"],240); + data.sliding_pos_fade = filledval(src["sliding_pos.fade"],0); + data.spread_calc_stdev_hours = filledval(src.spread_calc_stdev_hours,160); + data.spread_calc_sma_hours = filledval(src.spread_calc_sma_hours,3); + data.dynmult_raise = filledval(src.dynmult_raise,250); + data.dynmult_fall = filledval(src.dynmult_fall, 5); + data.dynmult_mode = filledval(src.dynmult_mode, "half_alternate"); + data.dynmult_scale = filledval(defval(src.dynmult_scale,true)?1:0,1); + data.spread_mult = filledval(Math.log(defval(src.buy_step_mult,1))/Math.log(2)*100,0); + data.order_mult = filledval(defval(src.buy_mult,1)*100,100); + data.min_size = filledval(src.min_size,0); + data.max_size = filledval(src.max_size,0); + data.internal_balance = filledval(src.internal_balance,0); + data.dust_orders = filledval(src.dust_orders,true); + data.detect_manual_trades = filledval(src.detect_manual_trades,false); + data.report_position_offset = filledval(src.report_position_offset,0); + data.force_spread = filledval(adjNum((Math.exp(defval(src.force_spread,0))-1)*100),"0.000"); + + + + + data.icon_repair={"!click": this.repairTrader.bind(this, src.id)}; + data.icon_reset={"!click": this.resetTrader.bind(this, src.id)}; + data.icon_delete={"!click": this.deleteTrader.bind(this, src.id)}; + data.icon_undo={"!click": this.undoTrader.bind(this, src.id)}; + data.icon_trading={"!click":this.tradingForm.bind(this, src.id)}; + + function refresh_hdr() { + if (trg.getRoot().isConnected) { + updateHdr(); + setTimeout(refresh_hdr,60000); + } + } + + function unhide_changed(x) { + + + + var root = trg.getRoot(); + var items = root.getElementsByClassName("changed"); + Array.prototype.forEach.call(items,function(x) { + while (x && x != root) { + x.classList.add("unhide"); + x = x.parentNode; + } + }); + + var inputs = trg.getRoot().querySelectorAll("input,select"); + Array.prototype.forEach.call(inputs, function(x) { + function markModified() { + var n = this; + while (n && n != trg.getRoot()) { + n.classList.add("modified"); + n = n.parentNode; + } + } + x.addEventListener("change", markModified ); + }); + + refresh_hdr(); + + var ambt = null; + trg.setItemEvent("auto_max_backtest_time","input",function() { + if (ambt) clearTimeout(ambt); + ambt=setTimeout(function() { + trg.setData({auto_max_backtest_result:{classList:{wait:true}}}); + trg.setData({auto_max_backtest_result: + fetchSizeBacktest.call(this,trg.readData(["auto_max_backtest_time"]).auto_max_backtest_time) + .then(function(x) { + return { + value:x,classList:{wait:false} + }})}); + }.bind(this),250); + + }.bind(this)); + + + return x; + } + + + return trg.setData(data).catch(function(){}).then(unhide_changed.bind(this)).then(trg.dlgRules.bind(trg)); +} + + +App.prototype.saveForm = function(form, src) { + + var data = form.readData(); + var trader = {} + var goal = data.goal; + trader.strategy = {}; + trader.strategy.type = data.strategy; + if (data.strategy == "plfrompos") { + trader.strategy.accum = data.pl_acum/100.0; + trader.strategy.cstep = data.cstep; + trader.strategy.neutral_pos = data.neutral_pos; + trader.strategy.maxpos = data.max_pos; + trader.strategy.closepos = data.pl_closepos; + trader.strategy.reduce_factor = data.pl_redfact/100; + } else if (data.strategy == "halfhalf" || data.strategy == "keepvalue") { + trader.strategy.accum = data.acum_factor/100.0; + trader.strategy.ea = data.external_assets; + } + trader.id = src.id; + trader.broker =src.broker; + trader.pair_symbol = src.pair_symbol; + trader.title = data.title; + trader.enable = data.enabled; + trader.dry_run = data.dry_run; + trader.advanced = data.advanced; + trader.accept_loss = data.accept_loss; + trader.spread_calc_stdev_hours =data.spread_calc_stdev_hours ; + trader.spread_calc_sma_hours = data.spread_calc_sma_hours; + trader.dynmult_raise = data.dynmult_raise; + trader.dynmult_fall = data.dynmult_fall; + trader.dynmult_mode = data.dynmult_mode; + trader.dynmult_scale = data.dynmult_scale == "1"; + trader.buy_mult = data.order_mult/100; + trader.sell_mult = data.order_mult/100; + trader.buy_step_mult = Math.pow(2,data.spread_mult*0.01) + trader.sell_step_mult = Math.pow(2,data.spread_mult*0.01) + trader.min_size = data.min_size; + trader.max_size = data.max_size; + trader.internal_balance = data.internal_balance; + trader.dust_orders = data.dust_orders; + trader.detect_manual_trades = data.detect_manual_trades; + trader.report_position_offset = data.report_position_offset; + trader.force_spread = Math.log(data.force_spread/100+1); + return trader; + +} + +App.prototype.openTraderForm = function(trader) { + var form = this.createTraderForm(); + var p = this.fillForm(this.traders[trader], form); + return Promise.resolve(form); +} + +TemplateJS.View.regCustomElement("X-SLIDER", new TemplateJS.CustomElement( + function(elem,val) { + var range = elem.querySelector("input[type=range]"); + var number = elem.querySelector("input[type=number]"); + var mult = parseFloat(elem.dataset.mult); + var fixed = parseInt(elem.dataset.fixed) + var toFixed = function(v) { + if (!isNaN(fixed)) return parseFloat(v).toFixed(fixed); + else return v; + } + if (!range) { + range = document.createElement("input"); + range.setAttribute("type","range"); + number = document.createElement("input"); + number.setAttribute("type","number"); + number.setAttribute("step",mult); + var env1 = document.createElement("div"); + var env2 = document.createElement("div"); + var min = parseFloat(elem.dataset.min); + var max = parseFloat(elem.dataset.max); + var rmin = Math.floor(min/mult); + var rmax = Math.floor(max/mult); + range.setAttribute("min",rmin); + range.setAttribute("max",rmax); + range.addEventListener("input",function() { + var v = parseInt(this.value); + var val = v * mult; + number.value = toFixed(val); + elem.dispatchEvent(new Event("change")); + }); + number.addEventListener("change", function() { + var v = parseFloat(this.value); + var val = v / mult; + range.value = val; + elem.dispatchEvent(new Event("change")); + }); + env1.appendChild(range); + env2.appendChild(number); + elem.appendChild(env1); + elem.appendChild(env2); + } + range.value = val / mult; + number.value = toFixed(val); + + + }, + function(elem) { + var number = elem.querySelector("input[type=number]"); + if (number) return parseFloat(number.valueAsNumber); + else return 0; + + }, + function(elem,attrs) { + + } +)); + +App.prototype.init = function() { + this.strtable = document.getElementById("strtable").dataset; + this.desktop = TemplateJS.View.createPageRoot(true); + this.desktop.loadTemplate("desktop"); + var top_panel = TemplateJS.View.fromTemplate("top_panel"); + this.top_panel = top_panel; + this.desktop.setItemValue("top_panel", top_panel); + top_panel.setItemEvent("save","click", this.save.bind(this)); + top_panel.setItemEvent("login","click", function() { + location.reload(); + }); + + + return this.loadConfig().then(function() { + top_panel.showItem("login",false); + top_panel.showItem("save",true); + var menu = this.createTraderList(); + this.menu = menu; + this.desktop.setItemValue("menu", menu); + this.stopped = {}; + + + }.bind(this)); + +} + +App.prototype.brokerSelect = function() { + var _this = this; + return new Promise(function(ok, cancel) { + + var form = TemplateJS.View.fromTemplate("broker_select"); + _this.waitScreen(fetch_with_error("api/brokers/_all")).then(function(x) { + form.openModal(); + var show_excl = false; + var lst = x.map(function(z) { + var e = _this.config.apikeys && _this.config.apikeys[z]; + var ex = e !== undefined || z.trading_enabled; + show_excl = show_excl || !ex; + return { + excl_info: {".hidden":ex}, + image:_this.brokerImgURL(z.name), + capiton: z.name, + "":{"!click": function() { + form.close(); + ok(z.name); + }} + }; + }); + form.setData({ + "item":lst, + "excl_info": {".hidden": !show_excl}, + }); + form.setCancelAction(function() { + form.close(); + cancel(); + },"cancel"); + + }); + }); +} + +App.prototype.pairSelect = function(broker) { + var _this = this; + return new Promise(function(ok, cancel) { + var form = TemplateJS.View.fromTemplate("broker_pair"); + form.openModal(); + form.setCancelAction(function() { + form.close(); + cancel(); + },"cancel"); + form.setDefaultAction(function() { + form.close(); + var d = form.readData(); + if (!d.pair || !d.name) return; + var name = d.name.replace(/[^-a-zA-Z0-9_.~]/g,"_"); + ok([broker, d.pair, name]); + },"ok"); + fetch_with_error(_this.pairURL(broker,"")).then(function(data) { + var pairs = [{"":{"value":"----",".value":""}}].concat(data["entries"].map(function(x) { + return {"":{"value":x,".value":x}}; + })); + form.setItemValue("item",pairs); + form.showItem("spinner",false); + dlgRules(); + },function() {form.close();cancel();}); + form.setItemValue("image", _this.brokerImgURL(broker)); + var last_gen_name=""; + function dlgRules() { + var d = form.readData(["pair","name"]); + if (d.name == last_gen_name) { + d.name = last_gen_name = broker+"_"+d.pair; + form.setItemValue("name", last_gen_name); + } + form.enableItem("ok", d.pair != "" && d.name != ""); + }; + + + form.setItemEvent("pair","change",dlgRules); + form.setItemEvent("name","change",dlgRules); + dlgRules(); + }); + +} + +App.prototype.updateTopMenu = function(select) { + this.createTraderList(this.menu); + if (select) this.menu.select(select); +} + +App.prototype.waitScreen = function(promise) { + var d = TemplateJS.View.fromTemplate('waitdlg'); + d.openModal(); + return promise.then(function(x) { + d.close();return x; + }, function(e) { + d.close();throw e; + }); +} + +App.prototype.cancelAllOrdersNoDlg = function(id) { + + var tr = this.traders[id]; + var cr = fetch_with_error( + this.traderPairURL(id, tr.pair_symbol)+"/orders", + {method:"DELETE"}); + var dr = fetch_json( + this.traderURL(tr.id)+"/stop", + {method:"POST"}).catch(function() {}); + + + return this.waitScreen(Promise.all([cr,dr])) + .catch(function(){}) + .then(function() { + this.stopped[id] = true; + }.bind(this)); + + +} + +App.prototype.deleteTrader = function(id) { + return this.dlgbox({"text":this.strtable.askdelete},"confirm").then(function(){ + this.curForm.close(); + delete this.traders[id]; + this.updateTopMenu(); + this.curForm = null; + }.bind(this)); +} + +App.prototype.undoTrader = function(id) { + this.curForm.close(); + this.curForm = null; + this.updateTopMenu(id); +} + +App.prototype.resetTrader = function(id) { + this.dlgbox({"text":this.strtable.askreset, + "ok":this.strtable.yes, + "cancel":this.strtable.no},"confirm").then(function(){ + + var tr = this.traders[id]; + this.waitScreen(fetch_with_error( + this.traderURL(tr.id)+"/reset", + {method:"POST"})).then(function() { + this.updateTopMenu(tr.id); + }.bind(this)); + }.bind(this)); +} + +App.prototype.repairTrader = function(id) { + this.dlgbox({"text":this.strtable.askrepair, + "ok":this.strtable.yes, + "cancel":this.strtable.no},"confirm").then(function(){ + + var tr = this.traders[id]; + this.waitScreen(fetch_with_error( + this.traderURL(tr.id)+"/repair", + {method:"POST"})).then(function() { + this.updateTopMenu(tr.id); + }.bind(this)); + }.bind(this)); +} + +App.prototype.cancelAllOrders = function(id) { + return this.dlgbox({"text":this.strtable.askcancel, + "ok":this.strtable.yes, + "cancel":this.strtable.no},"confirm").then(this.cancelAllOrdersNoDlg.bind(this,id)) + .then(function() { + this.updateTopMenu(id); + }.bind(this)); + +} + +App.prototype.addUser = function() { + return new Promise(function(ok,cancel) { + var dlg = TemplateJS.View.fromTemplate("add_user_dlg"); + dlg.openModal(); + dlg.setCancelAction(function(){ + dlg.close(); + cancel(); + },"cancel"); + dlg.setDefaultAction(function(){ + dlg.close(); + var data = dlg.readData(); + var dlg2 = TemplateJS.View.fromTemplate("password_dlg"); + dlg2.openModal(); + dlg2.setData(data); + dlg2.setCancelAction(function() { + dlg2.close(); + cancel(); + },"cancel"); + dlg2.setDefaultAction(function() { + dlg2.unmark(); + var data2 = dlg2.readData(); + if (data2.pwd == "" ) return + if (data2.pwd2 == "" ) { + dlg2.findElements("pwd2")[0].focus(); + return; + } + if (data2.pwd != data2.pwd2) { + dlg2.mark("errpwd"); + } else { + ok({ + username:data.username, + password:data2.pwd, + comment:data.comment + }); + dlg2.close(); + } + },"ok"); + },"ok"); + }); +} + + +App.prototype.securityForm = function() { + var form = TemplateJS.View.fromTemplate("security_form"); + + function dropUser(user, text){ + this.dlgbox({"text": text+user}, "confirm").then(function() { + this.users = this.users.filter(function(z) { + return z.username != user; + }); + update.call(this); + }.bind(this)); + } + + + function update() { + + var rows = this.users.map(function(x) { + var that = this; + return { + user:x.username, + role:{ + "value":x.admin?"admin":"viewer", + "!change": function() { + x.admin = this.value == "admin" + } + }, + comment:x.comment, + drop:{"!click":function() { + dropUser.call(that, x.username, this.dataset.question); + }} + } + },this) + + function setKey(flag, broker, binfo, cfg) { + + if (flag) { + fetch_with_error(this.brokerURL(broker)+"/apikey") + .then(function(ff){ + var w = formBuilder(ff); + w.setData(cfg); + this.dlgbox({text:w},"confirm").then(function() { + if (!this.config.apikeys) this.config.apikeys = {}; + this.config.apikeys[broker] = w.readData(); + form.showItem("need_save",true); + update.call(this); + }.bind(this)); + }.bind(this)); + } else { + if (!this.config.apikeys) this.config.apikeys = {}; + this.config.apikeys[broker] = null; + form.showItem("need_save",true); + update.call(this); + + } + + } + + var brokers = fetch_with_error(this.brokerURL("_all")). + then(function(x) { + return x.map(function(binfo) { + var z = binfo.name; + var cfg = this.config.apikeys && this.config.apikeys[z] + var e = cfg === undefined?binfo.trading_enabled:cfg; + return { + img:this.brokerImgURL(z), + broker: z, + exchange: { + value:binfo.exchangeName, + href:binfo.exchangeUrl + }, + state: { + value: e?"✓":"∅", + classList:{set:e, notset:!e} + }, + bset: { + ".disabled": binfo.trading_enabled && cfg === undefined, + "!click": setKey.bind(this, true, z, binfo, cfg) + }, + + berase: { + ".disabled":!binfo.trading_enabled && !cfg, + "!click": setKey.bind(this, false, z, binfo, cfg) + }, + info_button: { + "!click": function() { + this.dlgbox({text:fetch_with_error(this.brokerURL(z)+"/licence") + .then(function(t) { + return {"value":t,class:"textdoc"}; + }), + cancel:{".hidden":true}},"confirm") + }.bind(this) + }, + } + }.bind(this)); + }.bind(this)); + + + var data = { + rows:rows, + brokers:brokers, + add:{ + "!click":function() { + + this.addUser().then(function(u) { + var itm = this.users.find(function(z) { + return z.username == u.username; + }); + if (itm) itm.password = u.password; + else this.users.push(u); + update.call(this); + }.bind(this)); + }.bind(this) + + }, + "logout":{"!click": this.logout.bind(this)}, + guest_role:{ + "!change":function() { + this.config.guest = form.readData(["guest_role"]).guest_role == "viewer"; + }.bind(this), + "value": !this.config.guest?"none":"viewer" + }, + spinner: brokers.then(function() { + return {".hidden":true} + }) + }; + + form.setData(data); + + } + update.call(this); + + return form; +} + +App.prototype.dlgbox = function(data, template) { + return new Promise(function(ok, cancel) { + var dlg = TemplateJS.View.fromTemplate(template); + dlg.openModal(); + dlg.setData(data); + dlg.setCancelAction(function() { + dlg.close();cancel(); + },"cancel"); + dlg.setDefaultAction(function() { + dlg.close();ok(); + },"ok"); + }); +} + +App.prototype.save = function() { + if (this.curForm) { + this.curForm.save(); + } + if (!this.config) { + this.desktop.close(); + this.init(); + return; + } + var top = this.top_panel; + top.showItem("saveprogress",true); + top.showItem("saveok",false); + this.config.users = this.users; + this.config.traders = this.traders; + this.validate(this.config).then(function(config) { + fetch_json("api/config",{ + method: "PUT", + headers: { + "Content-Type":"application/json", + }, + body:JSON.stringify(config) + }).then(function(x) { + top.showItem("saveprogress",false); + top.showItem("saveok",true); + this.processConfig(x); + this.updateTopMenu + this.stopped = {}; + }.bind(this),function(e){ + top.showItem("saveprogress",false); + if (e.status == 409) { + this.dlgbox({hdr:this.strtable.conflict1, + text:this.strtable.conflict2, + ok:this.strtable.reload}, "confirm") + .then(function() { + location.reload(); + }) + } else { + fetch_error(e); + }pl + + }.bind(this)); + }.bind(this), function(e) { + top.showItem("saveprogress",false); + if (e.trader) + this.menu.select(e.trader); + this.dlgbox({text:e.message},"validation_error"); + }.bind(this)); +} + +App.prototype.logout = function() { + this.desktop.setData({menu:"",content:""}); + this.config = null; + fetch("api/logout").then(function(resp) { + if (resp.status == 200) { + location.reload(); + } + }); +} + +App.prototype.validate = function(cfg) { + return new Promise(function(ok, error) { + var admin = cfg.users.find(function(x) { + return x.admin; + }) + if (!admin) return error({ + "trader":"!", + "message":this.strtable.need_admin + }); + ok(cfg); + }.bind(this)); +} + + + + +App.prototype.init_backtest = function(form, id, pair) { + var url = this.traderURL(id)+"/trades"; + var el = form.findElements("backtest_anchor")[0]; + var bt = TemplateJS.View.fromTemplate("backtest_res"); + el.parentNode.insertBefore(bt.getRoot(),el.nextSibling); + form.enableItem("init_backtest",false); + bt.getRoot().scrollIntoView(false); + Promise.all([fetch_with_error(url),pair]).then(function(v) { + var trades=v[0]; + var pair = v[1]; + if (trades.length == 0) { + bt.close(); + this.dlgbox({text:this.strtable.backtest_nodata,cancel:{hidden:"hidden"}},"confirm"); + return; + } + if (trades.length < 100) { + this.dlgbox({text:this.strtable.backtest_lesstrades,cancel:{hidden:"hidden"}},"confirm"); + } + + var lastTime = trades[trades.length-1].time; + var firstTime = this.config.backtest_interval?(lastTime - this.config.backtest_interval):trades[0].time; + + var elems = ["external_assets","power", "max_pos", + "sliding_pos_hours", "sliding_pos_fade", + "order_mult","min_size","max_size","dust_orders","expected_trend","goal"]; + + var chart1 = bt.findElements('chart1')[0]; + var chart2 = bt.findElements('chart2')[0]; + //var chart3 = bt.findElements('chart3')[0]; + var xdata = { + "date_from":{ + "value":new Date(firstTime), + "!change":recalc + }, + "date_to":{ + "value":new Date(lastTime+86400000), + "!change":recalc + }, + "apply_from":{ + "value":new Date(firstTime), + "!change":recalc + }, + "start_pos":{ + "value":0, + "!input":recalc + } + }; + bt.setData(xdata); + var chart3 = bt.findElements('chart3')[0]; + + var tm; + + var prevChart; + + function recalc() { + if (tm) clearTimeout(tm); + tm = setTimeout(function() { + var data = form.readData(elems); + var xdata = bt.readData(); + + + var min_size = parseFloat(data.min_size); + if (pair.min_size > data.min_size) min_size = pair.min_size; + if (data.dust_orders == false) min_size = 0; + + var h = parseFloat(data.sliding_pos_hours); + var w = parseFloat(data.sliding_pos_fade); + var g = data.goal; + if (g == "norm") { + w = 0; + h = 0; + } + var et = parseFloat(data.expected_trend)*0.01; + if (pair.invert_price) { + et = -et; + } + + var trades2 = trades.filter(function(x) { + var d = new Date(x.time); + return d>=xdata.apply_from; + }); + + var btdata = { + data:trades2, + external_assets:parseFloat(data.external_assets), + sliding_pos:h, + fade:w, + multiplicator:parseFloat(data.order_mult)*0.01, + min_order_size:min_size, + invert:false, + expected_trend:et, + start_pos:parseFloat(xdata.start_pos), + max_order_size:parseFloat(data.max_size), + step: pair.min_size, + max_pos:parseFloat(data.max_pos) + }; + + var res = calculateBacktest(btdata); + + if (pair.invert_price) { + res.chart.forEach(function(x){ + x.price = 1/x.price; + x.np = 1/x.np; + x.achg = -x.achg; + }); + } + + var chartData = res.chart.filter(function(x){ + var d = new Date(x.time); + return d >=xdata.date_from && d<=xdata.date_to; + }); + var interval = chartData.length?chartData[chartData.length-1].time - chartData[0].time:1; + var drawChart = initChart(interval,5,700000); + + + bt.setData({ + bt_pl: adjNum(res.pl), + bt_pln: adjNum(res.pln), + bt_dd: adjNum(res.mdd), + bt_mp: adjNum(res.maxpos), + }) + + drawChart(chart1,chartData,"pl",[],"pln"); + drawChart(chart2,chartData,"price",[],"np"); + drawChart(chart3,chartData,"achg",[]); + }.bind(this), 1); + } + + recalc.call(this); + elems.forEach(function(x) { + var el = form.findElements(x)[0]; + el.addEventListener("input", recalc.bind(this)); + },this); + + + + }.bind(this),function() { + bt.close(); + }); +} + +App.prototype.optionsForm = function() { + var form = TemplateJS.View.fromTemplate("options_form"); + var data = { + report_interval: defval(this.config.report_interval,864000000)/86400000, + backtest_interval: defval(this.config.backtest_interval,864000000)/86400000, + stop:{"!click": function() { + this.waitScreen(fetch_with_error("api/stop",{"method":"POST"})); + for (var x in this.traders) this.stopped[x] = true; + }.bind(this)}, + reload_brokers:{"!click":function() { + this.waitScreen(fetch_with_error("api/brokers/_reload",{"method":"POST"})); + }.bind(this)} + + }; + form.setData(data); + form.save = function() { + var data = form.readData(); + this.config.report_interval = data.report_interval*86400000; + this.config.backtest_interval = data.backtest_interval*86400000; + }.bind(this); + return form; + +} + +App.prototype.tradingForm = function(id) { + if (this.curForm && this.curForm.save) { + this.curForm.save(); + } + this.curForm = null; + var _this = this; + var cfg = this.traders[id]; + if (!cfg) return; + + + var form = TemplateJS.View.fromTemplate("trading_form"); + this.desktop.setItemValue("content", form); + + function dialogRules() { + var data = form.readData(["order_size","order_price","edit_order"]); + var p = !!data.order_price; + var q = !!data.order_size; + form.showItem("button_buy",p); + form.showItem("button_sell",p); + form.showItem("button_buybid",!p); + form.showItem("button_sellask",!p); + form.enableItem("button_buy",q); + form.enableItem("button_sell",q); + form.enableItem("button_buybid",q); + form.enableItem("button_sellask",q); + } + + form.setItemEvent("order_price","input",dialogRules); + form.setItemEvent("order_size","input",dialogRules); + form.setItemEvent("edit_order","change",dialogRules); + dialogRules(); + + function update() { + var traderURL = _this.traderURL(id); + var f = fetch_json(traderURL+"/trading").then(function(rs) { + var pair = rs.pair; + var chartData = rs.chart; + var trades = rs.trades; + var orders = rs.orders; + var ticker = rs.ticker; + var invp = function(x) {return pair.invert_price?1/x:x;} + var invs = function(x) {return pair.invert_price?-x:x;} + var now = Date.now(); + var skip = true; + chartData.push(ticker); + trades.unshift({ + time: chartData[0].time-100000, + price: chartData[0].last + }); + + var drawChart = initChart(chartData[chartData.length-1].time - chartData[0].time); + var data = mergeArrays(chartData, trades, function(a,b) { + return a.time - b.time + },function(n, k, f, t) { + var p1; + var p2; + var achg; + if (n == 0) { + if (f != null && t != null) { + p2 = invp(interpolate(f.time, t.time, k.time, f.price, t.price)); + } else if (f != null) { + p2 = invp(f.price); + } + p1 = invp(k.last); + skip = false; + + } else { + if (skip) return null; + p1 = p2 = invp(k.price); + achg = invs(k.size); + } + return { + time: k.time, + p1:p1, + p2:p2, + achg: achg + } + }).filter(function(x) {return x != null;}); + + var lines = []; + orders.forEach(function(x) { + var sz = invs(x.size); + var l = sz < 0?_this.strtable.sell:_this.strtable.buy + lines.push({ + p1: invp(x.price), + label: l+" "+Math.abs(sz)+" @", + class: sz < 0?"sell":"buy" + }); + }); + form.findElements("chart").forEach(function(elem) { + drawChart(elem, data, "p1",lines,"p2" ); + }) + var formdata = {}; + if (pair.invert_price) { + formdata.ask_price = adjNumN(1/ticker.bid); + formdata.bid_price = adjNumN(1/ticker.ask); + } else { + formdata.ask_price = adjNumN(ticker.ask); + formdata.bid_price = adjNumN(ticker.bid); + } + formdata.bal_assets = adjNumN(invs(pair.asset_balance)) + " "+pair.asset_symbol; + formdata.bal_currency = adjNumN(pair.currency_balance) + " "+pair.currency_symbol; + formdata.last_price = adjNumN(invp(ticker.last)); + var orderMap = orders.map(function(x) { + return {"":{ + ".value":JSON.stringify(x.id), + "value":(invs(x.size)<0?_this.strtable.sell:_this.strtable.buy) + + " " + adjNumN(Math.abs(x.size)) + +" @ " + adjNumN(invp(x.price)) + }} + }); + formdata.orders = orders.map(function(x) { + return {id:x.id, + dir:invs(x.size)<0?_this.strtable.sell:_this.strtable.buy, + size:adjNumN(Math.abs(x.size)), + price:adjNumN(invp(x.price)), + cancel:{ + "!click":function(ev) { + ev.stopPropagation(); + this.hidden = true; + this.nextSibling.hidden = false; + _this.cancelOrder(cfg.id, cfg.pair_symbol, x.id). + then(update); + }, + ".hidden":false + + }, + "spinner":{ + ".hidden":true + }, + "":{ + "!click":function() { + form.setData({ + edit_orders: orderMap, + edit_order: JSON.stringify(x.id), + order_size: adjNumN(Math.abs(x.size)), + order_price: adjNumN(invp(x.price)), + }); + dialogRules(); + } + } + } + }); + var cur_edit = form.readData(["edit_order"]); + if (cur_edit.edit_order == "") { + formdata.edit_orders=orderMap; + formdata.edit_order = { + "!change": function() { + if (this.value=="") { + form.setData({ + "order_size":"", + "order_price":"" + }); + } else { + var itm = orders.find(function(x) { + return this.value == JSON.stringify(x.id); + }.bind(this)); + if (itm) form.setData({ + order_size: adjNumN(Math.abs(itm.size)), + order_price: adjNumN(invp(itm.price)) + }); + } + dialogRules(); + } + } + } + + form.setData(formdata); + return rs; + }) + + return f; + } + function cycle_update() { + if (!form.getRoot().isConnected) return; + setTimeout(cycle_update, 15000); + return update(); + } + + function clearForm() { + form.setData({ + "order_size":"", + "order_price":"", + "edit_order":"" + }); + dialogRules(); + } + + + cycle_update().then(function(f) { + var pair = f.pair; + + function postOrder() { + if (!_this.stopped[id] && cfg.enable) { + _this.dlgbox({text:_this.strtable.trade_ask_stop},"confirm") + .then(function() { + return _this.waitScreen(fetch_with_error(_this.traderURL(cfg.id)+"/stop",{method:"POST"})); + }).then(function() { + _this.stopped[id] = true; + postOrder.call(this); + }.bind(this)); + return; + } + + var b = this.dataset.name; + var d = form.readData(["edit_order","order_price","order_size"]); + + var price = b == "button_buybid"?(pair.invert_price?"ask":"bid") + :(b == "button_sellask"?(pair.invert_price?"bid":"ask"): + (pair.invert_price?1/d.order_price:d.order_price)); + var size = ((b == "button_buybid" || b == "button_buy") == pair.invert_price?-1:1)*d.order_size; + var id; + if (d.edit_order) id = JSON.parse(d.edit_order); + var url = _this.traderPairURL(cfg.id, cfg.pair_symbol)+"/orders"; + var req = { + size: size, + price: price, + replaceId: id, + replaceSize: 0 + }; + clearForm(); + _this.waitScreen(fetch_with_error(url, {method:"POST", body: JSON.stringify(req)}).then(update)); + } + + form.setItemEvent("button_buy","click", postOrder); + form.setItemEvent("button_sell", "click",postOrder); + form.setItemEvent("button_buybid", "click",postOrder); + form.setItemEvent("button_sellask", "click",postOrder); + }).catch(fetch_error); +} + +App.prototype.cancelOrder = function(trader, pair, id) { + var url = this.traderPairURL(trader, pair)+"/orders" + var req = { + size:0, + price:0, + replaceId: id, + replaceSize: 0, + }; + return fetch_with_error(url,{method:"POST",body: JSON.stringify(req)}); +} + +App.prototype.brokerConfig = function(id, pair) { + var burl = this.pairURL(id, pair)+"/settings"; + var form = fetch_with_error(burl).then(formBuilder); + this.dlgbox({custom:form}, "confirm").then(function() { + form.then(function(f) { + var d = f.readData(); + this.waitScreen(fetch_with_error(burl, {method:"PUT",body:JSON.stringify(d)})); + }.bind(this)) + }.bind(this)) +} diff --git a/www/admin/index.html b/www/admin/index.html new file mode 100644 index 00000000..9e70a506 --- /dev/null +++ b/www/admin/index.html @@ -0,0 +1,452 @@ + + + + +MM Bot + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/admin/style.css b/www/admin/style.css new file mode 100644 index 00000000..2c5646ad --- /dev/null +++ b/www/admin/style.css @@ -0,0 +1,775 @@ +@charset "UTF-8"; + +@import url(https://fonts.googleapis.com/css?family=Ruda&display=swap); +body { + background-color:#222; + color:#CDCDCD; + font-family: Ruda; + margin:0; + font-size: 0.4cm; + padding: 0; +} + +a {color: yellow;} +a:visited {color: #ffa800;} + +[hidden] { display: none !important;} +input,select,button,textarea { + background-color: black; + border: 1px solid #888; + padding: 0.1cm; + color: white; + font-family: Ruda; + font-size: 0.4cm; + box-sizing:border-box; +} +input[type="date"] { + padding: 0; + width: 49%; + text-align: center; + font-size: 0.8em; +} +input[readonly] { + border: 0; + cursor: default; +} +input[disabled],select[disabled],button[disabled],button[disabled]:hover { + background-color: #444; + color: #888; + +} + +p.textdoc {white-space: pre-wrap;text-align: justify;font-family: monospace;} + +button { + background-color: #444; + box-shadow: 2px 2px 5px black; + min-width: 5em; + margin: 0 0.4em; +} + +button:hover { + color: yellow; +} +button:active { + box-shadow: 0px 0px 0px black; + transform: translate(2px,2px); +} + +x-slider { + display: inline-block; + padding-left: 5.2em; + position: relative; +} +x-slider > div { + height: 100%; +} +x-slider > div >input { + display: block; + box-sizing: border-box; + width:100%; + height:100%; + text-align: center; +} +x-slider > div:nth-child(2) { + position:absolute; + left: 0; + top:0; + width: 5em; + bottom:0; +} +x-form { + display:block; +} +x-form.sections { + margin: 0px 2%; +} +x-form label, x-form div.label { + display:flex; + align-items: center; + margin: 0.2em 0; + position: relative; +} +x-form label > *, x-form div.label > * { + display:block; + width: 50%; + box-sizing: border-box; +} + +x-form div.label label > *{ + width: unset; +} + +x-form img.inline_img { + height: 2em; + vertical-align: middle; +} +x-form input[type=number] { + text-align:center; +} +x-section { + display:block; + border: 1px solid; + padding: 1em 0.5em; + position: relative; + margin: 1em 0; + box-sizing: border-box; +} +x-section.goal { + top: 0; + margin: 1.1em 0; +} +x-section.goal.hidden { + display:none +} +x-section-caption { + background-color: #222; + /* border: 1px solid; */ + display: block; + position: absolute; + left: 1em; + top: -0.7em; + padding: 0 0.3em; + /* z-index:1; */ +} + +x-form.no_adv .adv { + display:none; +} + +x-form.no_experimental .experimental { + display:none; +} + + + +x-form.no_adv .adv.unhide,x-form.no_experimental .experimental.unhide { + display:block; +} +x-form.no_adv label.adv.unhide, x-form.no_adv div.label.adv.unhide { + display: flex; +} +x-form label.modified, x-form div.label.modified { + color:yellow; +} + +div.trader.list .selected { + background-color: #333; +} + +div.trader.list { + display: flex; + justify-content:center; + flex-wrap: wrap; +} +div.trader.list > * { + cursor: pointer; +} +div.trader.list > *:hover { + background-color:#223; +} + +div.trader.list > *:active { + background-color:#484b52; +} + + +div.trader.list > * { + margin: 0.2cm 0.1cm; + text-align:center; + position: relative; +} + +div.trader.list .exclamation {position: absolute;right: 0;top: -5px;font-size: 0.7cm;/* font-weight: bold; */color: #F44;text-shadow: 0 0 10px black, 0 0 5px black, 0 0 2px black;} +.no_api_key {color: #F44;} + +div.trader.list img { + width: 1.2cm; + height: 1.2cm; +} + +div.broker.pair img { + width: 2cm; +} + +div.dialog > div { + margin:auto; + text-align: center; + background-color: black; + border: 1px solid #888; + display: inline-block; + vertical-align: middle; + padding: 0 0 1% 0; + min-width: 8cm; + width: 90%; + max-width: 20cm; + white-space: normal; + box-shadow: 10px 6px 20px black; + box-sizing: border-box; + padding: 5px; + max-width: 16cm; +} + + +div.dialog div.confirm div[data-name=custom] { + text-align: justify; + margin: 2%; + white-space: pre-line; +} + +div.dialog > div.confirm p { + text-align: center; + white-space: pre-line; +} + +div.dialog { + position: fixed; + top: 0; + right: 0; + left: 0; + bottom: 0; + text-align: center; + white-space: nowrap; + overflow: auto; + z-index: 2; +} + +div.dialog::before { + content: ""; + display: inline-block; + height: 100vh; + vertical-align: middle; +} + +div.broker.pair .form { + margin: 1em; +} + + + +.control_row {display: flex;} +.control_row > * {flex-grow: 1;margin: 0 0.1em;min-width: 0;} +.control_row > *:first-child {margin-left: 0;} +.control_row > *:last-child {margin-right: 0;} + +.flexpanel { + display: flex; + justify-content: space-between; + border-bottom: 2px solid #884; + background-color: #332; + position: fixed; + width: 100%; + box-sizing: border-box; + z-index: 10; + top: 0; + left: 0; + right: 0; +} + +.flexpanel > * { + line-height: 1.5cm; + white-space: nowrap; +} + +.flexpanel > *:first-child { + white-space: nowrap; + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; +} + +.fonticon { + display: inline-block; + font-size: 1cm; + vertical-align: middle; + width: 1.1cm; + height: 1.1cm; + overflow: hidden; + text-align: center; + font-weight: bold; + cursor: default; + box-sizing: content-box; + padding: 0.2cm; + border: 0; + line-height: 1.1cm; +} + + + +.backicon { + color: #CC8; +} + +a { + margin: 0; +} + +.desktop { + padding: 4em 0 0 0; + margin: 0 0 0 0; +} + +.lightbox { + background-color: #5f5f5fb0; + opacity: 1; + display: block; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 2; +} + +.main_form_wait x-section { + height: 13em; + position: relative; +} + +.spinner { + border-left: 10px solid #ffd202; + border-top: 10px solid #ffd20287; + border-right: 10px solid #ffd202; + border-bottom: 10px solid #ffd20287; + border-radius: 1cm; + animation: rot 3s infinite cubic-bezier(0.39, -0.01, 0.52, 1.01); +} + +.save.spinner { + width: 0.7cm; + height: 0.7cm; + display: inline-block; + vertical-align: middle; + box-sizing: border-box; +} + +.save.hide { + display: inline-block; + animation: savehide 3s; + animation-fill-mode: forwards; + vertical-align:middle; + font-size: 2em; + width: 0.7cm; + color: lime; + box-sizing: border-box; + /* font-weight: bold; */ + text-shadow: 0 0 10px white; + overflow: hidden; +} + +.spinner.inline { + display: inline-block; + vertical-align: middle; +} + +.security_form .apikeys img { + width: 32px; + vertical-align: inherit; + margin-right: 1em; +} +.security_form .apikeys td.set { + font-size: 2em; + color: green; +} +.security_form .apikeys td.notset { + font-size: 2em; + color: red; +} +.security_form .apikeys [data-name=info_button] { + cursor: pointer; +} + + +.main_form_wait x-section div, .waitdlg div { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + width: 1cm; + height: 1cm; + margin: auto; +} + +.broker.pair .spinner { + width: 0.4em; + height: 0.4em; + display: inline-block; + vertical-align: text-bottom; + margin-left: 1em; +} + +.spinner.fast { + animation: rot_fast 0.5s infinite linear; +} + +.backtest .charts { + overflow: auto; +} + +.backtest { + width: 100% !important; +} + +.trouble_sect { + display:flex; + flex-wrap: wrap; +} + +.trouble_sect > * { + flex-grow: 1; + width: 7cm; + text-align: justify; + margin: 1em; + /* height: 100%; */ + position: relative; + padding-bottom: 4em; +} +.trouble_sect > * > div { + text-align: center; + position: absolute; + bottom: 0; + left: 0; + right: 0; +} +.trouble_sect > * > div > button { + width: 50%; + height: 3em; +} + + +@keyframes rot { + 0% { + transform: rotate(0); + opacity: 0; + } + 40%{ + opacity:1; + } + 60% { + opacity:1; + } + 100% { + transform: rotate(960deg);; + opacity:0; + } +} + +@keyframes rot_fast { + 0% { + transform: rotate(0); + } + 100% { + transform: rotate(360deg); + } +} + +@keyframes savehide { + 0% { + width: 0.7cm; + opacity: 1; + text-shadow: 0 0 0px white; + } + 50% { + width: 0.7cm; + opacity: 1; + text-shadow: 0 0 20px white; + } + 75% { + width: 0.7cm; + opacity: 0; + text-shadow: 0 0 100px white; + } + 100% { + width: 0; + opacity: 0; + } +} + +.error { + color: red; + font-size: 0.9em; + display: none; +} +.error.mark { + display: block; +} + +.warning { + color: #f4ff88; + font-size: 1em; + padding-left: 2em; + background: url(data:image/pmh;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAdCAMAAAD8QJ61AAAAM1BMVEVAAAABAwA3KwVVQgZ2YwSQfgC7jwCsoADjtQD9sADkvwD9ugD+wAD9yQD+0wD93AD96gCsTSPwAAAAAXRSTlMAQObYZgAAAM9JREFUKM99ktESxRAMRHG1jVTw/197o6oNynlKYmfFDqUagAjUAogpxZWAEoNrA7aYXxL4lBVhdo4pWa23FHFqcGjmmFkUg4UFX78Vh2+L/MQqiDR5YtnhO60QmUuAuaLRIIOPYEgr9IJuTxSCUobWItzwual1a3BjhECkBYEK4ae1rY14CD1w1vbtxD+rcJTb28FgQGCMkN8WSP5BlLkrezbDfW8Ul4E/X3gHK1qPeUMxOHOSTvS8JzkJ52CaAXUCZ+3eCcC7BR6amEZA/QGVSRdPeDJxawAAAABJRU5ErkJggg==); + background-size: 1.4em; + background-repeat: no-repeat; + background-position: 0% 50%; + display: none; +} +.warning.mark { + display: block; +} + + +.security_form table { + width: 100%; + background-color: #3e4130; + padding: 0.2cm; +} +.security_form table td,.security_form table th { + text-align: center; + padding: 0.2cm; + border-bottom: 1px solid #222; +} +.security_form table td:first-child { + text-align:left; +} + +.panelbutton { + padding-right: 1em; + cursor: pointer; + display: inline-block; +} + +.panelbutton:hover { + background-color: #443; +} + +.waitdlg { + width: 3cm !important; + height: 6cm; +} + +.rightinfo { + text-align: right; + font-style: italic; + font-size: 0.8em; +} + +x-form.pair table.orders {margin: auto;border-collapse: collapse;width: 100%;font-size: 0.8em;background-color: #3e4130;} +x-form.pair table.orders td,x-form.pair table th { + text-align: center; + padding: 5px; + position:relative; +} + +x-form.pair table.orders td:nth-child(4) { + width: 5em; +} + +x-form.pair table.orders td button { + display: block; + left:0; + top: -6px; + right:0; + bottom: 5px; + min-width: unset; + width: 2em; + position: absolute; + z-index: 1; +} + +.pairactions div.icon { + display: inline-block; + border-radius: 10cm; + overflow: hidden; + border: 1px solid #555; + margin: 0.05cm; +} + +.pairactions div.icon.red img { + background-color: #6f1111; +} + +.pairactions div.icon.red img:hover { + background-color: red; +} + + +.pairactions img { + width: 1.3cm; + height: 1.3cm; + vertical-align: middle; + border-radius: 0; + padding: 0.2cm; + box-sizing: border-box; + filter: drop-shadow(0px 0px 5px black); +} + +.pairactions img:hover { + background-color: #5f5133; +} + +.pairactions { + margin-bottom: 0.2cm; + background: linear-gradient(to right, transparent,#3e3e3e); + margin: -1em -0.4em 1em 0; + padding-right: 0.2cm; + text-align: right; +} + +.security_form .buttons { + display: flex; + justify-content: space-between; + flex-wrap:wrap; + margin: 1em 1em 0 1em; +} + +button > img { + height: 1em; + vertical-align: middle; +} + +.textorderline.current { + font-size: 1cm; +} + +span.tooltip { + position: absolute; + display: block; + width: 40%; + background: black; + z-index: 5000; + padding: 5px; + border: 1px solid #888; + top: 0; + left: 2em; + color: white; + font-size: smaller; + pointer-events: none; + opacity: 0; + transition: 1s all cubic-bezier(1, -2.49, 1, 0.97); +} + +label span:hover span.tooltip, div.label span:hover span.tooltip { + display: block; + opacity:1; + +} + +.strategygoal div {display:none;font-style:italic;margin-bottom: 1em;} +.strategygoal > div.plfrompos > div.plfrompos {display:block;} +.strategygoal > div.halfhalf > div.halfhalf {display:block;} +.strategygoal > div.keepvalue > div.keepvalue {display:block;} + +x-form.trading .buttons {text-align: center;} +x-form.trading .buttons button {width: 40%} +x-form.trading button.buy {background-color:#024e02;} +x-form.trading button.sell {background-color:#5f0101;} +x-form.trading table.orders {width: 100%;border-collapse:collapse} +x-form.trading table.orders td {text-align:center;padding: 3px;border-top: 1px solid #333} +x-form.trading table.orders tr { background-color:black;} +x-form.trading table.orders td button { + min-width: 2em; + background-color: transparent; + border: 0; + box-shadow: 0 0 0; +} +x-form.trading table.orders td .spinner {display: inline-block;} +x-form.trading .chart .spinner { + display: block; + height: 1cm; + width: 1cm; + margin: 3cm auto; +} + +[data-name=auto_max_backtest_result] { + font-weight:bold; +} + +[data-name=auto_max_backtest_result].wait::before { + content:""; + display:inline-block; + animation: rot_fast 0.5s infinite linear; + border-top: 2px solid; + border-bottom: 2px solid; + width: 1em; + height: 1em; + border-radius: 1em; + vertical-align: middle; + margin: 0.2em; + box-sizing: border-box; +} + +@media all and (max-width: 15cm) { + + +input,select,button,textarea { + font-size: 0.4cm; + padding:0.05cm; +} +.security_form table tr { + display: flex; + flex-wrap:wrap; + border-bottom: 1px solid #000; +} +.security_form table th { + display:none; +} +.security_form table td { + border-bottom: 0; + flex-grow:1; + width: 40%; + +} +} + + +@media all and (min-width: 25cm) { + +.vertical { + position: relative; +} + +.vertical .menu { + position: absolute; + width: 6cm; +} + +.vertical .content { + margin-left: 6cm; + box-sizing:border-box; + +} + +.vertical div.trader.list { + flex-direction: column; + margin-top: 1em; +} +.vertical div.trader.list > *{ + flex-direction: column; + text-align: left; + margin: 0.1cm; +} +.vertical div.trader.list > *:last-child { + text-align: center; +} + +.vertical div.trader.list img {width: 1cm;height: 1cm;vertical-align:middle;} +.vertical div.trader.list .item div { + display: inline-block; +} +.vertical .content x-form.sections { + display:flex; + flex-wrap:wrap; + max-width: none; + justify-content: center; + margin: 0; +} +.vertical .content x-form.sections > * { + flex-grow:1; + width: 15cm; + margin-right: 1em; +} + +.vertical .content x-section.small_box { + width: 40%; + flex-grow: 2; +} + +.vertical .content x-section.center_box { + flex-grow:0; + width: 11cm; +} + +} + diff --git a/www/index.html b/www/index.html index 7f181ffe..87bd68c9 100644 --- a/www/index.html +++ b/www/index.html @@ -4,13 +4,15 @@ MM Bot + +
-
+
@@ -34,18 +37,18 @@
- +
-
+ + +
LOG
@@ -99,96 +101,6 @@
- + diff --git a/www/manifest.json b/www/manifest.json index 85a63df8..a409f695 100644 --- a/www/manifest.json +++ b/www/manifest.json @@ -12,7 +12,11 @@ "src": "res/logo.png", "type": "image/png", "sizes": "512x512" - }] + }, { + "src": "res/icon64.png", + "type": "image/png", + "sizes": "64x64" + }] } diff --git a/www/res/add_icon.png b/www/res/add_icon.png new file mode 100644 index 00000000..996a7b9f Binary files /dev/null and b/www/res/add_icon.png differ diff --git a/www/res/adduser.png b/www/res/adduser.png new file mode 100644 index 00000000..b541fec9 Binary files /dev/null and b/www/res/adduser.png differ diff --git a/www/res/broker_settings.png b/www/res/broker_settings.png new file mode 100644 index 00000000..3fed42d3 Binary files /dev/null and b/www/res/broker_settings.png differ diff --git a/www/res/chart.css b/www/res/chart.css new file mode 100644 index 00000000..5987a84e --- /dev/null +++ b/www/res/chart.css @@ -0,0 +1,93 @@ +.stdline { + stroke: #FF8; + stroke-width: 2px; +} + +.stdline2 { + stroke: #afdeff; + stroke-width: 2px; +} + + +.minoraxe { + stroke: #888; + stroke-opacity: 0.5; +} + +.marker.buy { + fill: #4F4; +} + +.marker.sell { + fill: #F44; +} +line.marker.buy { + stroke:#4f4; + stroke-width:3px; +} +line.marker.sell { + stroke:#F44; + stroke-width:3px; +} +.majoraxe { + stroke: #FFF; +} + + +.lineaxis { + stroke: #888; + stroke-width: 1px; +} + +.textaxis { + vertical-align: center; + text-anchor: begin; + alignment-baseline: after-edge; + font-size: 20px; + fill: #888; +} + +.textaxisx { + text-anchor: middle; + alignment-baseline: before-edge; + font-size: 20px; + fill: #888; +} + +.textaxis.bottom { + alignment-baseline: after-edge; +} +.orderline { + stroke: #FAA; + stroke-width:2; + stroke-dasharray:4; +} + +.orderline.buy{ + stroke: #AFA; +} + +.orderline.sell{ + stroke: #FAA; +} +.orderline.last{ + stroke: #AAF; +} + +.orderline.current{ + stroke: #f4ff88; +} + +.textorderline { + font-size: 27px; + fill: #FFF; + alignment-baseline: after-edge; + font-style: italic; + text-shadow: 0 0 10px black; + font-weight: bold; +} + +o + fill: #f4ff88; + font-size: 20px; +} diff --git a/www/res/code.js b/www/res/code.js index a0a0261f..fe6f75e8 100644 --- a/www/res/code.js +++ b/www/res/code.js @@ -1,172 +1,8 @@ -function beginOfDay(dt) { - return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()); -} - - -function fetch_json(file) { - "use strict"; - - return fetch(file).then(function(req) { - return req.json(); - }); - -} +"use strict"; var changeinterval=null; var source_data=null; - -function adjNum(n, decimals) { - "use strict"; - if (typeof n != "number") return n; - if (isNaN(n)) return "---"; - if (!isFinite(n)) { - if (s < 0) return "-∞"; - else return "∞"; - } - if (decimals !== undefined) return n.toFixed(decimals); - var an = Math.abs(n); - if (an >= 100000) return n.toFixed(0); - else if (an >= 1) return n.toFixed(2); - else if (an > 0.0001) return n.toFixed(6); - else { - var s = (n*1000000).toFixed(3); - if (s == "0.000") return s; - return s+"µ"; - } -} - -var base_interval=1200000; - var chart_interval=1000; - - -function new_svg_el(name, attrs, childOf) { - "use strict"; - var elem = document.createElementNS("http://www.w3.org/2000/svg", name); - if (attrs) {for (var i in attrs) { - elem.setAttributeNS(null,i, attrs[i]); - }} - if (childOf) childOf.appendChild(elem); - return elem; - } - - -function drawChart(elem, chart, fld, lines, fld2) { - "use strict"; - - var now = new Date(chart[chart.length-1].time); - var bday = beginOfDay(now); - var skiphours = Math.floor((now - bday)/(base_interval)); - - elem.innerText = ""; - - var daystep = 24*3600000/base_interval; - var step = 2*864000000/chart_interval; - var activewidth=step*chart_interval/base_interval; - var dattextstep = Math.ceil(120/(daystep*step)); - var activeheight = (activewidth/3)|0; - var minmax = chart.concat(lines?lines:[]).reduce(function(c,x) { - if (c.min > x[fld]) c.min = x[fld]; - if (c.max < x[fld]) c.max = x[fld]; - return c; - },{min:chart[0][fld],max:chart[0][fld]}); - minmax.sz = minmax.max - minmax.min; - minmax.min = minmax.min - minmax.sz*0.05; - minmax.max = minmax.max + minmax.sz*0.05; - var priceStep = activeheight/(minmax.max-minmax.min); - var axis = 2; - var label = 20; - var rowstep = Math.pow(10,Math.floor(Math.log10((minmax.max-minmax.min)/3))); - var rowbeg= Math.floor(minmax.min/rowstep); - var rowend= Math.floor(minmax.max/rowstep); - - var svg = new_svg_el("svg",{viewBox:"0 0 "+(activewidth+axis)+" "+(activeheight+axis+label)},elem); - new_svg_el("line",{x1:axis, y1:0,x2:axis,y2:activeheight+axis,class:"lineaxis"},svg); - new_svg_el("line",{x1:0, y1:activeheight+1,x2:activewidth+axis,y2:activeheight+1,class:"lineaxis"},svg); - var cnt = chart.length; - function map_y(p) { - return activeheight-(p-minmax.min)*priceStep; - } - function map_x(x) { - return x*step+axis; - } - - if (!isFinite(rowbeg) || !isFinite(rowend)) return; - - for (var i = rowbeg; i <=rowend; i++) { - var v = i*rowstep; - var maj = Math.abs(v) 0) { - var xtm = map_x(xtmpos); - new_svg_el("line",{x1:xtm,y1:0,x2:xtm,y2:activeheight,class:"minoraxe"},svg); - xtmpos-=daystep; - } - xtmpos = activewidth/step-skiphours; - while (xtmpos > 0) { - var xtm = map_x(xtmpos); - new_svg_el("text",{x:xtm,y:activeheight,class:"textaxisx"},svg).appendChild(document.createTextNode(bday.toLocaleDateString())); - bday.setDate(bday.getDate()-dattextstep); - new_svg_el("line",{x1:xtm,y1:activeheight-5,x2:xtm,y2:activeheight,class:"majoraxe"},svg); - xtmpos-=daystep*dattextstep; - } - var tmstart=(now/base_interval-activewidth/step) - for (var i = 0; i 0); @@ -279,7 +115,8 @@ function app_start(){ error_element.classList.remove("error"); } } - elem_title.innerText = title; + elem_title.innerText = title; + if (misc && misc.icon) elem_icon.src = misc.icon; else elem_icon.hidden = true; var info = curchart.querySelector("[data-name=info]"); if (ranges) { info.hidden = false; @@ -438,7 +275,7 @@ function app_start(){ // misc.avgt = last_norm/misc.mt; misc.avgh = lt.norm/misc.tt*it; misc.avgha = lt.nacum*it/misc.tt; - misc.avghpl = it*lt.pln/misc.tt; + misc.avghpl = it*lt.pl/misc.tt; } for (var n in misc) @@ -744,7 +581,9 @@ function app_start(){ } function hasChart(chart, item) { - return (chart.length != 0) && chart[0][item] !== undefined; + return (chart.length != 0) && chart.find(function(x) { + return item in x + }) !== undefined; } function update() { @@ -836,6 +675,7 @@ function app_start(){ class: "last", }) ranges[sm]["last"] = [stats.prices[sm],""]; + stats.misc[sm].icon = infoMap[sm].brokerIcon; } @@ -843,7 +683,7 @@ function app_start(){ localStorage["mmbot_time"] = Date.now(); - chart_interval = stats.interval; + drawChart = initChart(stats.interval); redraw = function() { var fld = location.hash; @@ -868,6 +708,9 @@ function app_start(){ appendList("_"+k,infoMap[k], ranges[k], stats.misc[k]); } updateLastEventsAll(charts); + } else if (fld == "+dpr") { + setMode(6); + appendDailyPerformance(stats.performance); } else if (fld.startsWith("!")) { setMode(1); var pair = fld.substr(1); @@ -898,6 +741,9 @@ function app_start(){ redraw(); } + document.getElementById("chartarea") + .appendChild(chart_padding); + } @@ -924,6 +770,37 @@ function app_start(){ + function appendDailyPerformance(data) { + document.getElementById("lastevents").innerText=""; + var table = document.createElement("table"); + table.setAttribute("class","perfmod"); + var hdr = document.createElement("tr"); + data.hdr.forEach(function(x) { + var h = document.createElement("th"); + h.innerText = x; + hdr.appendChild(h); + }) + table.appendChild(hdr); + data.rows.forEach(function(r) { + var first = true; + var hr = document.createElement("tr"); + r.forEach(function(c) { + var h = document.createElement("td"); + if (first && typeof(c) == "number") { + first = false; + h.innerText = (new Date(c*1000)).toLocaleDateString(); + } else { + h.innerText = adjNum(c); + } + hr.appendChild(h); + + }); + table.appendChild(hr); + }) + var curchart = createChart("perfrep", "perfrep"); + curchart .innerText = ""; + curchart .appendChild(table); + } window.addEventListener("hashchange", function() { @@ -952,7 +829,7 @@ function app_start(){ removeLogo(); } else { logo.hidden = false; - setTimeout(removeLogo, 3000); + setTimeout(removeLogo, 2000); } opencloselog.addEventListener("click",function() { @@ -960,7 +837,6 @@ function app_start(){ v.hidden = !v.hidden; }) - init_calculator(); changeinterval = function() { interval = (interval+1)%intervals.length; @@ -1001,218 +877,6 @@ function createCSV(chart, infoMap) { return rows.join("\r\n"); } -function pow2(x) { - return x*x; -} - -function init_calculator() { - "use strict"; - - - var calc = document.getElementById("calculator"); - var menu = calc.querySelector("menu"); - var menu_items = menu.querySelectorAll("li"); - var menu_fn = function() { - Array.prototype.forEach.call(menu_items, function(x) { - x.classList.remove("selected"); - document.getElementById(x.dataset.name).hidden = true; - }); - this.classList.add("selected"); - document.getElementById(this.dataset.name).hidden = false; - } - Array.prototype.forEach.call(menu_items, function(x) { - x.addEventListener("click",menu_fn); - }); - - function update_ranges(f,min,max,boost) { - f.querySelector(".range_max").innerText = adjNum(max); - f.querySelector(".range_min").innerText = adjNum(min); - f.querySelector(".boost").innerText = adjNum(boost); - } - - var order_form = document.getElementById("form_order"); - var order_form_calc = function() { - var p = parseFloat(order_form.p.value); - var a = parseFloat(order_form.a.value); - var n = parseFloat(order_form.n.value); - - var sz = a*(Math.sqrt(p/n) - 1); - var calcvol = a*(Math.sqrt(p*n)-p); - var actvol = -sz * n; - var extra = actvol - calcvol; - var accumsz = sz + extra/n; - - var otype = order_form.querySelector(".order_type"); - otype.classList.toggle("buy", sz > 0); - otype.classList.toggle("sell", sz < 0); - - order_form.querySelector(".order_size.acc0").innerText = adjNum(Math.abs(sz)); - order_form.querySelector(".order_size.acc100").innerText = adjNum(Math.abs(accumsz)); - order_form.querySelector(".norm_profit").innerText = adjNum(extra); - order_form.querySelector(".cash_flow").innerText = adjNum(actvol); - } - Array.prototype.forEach.call(order_form.elements,function(x){ - x.addEventListener("input", order_form_calc); - }); - - var range_form = document.getElementById("form_range_exchange"); - var range_form_calc = function() { - var aa = parseFloat(this.aa.value); - var ea = parseFloat(this.ea.value); - var am = parseFloat(this.am.value); - var p = parseFloat(this.p.value); - var ab = aa+ea; - var value = ab * p; - var max_price = pow2((ab * Math.sqrt(p))/ea); - var S = value - am; - var min_price = S<=0?0:pow2(S/(ab*Math.sqrt(p))); - var boost = ab/aa; - - update_ranges(this, min_price, max_price, boost) - }.bind(range_form); - Array.prototype.forEach.call(range_form.elements,function(x){ - x.addEventListener("input", range_form_calc); - }); - - var range_form_margin = document.getElementById("form_range_margin"); - var range_form_margin_calc = function() { - var aa = parseFloat(this.aa.value); - var ea = parseFloat(this.ea.value); - var am = parseFloat(this.am.value); - var p = parseFloat(this.p.value); - var l = parseFloat(this.l.value); - var ab = aa+ea; - var colateral = am* (1 - 1 / l); - var min_price = (ab*p - 2*Math.sqrt(ab*colateral*p) + colateral)/ab; - var max_price = (ab*p + 2*Math.sqrt(ab*colateral*p) + colateral)/ab; - var boost = ab * p / colateral; - - update_ranges(this, min_price, max_price, boost) - }.bind(range_form_margin); - - Array.prototype.forEach.call(range_form_margin.elements,function(x){ - x.addEventListener("input", range_form_margin_calc); - }); - - var form_range_futures = document.getElementById("form_range_futures"); - var form_range_futures_calc = function() { - var aa = parseFloat(this.aa.value); - var ea = parseFloat(this.ea.value); - var am = parseFloat(this.am.value); - var p = 1.0/parseFloat(this.p.value); - var l = parseFloat(this.l.value); - var ab = aa+ea; - var colateral = am* (1 - 1 / l); - var max_price = 1.0/((ab*p - 2*Math.sqrt(ab*colateral*p) + colateral)/ab); - var min_price = 1.0/((ab*p + 2*Math.sqrt(ab*colateral*p) + colateral)/ab); - var boost = ab * p / colateral; - - update_ranges(this, min_price, max_price, boost) - }.bind(form_range_futures); - - Array.prototype.forEach.call(form_range_futures.elements,function(x){ - x.addEventListener("input", form_range_futures_calc); - }); - - var form_sliding = document.getElementById("form_sliding_pos"); - var form_sliding_calc = function() { - var data_id = form_sliding.data.value; - var data = source_data.charts[data_id]; - var ea = parseFloat(form_sliding.ea.value); - var ga = parseFloat(form_sliding.ga.value); - var fb = parseFloat(form_sliding.fb.value); - var mlt = parseFloat(form_sliding.mlt.value)*0.01+1; - var mos = parseFloat(form_sliding.mos.value); - var inv = source_data.info[data_id].inverted; - if (inv) eq = 1/eq; - var pp = inv?1/data[0].price:data[0].price; - var eq = pp; - var pl = 0; - var mdd = 0; - var mpos = 0; - var pos = 0; - var norm = 0; - var newchart = []; - var tframe = ga*3600000; - var tm = data[0].time; - var eaa = ea; - data.forEach(function(x) { - var p = inv?1/x.price:x.price; - var dr = Math.sign(p - pp); - var pldiff = pos * (p - pp); - var tmdiff = x.time - tm; - var neq = pldiff*Math.sign(tframe)>0?eq + (p - eq) * (tmdiff/Math.abs(tframe)):eq; - if (Math.abs(pos) > mpos) mpos = Math.abs(pos); - var nxpos = ea*Math.sqrt(eq/p)-ea; - var dpos = nxpos - pos ; - var maxpos = ea * fb * 0.01; - var mult = (maxpos - Math.abs(nxpos))/maxpos*mlt; - if (mult < 0.000001) mult = 0.000001 - if (dpos * dr >= -mos) { - if (mos <= 0) return; - dpos = -mos * dr - } - pos = pos + dpos * mult; - pp = p; - eq = neq; - tm = x.time; - pl = pl+pldiff; - norm = pl+pos*(eq-Math.sqrt(p*eq)); - if (pl < mdd) mdd = pl; - newchart.push({ - price:x.price, - pl:pl, - pln:norm, - np:inv?1/eq:eq, - time:x.time, - achg:(inv?-1:1)*dpos - }); - }); - if (inv) eq = 1/eq; - - - form_sliding.querySelector(".pl").innerText = adjNum(pl); - form_sliding.querySelector(".pln").innerText = adjNum(norm); - form_sliding.querySelector(".pos").innerText = adjNum((inv?-1:1)*pos); - form_sliding.querySelector(".mdd").innerText = adjNum(mdd); - form_sliding.querySelector(".maxpos").innerText = adjNum(mpos); - form_sliding.querySelector(".eq").innerText = adjNum(eq); - drawChart(form_sliding.querySelector(".chart1"),newchart,"price",[],"np"); - drawChart(form_sliding.querySelector(".chart2"),newchart,"pl",[],"pln"); - drawChart(form_sliding.querySelector(".chart3"),newchart,"achg",[]); - }; - - Array.prototype.forEach.call(form_sliding.elements,function(x){ - x.addEventListener("input", form_sliding_calc); - }); - - - calc.querySelector(".close_butt").addEventListener("click",function() { - calc.classList.toggle("fade",true); - setTimeout(function(){ - calc.hidden = true; - },500); - }); - - - document.getElementById("calculator_icon").addEventListener("click",function(){ - calc.classList.toggle("fade",true); - setTimeout(function(){ - calc.hidden = false; - },5); - setTimeout(function(){ - calc.classList.toggle("fade",false); - },10); - form_sliding.data.innerText = ""; - for (var i in source_data.info) { - var opt = document.createElement("option"); - opt.innerText = source_data.info[i].title; - opt.value = i; - form_sliding.data.appendChild(opt); - } - }); -} - function donate() { var w = document.getElementById("donate_window"); @@ -1231,6 +895,9 @@ function donate() { hd.checked = localStorage["donation_hidden"] === hd.value; } } +function setup() { + location.href="admin/index.html"; +} function close_donate() { var w = document.getElementById("donate_window"); w.classList.remove("shown"); diff --git a/www/res/common.js b/www/res/common.js new file mode 100644 index 00000000..0918c883 --- /dev/null +++ b/www/res/common.js @@ -0,0 +1,398 @@ +"use strict"; + + +function beginOfDay(dt) { + return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()); +} + + +function fetch_json(file, opt) { + return fetch(file, opt).then(function(req) { + if (req.status>299 || req.status < 200) throw req; + return req.json(); + }); +} + +function invPrice(v, inv) { + return inv?1/v:v; +} +function invSize(v, inv) { + return (inv?-1:1)*v; +} + +function adjNum(n, decimals) { + if (typeof n != "number") return n; + if (isNaN(n)) return "---"; + if (!isFinite(n)) { + if (s < 0) return "-∞"; + else return "∞"; + } + if (decimals !== undefined) return n.toFixed(decimals); + var an = Math.abs(n); + if (an >= 100000) return n.toFixed(0); + else if (an >= 1) return n.toFixed(2); + else if (an > 0.0001) return n.toFixed(6); + else { + var s = (n*1000000).toFixed(3); + if (s == "0.000") return s; + return s+"µ"; + } +} + +function adjNumN(n) { + if (typeof n != "number") return n; + var an = Math.abs(n); + if (an >= 100000) return n.toFixed(0); + else if (an >= 1) return n.toFixed(2); + else if (an > 0.0001) return n.toFixed(6); + else { + if (an === 0) return an; + else { + var s = n.toFixed(10); + while (s[s.length-1] == '0') { + s = s.substr(0,s.length-1); + } + return s; + } + } + +} + +function new_svg_el(name, attrs, childOf) { + var elem = document.createElementNS("http://www.w3.org/2000/svg", name); + if (attrs) {for (var i in attrs) { + elem.setAttributeNS(null,i, attrs[i]); + }} + if (childOf) childOf.appendChild(elem); + return elem; +} + +function pow2(x) { + return x*x; +} + +function calculateBacktest(params) { + +var data = params.data; +var ea = params.external_assets; +var ga = params.sliding_pos; +var fb = params.fade; +var mlt = params.multiplicator || 1; +var mos = params.min_order_size || 0; +var inv = params.invert || false; +var et = params.expected_trend || 0; +var pos = (inv?-1:1)*params.start_pos || 0; +var mxs = params.max_order_size || 0; +var max_pos = params.max_pos || 0; + +if (data.length == 0) return { + pl:0, + pln: 0, + pos: 0, + mdd:0, + maxpos:0, + eq:0, + chart:[] +}; +var pp = inv?1/data[0].price:data[0].price; +var eq = pp*pow2((ea+pos)/ea); +var pl = 0; +var mdd = 0; +var mpos = 0; +var norm = 0; +var tframe = ga*3600000; +var tm = data[0].time; +var newchart = []; +var curet = 0; + +data.forEach(function(x) { + var p = inv?1/x.price:x.price; + var dr = Math.sign(p - pp); + var pldiff = pos * (p - pp); + var tmdiff = x.time - tm; + if (tmdiff > Math.abs(tframe)) tmdiff = Math.abs(tframe); + var neq = pldiff*Math.sign(tframe)>0?eq + (p - eq) * (tmdiff/Math.abs(tframe)):eq; + if (Math.abs(pos) > mpos) mpos = Math.abs(pos); + var nxpos = ea*Math.sqrt(eq*(1+curet)/p)-ea; + var dpos = nxpos - pos ; + var mult = (fb?(fb - Math.abs(nxpos))/fb:1); + if (mult < 0.000001) mult = 0.000001 + if (pos * dpos < 0) { + mult = 1; + } + curet = curet + (et - curet)*0.05; + dpos = dpos * mult * mlt; + if (mxs && dpos * dr < -mxs) { + dpos = -mxs * dr + } + if (dpos * dr >= -mos) { + if (mos <= 0) return; + dpos = -mos * dr + } + if (params.step) dpos = Math.floor(dpos / params.step)*params.step; + pos = pos + dpos; + if (max_pos && Math.abs(pos) > max_pos) { + pos = max_pos * Math.sign(pos); + neq = p*pow2((ea+pos)/ea); + } + pp = p; + eq = neq; + tm = x.time; + pl = pl+pldiff; + norm = pl+pos*(eq-Math.sqrt(p*eq)); + if (pl < mdd) mdd = pl; + newchart.push({ + price:x.price, + pl:pl, + pos:pos, + pln:norm, + np:inv?1/eq:eq, + time:x.time, + achg:(inv?-1:1)*dpos + }); +}); +if (inv) eq = 1/eq; + +return { + pl:pl, + pln: norm, + pos: (inv?-1:1)*pos, + mdd:mdd, + maxpos:mpos, + eq:eq, + chart:newchart +}; +} + + +function initChart(chart_interval, ratio, base_interval) { + + if (!ratio) ratio = 3; + if (!base_interval) base_interval=1200000; + + + + + return function (elem, chart, fld, lines, fld2) { + "use strict"; + + chart = chart.filter(function(x) {return fld in x}); + + elem.innerText = ""; + if (chart.length == 0) return; + + var now = new Date(chart[chart.length-1].time); + var bday = beginOfDay(now); + var skiphours = Math.floor((now - bday)/(base_interval)); + + + var daystep = 24*3600000/base_interval; + var step = 2*864000000/chart_interval; + var activewidth=step*chart_interval/base_interval; + var dattextstep = Math.ceil(120/(daystep*step)); + var activeheight = (activewidth/ratio)|0; + var minmax = chart.concat(lines?lines:[]).reduce(function(c,x) { + if (c.min > x[fld]) c.min = x[fld]; + if (c.max < x[fld]) c.max = x[fld]; + return c; + },{min:chart[0][fld],max:chart[0][fld]}); + minmax.sz = minmax.max - minmax.min; + minmax.min = minmax.min - minmax.sz*0.05; + minmax.max = minmax.max + minmax.sz*0.05; + if (minmax.min == minmax.max) {minmax.min = -10; minmax.max = 10;} + var priceStep = activeheight/(minmax.max-minmax.min); + var axis = 2; + var label = 20; + var rowstep = Math.pow(10,Math.floor(Math.log10((minmax.max-minmax.min)/3))); + var rowbeg= Math.floor(minmax.min/rowstep); + var rowend= Math.floor(minmax.max/rowstep); + + var svg = new_svg_el("svg",{viewBox:"0 0 "+(activewidth+axis)+" "+(activeheight+axis+label)},elem); + new_svg_el("line",{x1:axis, y1:0,x2:axis,y2:activeheight+axis,class:"lineaxis"},svg); + new_svg_el("line",{x1:0, y1:activeheight+1,x2:activewidth+axis,y2:activeheight+1,class:"lineaxis"},svg); + var cnt = chart.length; + function map_y(p) { + return activeheight-(p-minmax.min)*priceStep; + } + function map_x(x) { + return x*step+axis; + } + + if (!isFinite(rowbeg) || !isFinite(rowend)) return; + + for (var i = rowbeg; i <=rowend; i++) { + var v = i*rowstep; + var maj = Math.abs(v) 0) { + var xtm = map_x(xtmpos); + new_svg_el("line",{x1:xtm,y1:0,x2:xtm,y2:activeheight,class:"minoraxe"},svg); + xtmpos-=daystep; + } + xtmpos = activewidth/step-skiphours; + while (xtmpos > 0) { + var xtm = map_x(xtmpos); + new_svg_el("text",{x:xtm,y:activeheight,class:"textaxisx"},svg).appendChild(document.createTextNode(bday.toLocaleDateString())); + bday.setDate(bday.getDate()-dattextstep); + new_svg_el("line",{x1:xtm,y1:activeheight-5,x2:xtm,y2:activeheight,class:"majoraxe"},svg); + xtmpos-=daystep*dattextstep; + } + var tmstart=(now/base_interval-activewidth/step) + for (var i = 0; i 0) { + last2 = array2[pos2]; + res.push(fn(1,last2, last1, array1[pos1])); + pos2++; + } else { + last1 = array1[pos1]; + last2 = array2[pos2]; + res.push(fn(0,last1, last2, last2)); + pos1++; + pos2++; + } + } + while (pos1 < cnt1) { + last1 = array1[pos1]; + res.push(fn(0,last1, last2, null)); + pos1++; + } + while (pos2 < cnt2) { + last2 = array2[pos2]; + res.push(fn(1,last2, last1, null)); + pos2++; + } + return res; +} + +function interpolate(beg, end, cur, a, b) { + var n = (cur - beg)/(end - beg); + return a+(b-a)*n; +} + + +function formBuilder(format) { + var items = format.map(function(itm) { + var el; + var lb = { + tag:"span", + text: itm.label + }; + + switch (itm.type) { + case "string": el ={tag:"input", + attrs: { + type:"text", + value:itm.default || "" + }};break; + case "number": el ={tag:"input", + attrs: { + type:"number", + step:"any", + value:itm.default + }};break; + case "textarea": el ={tag:"textarea", + text:itm.default || "", + attrs: { + rows:itm.rows || "5", + + }};break; + case "enum": el = {tag:"select", + attrs:{}, + content: Object.keys(itm.options).map(function(k){ + var attrs = {}; + if (k == itm.default) attrs.selected = "selected"; + attrs.value = k; + return {tag:"option", + attrs: attrs, + text: itm.options[k] + + } + })}; + break; + + default: + el = {tag:"span",text:"unknown: "+itm.type}; + break; + } + if (el.attrs && itm.name) el.attrs["data-name"]=itm.name; + return { + tag:"label", + content:[lb, el] + } + }); + var w = TemplateJS.View.fromTemplate({tag:"x-form",content:items}); + return w; +} + + diff --git a/www/res/delete.png b/www/res/delete.png new file mode 100644 index 00000000..5c223530 Binary files /dev/null and b/www/res/delete.png differ diff --git a/www/res/logout.png b/www/res/logout.png new file mode 100644 index 00000000..6d49c203 Binary files /dev/null and b/www/res/logout.png differ diff --git a/www/res/options.png b/www/res/options.png new file mode 100644 index 00000000..bb389506 Binary files /dev/null and b/www/res/options.png differ diff --git a/www/res/repair.png b/www/res/repair.png new file mode 100644 index 00000000..4f30c706 Binary files /dev/null and b/www/res/repair.png differ diff --git a/www/res/reset_stats.png b/www/res/reset_stats.png new file mode 100644 index 00000000..69f512af Binary files /dev/null and b/www/res/reset_stats.png differ diff --git a/www/res/restart_active.png b/www/res/restart_active.png new file mode 100644 index 00000000..59a20026 Binary files /dev/null and b/www/res/restart_active.png differ diff --git a/www/res/save_icon.png b/www/res/save_icon.png new file mode 100644 index 00000000..4f1fa90c Binary files /dev/null and b/www/res/save_icon.png differ diff --git a/www/res/security.png b/www/res/security.png new file mode 100644 index 00000000..84737965 Binary files /dev/null and b/www/res/security.png differ diff --git a/www/res/setup.png b/www/res/setup.png new file mode 100644 index 00000000..d8d45ef3 Binary files /dev/null and b/www/res/setup.png differ diff --git a/www/res/style.css b/www/res/style.css index c32c19f9..8bc19d07 100644 --- a/www/res/style.css +++ b/www/res/style.css @@ -13,99 +13,6 @@ a { color: #AAF; } -.stdline { - stroke: #FF8; - stroke-width: 2px; -} - -.stdline2 { - stroke: #afdeff; - stroke-width: 2px; -} - - -.minoraxe { - stroke: #888; - stroke-opacity: 0.5; -} - -.marker.buy { - fill: #4F4; -} - -.marker.sell { - fill: #F44; -} -line.marker.buy { - stroke:#4f4; - stroke-width:3px; -} -line.marker.sell { - stroke:#F44; - stroke-width:3px; -} -.majoraxe { - stroke: #FFF; -} - - -.lineaxis { - stroke: #888; - stroke-width: 1px; -} - -.textaxis { - vertical-align: center; - text-anchor: begin; - alignment-baseline: after-edge; - font-size: 20px; - fill: #888; -} - -.textaxisx { - text-anchor: middle; - alignment-baseline: before-edge; - font-size: 20px; - fill: #888; -} - -.textaxis.bottom { - alignment-baseline: after-edge; -} -.orderline { - stroke: #FAA; - stroke-width:2; - stroke-dasharray:4; -} - -.orderline.buy{ - stroke: #AFA; -} - -.orderline.sell{ - stroke: #FAA; -} -.orderline.last{ - stroke: #AAF; -} - -.orderline.current{ - stroke: #f4ff88; -} - -.textorderline { - font-size: 27px; - fill: #FFF; - alignment-baseline: after-edge; - font-style: italic; - text-shadow: 0 0 10px black; - font-weight: bold; -} - -.textorderline.current { - fill: #f4ff88; - font-size: 20px; -} .selchart { position: sticky; @@ -173,7 +80,7 @@ select > option[disabled] { margin: 0.2cm 0.2cm 0 0.2cm; flex-shrink: 1; flex-grow: 1; - min-width: 40%; + /* min-width: 40%; */ overflow: auto; } @@ -342,9 +249,15 @@ x-trbgr.hdr { .head { display: flex; align-items: flex-start; - /* overflow-x: auto; */ - /* flex-wrap: wrap; */ - /* justify-content: space-between; */ +} + +.head >.title > img { + height: 1.0em; + vertical-align: middle; + padding: 0 0 0.1em 0; +} +.head >.title.emulated > img { + filter: sepia(1); } .summary::-webkit-scrollbar{ @@ -355,6 +268,8 @@ x-trbgr.hdr { font-weight:bold; font-size: 1.2em; cursor: pointer; + white-space: nowrap; + } .title:hover, #home:hover { @@ -615,7 +530,6 @@ x-tr.hdr [data-name=export_csv] { position:relative; /* padding-right: 9px; */ color: #dcb684; - white-space: nowrap; } .emulated::after { content:""; @@ -785,8 +699,9 @@ form button { margin-top: 0.5cm; } -#calculator_icon {width: 0.5cm;height: 0.5cm;position: absolute;right: 0.95cm;top: 0.3cm;background-image: url(calculator.svg);background-size: cover;cursor: p;} -#donate_icon {width: 0.8cm;height: 0.8cm;position: absolute;right: 1.6cm;top: 0.15cm;background-image: url(donate_sml.svg);background-size: cover;} +#calculator_icon {width: 0.5cm;height: 0.5cm;position: absolute;right: 1.72cm;top: 0.3cm;background-image: url(calculator.svg);background-size: cover;cursor: pointer;} +#setup_icon {width: 0.5cm;height: 0.5cm;position: absolute;right: 0.95cm;top: 0.3cm;background-image: url(setup.png);background-size: cover;cursor: pointer;} +#donate_icon {width: 0.8cm;height: 0.8cm;position: absolute;right: 1.7cm;top: 0.15cm;background-image: url(donate_sml.svg);background-size: cover;cursor: pointer;} #calculator { transition: all 0.3s; @@ -895,6 +810,12 @@ table.extended td .color.pos::before {content: "+";} [onclick] { cursor: pointer } +table.perfmod { + width: 100%; +} +table.perfmod td{ + text-align: center; +} .head > .error_icon { width: 20px; @@ -1004,4 +925,6 @@ table.extended td .color.pos::before {content: "+";} #form_sliding_pos .second { color: #afdeff; -} \ No newline at end of file +} + + diff --git a/www/res/template.js b/www/res/template.js new file mode 100644 index 00000000..cb47267a --- /dev/null +++ b/www/res/template.js @@ -0,0 +1,1430 @@ + +///declare namespace TemplateJS +var TemplateJS = function(){ + "use strict"; + + ///registers to an event for once fire - returns promise + /** + * @param element element which to register + * @param name of the event similar to addEventListener + * @param arg arguments passed to the promise when event is fired + * + * @return Promise object which is resolved once the event triggers + */ + function once(element, event, args) { + + return new Promise(function(ok) { + + function fire(z) { + element.removeEventListener(event, fire, args); + ok(z); + } + element.addEventListener(event, fire, args); + }); + }; + + ///Creates a promise which is resolved after some tome + /** + * @param time time in milliseconds (same as setTimeout) + * @param arg argument passed to the promise + * @return a Promise resolved after specified time + */ + function delay(time, arg) { + return new Promise(function(ok) { + setTimeout(function() { + ok(arg); + },time); + }); + }; + + + + ///Creates a promise which is resolved once the specified element is added to the DOM + /** @param elem element to monitor + * @param arg arguments passed to the promise once the element is added to the DOM + * @param timeout count of seconds to wait for render. Default is 10 seconds + * @return a Promise resolved once the element is rendered + * + * @note The function takes strong reference to the element. To avoid memory leak, there is a timeout + * in which the element must be rendered (a.k.a. put to the DOM) otherwise, the Promise is rejected + * + * You can specify timeout by the timeout argument. Note that the timeout is not exactly in seconds. It + * defines count of DOM changes rounds, where each round contains all changes made during 1 seconds + * So if there is no activity in the DOM, the counter is stopped. + * This better accomodiate waiting to slow DOM changes and animations, which can cover an + * animations which takes longer than 10 seconds to play especially, when whole animation is made by CSS + * and no other DOM changes are made during the play + */ + function waitForRender(elem, arg, timeout){ + if (!timeout) timeout = 10; + if (elem.isConnected) return Promise.resolve(arg); + init_waitForRender(); + return new Promise(function(ok, err){ + + waitForRender_list.push({ + elem:elem, + fn:ok, + err:err, + arg:arg, + time:Date.now(), + timeouts: timeout, + st:true + }); + }); + + }; + + ///Creates a promise which is resolved once the specified element is removed from the DOM + /** @param elem element to monitor + * @param arg argument + * @param timeout specify timeout. If missing, function will never timeout + * @return a Promise resolved once the element is removed + * + * @note function is useful to emulate destructor when the particular element + * is removed from the DOM + */ + function waitForRemove(elem, arg, timeout) { + if (timeout === undefined) timeout = null; + if (!elem.isConnected) return Promise.resolve(arg); + init_waitForRender(); + return new Promise(function(ok, err){ + + waitForRender_list.push({ + elem:elem, + fn:ok, + err:err, + arg:arg, + time:Date.now(), + timeouts: timeout, + st:false + }); + }); + + } + + function init_waitForRender() { + if (waitForRender_observer == null) { + waitForRender_observer = new MutationObserver(waitForRender_callback); + waitForRender_observer.observe(document, + {attributes: false, + childList: true, + characterData: false, + subtree:true}); + } + } + + var waitForRender_list = []; + var waitForRender_observer = null; + var waitForRender_callback = function() { + if (waitForRender_list.length == 0) { + waitForRender_observer.disconnect(); + waitForRender_observer = null; + } else { + var tm = Date.now(); + waitForRender_list = waitForRender_list.reduce(function(acc,x){ + if (x.elem.isConnected == x.st) { + x.fn(x.arg); + } else if (x.timeouts !== null) { + if (tm - x.time > 1000) { + x.time = tm; + if (--x.timeouts <= 0) { + x.err(new Error("waitForRender/waitForRemove timeout")); + return acc; + } + } + acc.push(x); + } else { + acc.push(x); + } + return acc; + },[]); + } + }; + + + + + function Animation(elem) { + this.elem = elem; + + var computed = window.getComputedStyle(elem, null); + if (computed.animationDuration != "0" && computed.animationDuration != "0s") { + this.type = this.ANIMATION; + this.dur = computed.animationDuration; + } else if (computed.transitionDuration != "0" && computed.transitionDuration != "0s") { + this.type = this.TRANSITION; + this.dur = computed.transitionDuration; + } else { + this.type = this.NOANIM; + this.dur = "0s"; + } + if (this.dur.endsWith("ms")) this.durms = parseFloat(this.dur); + else if (this.dur.endsWith("s")) this.durms = parseFloat(this.dur)*1000; + else if (this.dur.endsWith("m")) this.durms = parseFloat(this.dur)*60000; + else this.durms = 1000; + } + Animation.prototype.ANIMATION = 1; + Animation.prototype.TRANSITION = 2; + Animation.prototype.NOANIM = 0; + + Animation.prototype.isAnimated = function() { + return this.type != this.NOANIM; + } + Animation.prototype.isTransition = function() { + return this.type == this.TRANSITION; + } + Animation.prototype.isAnimation = function() { + return this.type == this.ANIMATION; + } + + Animation.prototype.restart = function() { + var parent = this.elem.parentElement; + var next = this.elem.nextSibling; + parent.insertBefore(this.elem, next); + } + + Animation.prototype.wait = function(arg) { + var res; + switch (this.type) { + case this.ANIMATION: res = Promise.race([delay(this.durms),once(this.elem,"animationend")]);break; + case this.TRANSITION: res = Promise.race([delay(this.durms),once(this.elem,"transitionend")]);break; + default: + case this.NOTHING:res = Promise.resolve();break; + } + if (arg !== undefined) { + return res.then(function(){return arg;}); + } else { + return res; + } + } + + ///removes element from the DOM, but it plays "close" animation before removal + /** + * @param element element to remove + * @param skip_anim remove element immediately, do not play animation (optional) + * @return function returns Promise which resolves once the element is removed + */ + function removeElement(element, skip_anim) { + if (!element.isConnected) return Promise.resolve(); + if (element.dataset.closeAnim && !skip_anim) { + var remopen = element.dataset.openAnim; + if (remopen && !element.classList.contains(remopen)) { + return removeElement(element,true); + } + var closeAnim = element.dataset.closeAnim; + return waitForDOMUpdate().then(function() { + if (remopen) + element.classList.remove(remopen); + element.classList.add(closeAnim); + var anim = new Animation(element); + if (anim.isAnimation()) + anim.restart(); + return anim.wait(); + }).then(function() { + return removeElement(element,true); + }) + } else { + var event = new Event("remove"); + element.parentElement.removeChild(element); + element.dispatchEvent(event); + return Promise.resolve(); + } + } + + function waitForDOMUpdate() { + return new Promise(function(ok) { + window.requestAnimationFrame(function() { + window.requestAnimationFrame(ok); + }); + }) + } + + function addElement(parent, element, before) { + if (before === undefined) before = null; + if (element.dataset.closeAnim) { + element.classList.remove(element.dataset.closeAnim); + } + element.classList.remove(element.dataset.openAnim); + parent.insertBefore(element,before); + window.getComputedStyle(element); + if (element.dataset.openAnim) { + waitForDOMUpdate().then(function() { + element.classList.add(element.dataset.openAnim); + }); + } + } + + function createElement(def) { + if (typeof def == "string") { + return document.createElement(def); + } else if (typeof def == "object") { + if ("tag" in def) { + var elem = document.createElement(def.tag); + var attrs = def.attrs || def.attributes; + if (typeof attrs == "object") { + for (var i in attrs) { + elem.setAttribute(i,attrs[i]); + } + } + if ("html" in def) { + elem.innerHTML=def.html; + } else if ("text" in def) { + elem.appendChild(document.createTextNode(def.text)); + } else { + var content = def.content || def.value || def.inner; + if (content !== undefined) { + elem.appendChild(loadTemplate(content)); + } + } + return elem; + } else if ("text" in def) { + return document.createTextNode(def.text); + } + } + return document.createElement("div"); + } + + function loadTemplate(templateID) { + var tempel; + if (typeof templateID == "string") { + tempel = document.getElementById(templateID); + if (!tempel) { + throw new Error("Template element doesn't exists: "+templateID); + } + } else if (typeof templateID == "object") { + if (templateID instanceof Element) { + tempel = templateID; + } else if (Array.isArray(templateID)) { + return templateID.reduce(function(accum,item){ + var x = loadTemplate(item); + if (accum === null) accum = x; else accum.appendChild(x); + return accum; + },document.createDocumentFragment()); + } else { + return createElement(templateID); + } + } + var cloned; + if ("content" in tempel) { + cloned = document.importNode(tempel.content,true); + } else { + cloned = document.createDocumentFragment(); + var x= tempel.firstChild; + while (x) { + cloned.appendChild(x.cloneNode(true)); + x = x.nextSibling; + } + } + return cloned; + + } + + + function View(elem) { + if (typeof elem == "string") elem = document.getElementById(elem); + this.root = elem; + this.marked =[]; + this.groups =[]; + this.rebuildMap(); + //apply any animation now + if (this.root.dataset && this.root.dataset.openAnim) { + this.root.classList.add(this.root.dataset.openAnim); + } + + }; + + + ///Get root element of the view + View.prototype.getRoot = function() { + return this.root; + } + + ///Replace content of the view + /** + * @param elem element which is put into the view. It can be also instance of View + */ + View.prototype.setContent = function(elem) { + if (elem instanceof View) + return this.setContent(elem.getRoot()); + this.clearContent(); + this.defaultAction = null; + this.cancelAction = null; + this.root.appendChild(elem); + this.rebuildMap(); + }; + + ///Replace content of the view generated from the template + /** + * @param templateRef ID of the template + */ + View.prototype.loadTemplate = function(templateRef) { + this.setContent(loadTemplate(templateRef)); + } + + View.prototype.replace = function(view, skip_wait) { + + if (this.lock_replace) { + view.lock_replace = this.lock_replace = this.lock_replace.then(function(v) { + delete view.lock_replace + return v.replace(view,skip_wait); + }); + return this.lock_replace; + } + + var nx = this.getRoot().nextSibling; + var parent = this.getRoot().parentElement; + var newelm = view.getRoot(); + + view.modal_elem = this.modal_elem; + delete this.modal_elem; + + if (!skip_wait) { + var mark = document.createComment("#"); + parent.insertBefore(mark,nx); + view.lock_replace = this.close().then(function(){ + addElement(parent,view.getRoot(), mark); + parent.removeChild(mark); + delete view.lock_replace; + return view; + }); + return view.lock_replace; + } else { + this.close(); + addElement(parent,view.getRoot(),nx); + return Promise.resolve(view); + } + } + ///Visibility state - whole view is hidden + View.HIDDEN = 0; + ///Visibility state - whole view is visible + View.VISIBLE = 1; + ///Visibility state - whole view is hidden, but still occupies area (transparent) + View.TRANSPARENT=-1 + + View.prototype.setVisibility = function(vis_state) { + if (vis_state == View.VISIBLE) { + this.root.hidden = false; + this.root.style.visibility = ""; + } else if (vis_state == View.TRANSPARENT) { + this.root.hidden = false; + this.root.style.visibility = "hidden"; + } else { + this.root.hidden = true; + } + } + + View.prototype.show = function() { + this.setVisibility(View.VISIBLE); + } + + View.prototype.hide = function() { + this.setVisibility(View.HIDDEN); + } + + ///Closes the view by unmapping it from the doom + /** The view can be remapped through the setConent or open() + * + * @param skip_anim set true to skip any possible closing animation + * + * @return function returns promise once the view is closed, this is useful especially when + * there is closing animation + * + * */ + View.prototype.close = function(skip_anim) { + return removeElement(this.root).then(function() { + if (this.modal_elem && this.modal_elem.isConnected) + this.modal_elem.parentElement.removeChild(this.modal_elem); + }.bind(this)); + } + + ///Opens the view as toplevel window + /** @note visual of toplevel window must be achieved through styles. + * This function just only adds the view to root of page + * + * @param elem (optional)if specified, the view is opened under specified element + * + * @note function also installs focus handler allowing focus cycling by TAB key + */ + View.prototype.open = function(elem) { + if (!elem) elem = document.body; + addElement(elem,this.root); + this._installFocusHandler(); + } + + + ///Opens the view as modal window + /** + * Append lightbox which prevents accesing background of the window + * + * @note function also installs focus handler allowing focus cycling by TAB key + */ + View.prototype.openModal = function() { + if (this.modal_elem) return; + var lb = this.modal_elem = document.createElement("light-box"); + if (View.lightbox_class) lb.classList.add(View.lightbox_class); + else lb.setAttribute("style", "display:block;position:fixed;left:0;top:0;width:100vw;height:100vh;"+View.lightbox_style); + document.body.appendChild(lb); + this.open(); + // this.setFirstTabElement() + } + + View.clearContent = function(element) { + var event = new Event("remove"); + var x = element.firstChild + while (x) { + var y = x.nextSibling; + element.removeChild(x); + x.dispatchEvent(event) + x = y; + } + } + + View.prototype.clearContent = function() { + View.clearContent(this.root); + this.byName = {}; + }; + + ///Creates view at element specified by its name + /**@param name name of the element used as root of View + * + * @note view is not registered as collection, so it is not accessible from the parent + * view though the findElements() function. However inner items are still visible directly + * on parent view. + */ + View.prototype.createView = function(name) { + var elem = this.findElements(name); + if (!elem) throw new Error("Cannot find item "+name); + if (elem.length != 1) throw new Error("The element must be unique "+name); + var view = new View(elem[0]); + return view; + }; + + ///Creates collection at given element + /** + * @param selector which defines where collection is created. If there are multiple + * elements matching the selector, they are all registered as collection. + * @param name new name of collection. If the selector is also name of existing + * item, then this argument is ignored, because function replaces item by collection + * + * @note you don't need to call this function if you make collection by adding [] after + * the name + */ + View.prototype.createCollection = function(selector, name) { + var elems = this.findElements(selector); + if (typeof selector == "string" && this.byName[selector]) name = selector; + var res = elems.reduce(function(sum, item){ + var x = new GroupManager(item, name); + this.groups.push(x); + sum.push(x); + return sum; + },[]); + this.byName[name] = res; + }; + + ///Returns the name of class used for the mark() and unmark() + /** + * If you need to use different name, you have to override this value + */ + View.prototype.markClass = "mark"; + + ///Finds elements specified by selector or name + /** + * @param selector can be either a string or an array. If the string is specified, then + * the sting can be either name of the element(group), which is specified by data-name or name + * or it can be a CSS selector if it starts by dot ('.'), hash ('#') or brace ('['). It + * can also start by $ to specify, that rest of the string is complete CSS selector, including + * a tag name ('$tagname'). If the selector is array, then only last item can be selector. Other + * items are names of collections as the function searches for the elements inside of + * collections where the argument specifies a search path (['group-name','index1','index2','item']) + * + * @note if `index1` is null, then all collections of given name are searched. if `index2` is + * null, then result is all elements matching given selector for all items in the collection. This + * is useful especially when item is name, because searching by CSS selector is faster if + * achieveded directly from root + * + * + */ + View.prototype.findElements = function(selector) { + if (typeof selector == "string") { + if (selector) { + var firstChar =selector.charAt(0); + switch (firstChar) { + case '.': + case '[': + case '#': return Array.from(this.root.querySelectorAll(selector)); + case '$': return Array.from(this.root.querySelectorAll(selector.substr(1))); + default: return selector in this.byName?this.byName[selector]:[]; + } + } else { + return [this.root]; + } + } else if (Array.isArray(selector)) { + if (selector.length==1) { + return this.findElements(selector[0]); + } + if (selector.length) { + var gg = this.byName[selector.shift()]; + if (gg) { + var idx = selector.shift(); + if (idx === null) { + return gg.reduce(function(sum,item){ + if (item.findElements) + sum.push.apply(sum,item.findElements(selector)); + return sum; + },[]); + } else { + var g = gg[idx]; + if (g && g.findElements) { + return g.findElements(selector); + } + } + } + } + } else if (typeof selector == "object" && selector instanceof Element) { + return [selector]; + } + return []; + } + + + ///Marks every element specified as CSS selector with a mark + /** + * The mark class is stored in variable markClass. + * This function is useful to mark elements for various purposes. For example if + * you need to highlight an error code, you can use selectors equal to error code. It + * will mark all elements that contain anything relate to that error code. Marked + * elements can be highlighted, or there can be hidden message which is exposed once + * it is marked + * + */ + View.prototype.mark = function(selector) { + var items = this.findElements(selector); + var cnt = items.length; + for (var i = 0; i < cnt; i++) { + items[i].classList.add(this.markClass); + this.marked.push(items[i]); + } + }; + + + View.prototype.forEachElement = function(selector, fn, a, b) { + var items = this.findElements(selector); + items.forEach(fn, a, b); + } + + + ///Removes all marks + /** Useful to remove any highlight in the View + */ + View.prototype.unmark = function() { + var cnt = this.marked.length; + for (var i = 0; i < cnt; i++) { + this.marked[i].classList.remove(this.markClass); + } + this.marked = []; + }; + + View.prototype.anyMarked = function() { + return this.marked.length > 0; + } + ///Installs keyboard handler for keys ESC and ENTER + /** + * This function is called by setDefaultAction or setCancelAction, do not call directly + */ + View.prototype._installKbdHandler = function() { + if (this.kbdHandler) return; + this.kbdHandler = function(ev) { + var x = ev.which || ev.keyCode; + if (x == 13 && this.defaultAction && ev.target.tagName != "TEXTAREA" && ev.target.tagName != "BUTTON") { + if (this.defaultAction(this)) { + ev.preventDefault(); + ev.stopPropagation(); + } + } else if (x == 27 && this.cancelAction) { + if (this.cancelAction(this)) { + ev.preventDefault(); + ev.stopPropagation(); + } + } + }.bind(this); + this.root.addEventListener("keydown", this.kbdHandler); + }; + + ///Sets function for default action + /** Default action is action called when user presses ENTER. + * + * @param fn a function called on default action. The function receives reference to + * the view as first argument. The function must return true to preven propagation + * of the event + * @param el_name optional, if set, corresponding element receives click event for default action + * (button OK in dialog) + * + * The most common default action is to validate and sumbit entered data + */ + View.prototype.setDefaultAction = function(fn, el_name) { + this.defaultAction = fn; + this._installKbdHandler(); + if (el_name) { + var data = {}; + data[el_name] = {"!click":fn}; + this.setData(data) + } + }; + + ///Sets function for cancel action + /** Cancel action is action called when user presses ESC. + * + * @param fn a function called on cancel action. The function receives reference to + * the view as first argument. The function must return true to preven propagation + * of the event + + * @param el_name optional, if set, corresponding element receives click event for default action + * (button CANCEL in dialog) + * + * The most common cancel action is to reset form or to exit current activity without + * saving the data + */ + View.prototype.setCancelAction = function(fn, el_name) { + this.cancelAction = fn; + this._installKbdHandler(); + if (el_name) { + var data = {}; + data[el_name] = {"!click":fn}; + this.setData(data) + } + }; + + function walkDOM(el, fn) { + var c = el.firstChild; + while (c) { + fn(c); + walkDOM(c,fn); + c = c.nextSibling; + } + } + + ///Installs focus handler + /** Function is called from setFirstTabElement, do not call directly */ + View.prototype._installFocusHandler = function(fn) { + if (this.focus_top && this.focus_bottom) { + if (this.focus_top.isConnected && this.focus_bottom.isConnected) + this.focus_top.focus(); + return; + } + var focusHandler = function(where, ev) { + setTimeout(function() { + where.focus(); + },10); + }; + + var highestTabIndex=null; + var lowestTabIndex=null; + var firstElement=null; + var lastElement = null; + walkDOM(this.root,function(x){ + if (typeof x.tabIndex == "number" && x.tabIndex != -1) { + if (highestTabIndex===null) { + highestTabIndex = lowestTabIndex = x.tabIndex; + firstElement = x; + } else { + if (x.tabIndex >highestTabIndex) highestTabIndex = x.tabIndex; + else if (x.tabIndex <"); + this.idmap={}; + this.result = []; + this.curOrder =[]; + this.parent.insertBefore(this.anchor, this.baseEl); + this.parent.removeChild(this.baseEl); + this.name = name; + template_el.dataset.group=true; + template_el.removeAttribute("data-name"); + template_el.removeAttribute("name"); + + } + + GroupManager.prototype.isConnectedTo = function(elem) { + return elem.contains(this.anchor); + } + + GroupManager.prototype.begin = function() { + this.result = []; + this.newOrder = []; + } + + + GroupManager.prototype.setValue = function(id, data) { + var x = this.idmap[id]; + if (!x) { + var newel = this.baseEl.cloneNode(true); + var newview = new View(newel); + x = this.idmap[id] = newview; + } else { + this.lastElem = x.getRoot(); + } + this.newOrder.push(id); + var t = data["@template"]; + if (t) { + x.loadTemplate(t); + } + var res = x.setData(data); + if (res) + this.result.push(res); + } + + GroupManager.prototype.findElements = function(selector) { + var item = selector.shift(); + if (item === null) { + var res = []; + for (var x in this.idmap) { + res.push.apply(res,this.idmap[x].findElements(selector)); + } + return res; + } else { + return this.idmap[item]?this.idmap[item].findElements(selector):[]; + } + } + + GroupManager.prototype.finish = function() { + var newidmap = {}; + this.newOrder.forEach(function(x){ + if (this.idmap[x]) { + newidmap[x] = this.idmap[x]; + delete this.idmap[x]; + } else { + throw new Error("Duplicate row id: "+x); + + } + },this); + var oldp = 0; + var oldlen = this.curOrder.length; + var newp = 0; + var newlen = this.newOrder.length; + var ep = this.anchor.nextSibling; + var movedid = {}; + while (oldp < oldlen) { + var oldid = this.curOrder[oldp]; + var newid = this.newOrder[newp]; + if (oldid in this.idmap) { + oldp++; + ep = this.idmap[oldid].getRoot().nextSibling; + } else if (oldid == newid) { + oldp++; + newp++; + ep = newidmap[oldid].getRoot().nextSibling; + } else if (!movedid[oldid]) { + this.parent.insertBefore(newidmap[newid].getRoot(),ep); + newp++; + movedid[newid] = true; + } else { + oldp++; + } + } + while (newp < newlen) { + var newid = this.newOrder[newp]; + this.parent.insertBefore(newidmap[newid].getRoot(),ep); + newp++; + } + for (var x in this.idmap) { + try { + this.idmap[x].close(); + } catch (e) { + + } + } + + this.idmap = newidmap; + this.curOrder = this.newOrder; + this.newOrder = []; + return this.result; + + } + + GroupManager.prototype.readData = function() { + + var out = []; + for (var x in this.idmap) { + var d = this.idmap[x].readData(); + d["@id"] = x; + out.push(d); + } + return out; + + } + + ///enables items + /** + * @param name name of item + * @param enable true/false whether item has to be enabled + */ + View.prototype.enableItem = function(name, enable) { + var d = {}; + d[name] = {"disabled":enable?null:""}; + this.setData(d); + } + + ///show or hide item + /** + * @param name name of item + * @param showCmd true/false to show or hide item, or you can use constants View.VISIBLE,View.HIDDEN and View.TRANSPARENT + */ + View.prototype.showItem = function(name, showCmd) { + var d = {}; + if (typeof showCmd == "boolean") { + this.showItem(name,showCmd?View.VISIBLE:View.HIDDEN); + }else { + if (showCmd == View.VISIBLE) { + d[name] = {".hidden":false,".style.visibility":""}; + } else if (showCmd == View.TRANSPARENT) { + d[name] = {".hidden":false,".style.visibility":"hidden"}; + } else { + d[name] = {".hidden":true}; + } + } + this.setData(d); + } + + ///sets an event procedure to the item + /** + * @param name name of item + * @param event name of event procedure + * @param fn function. To remove event procedure, specify null + * + * @note it is faster to set the event procedure through setData along with other items + */ + View.prototype.setItemEvent = function(name, event, fn) { + var d = {} + var evdef = {}; + evdef["!"+event] = fn; + d[name] = evdef; + this.setData(d); + + } + + View.prototype.setItemValue = function(name, value) { + var d = {}; + d[name] = {value:value} + this.setData(d); + } + + View.prototype.loadItemTemplate = function(name, template_name) { + var v = View.createFromTemplate(template_name); + this.setItemValue(name, v); + return v; + } + + View.prototype.clearItem = function(name) { + this.setItemValue(name, null); + } + + ///Rebuilds map of elements + /** + * This function is called in various situations especialy, after content of the + * View has been changed. The function must be called manually to register + * any new field added by function outside of the View. + * + * After the map is builtm, you can access the elements through the variable byName["name"], + * Please do not modify the map manually + */ + View.prototype.rebuildMap = function(rootel) { + if (!rootel) rootel = this.root; + this.byName = {}; + + this.groups = this.groups.filter(function(x) {return x.isConnectedTo(rootel);}); + this.groups.forEach(function(x) {this.byName[x.name] = [x];},this); + + function checkSubgroup(el) { + while (el && el != rootel) { + if (el.dataset.group) return true; + el = el.parentElement; + } + return false; + } + + var elems = rootel.querySelectorAll("[data-name],[name]"); + var cnt = elems.length; + var i; + for (i = 0; i < cnt; i++) { + var pl = elems[i]; + if (rootel.contains(pl) && !checkSubgroup(pl)) { + var name = pl.name || pl.dataset.name || pl.getAttribute("name"); + name.split(" ").forEach(function(vname) { + if (vname) { + if (vname && vname.endsWith("[]")) { + vname = vname.substr(0,name.length-2); + var gm = new GroupManager(pl, vname); + this.groups.push(gm); + if (!Array.isArray(this.byName[vname])) this.byName[vname] = []; + this.byName[vname].push(gm); + } else{ + if (!Array.isArray(this.byName[vname])) this.byName[vname] = []; + this.byName[vname].push(pl); + } + } + },this); + + } + } + } + + ///Sets data in the view + /** + * @param structured data. Promise can be used as value, the value is rendered when the promise + * is resolved + * + * @return Returns Promise which becomes resolved once ale items are set to they + * controls. The delay can happen, when one of the values is a Promise. + */ + View.prototype.setData = function(data) { + var me = this; + var results = []; + + function checkSpecialValue(val, elem) { + if (val instanceof Element) { + View.clearContent(elem) + elem.appendChild(val); + return true; + } else if (val instanceof View) { + View.clearContent(elem) + elem.appendChild(val.getRoot()); + return true; + } else if (val instanceof Date && elem.type == "date") { + elem.valueAsDate = val; + return true; + } + + } + + function isPromise(v) { + return (typeof v == "object" && v instanceof Promise); + } + + + function processItem(itm, elemArr, val) { + var out = []; + elemArr.forEach(function(elem) { + var res /* = undefined*/; + if (elem) { + var eltype = elem.tagName; + if (elem.dataset && elem.dataset.type) eltype = elem.dataset.type; + var customEl = eltype && View.customElements[eltype.toUpperCase()]; + if (typeof val == "object" && val !== null) { + if (checkSpecialValue(val,elem)) { + return + } else if (!Array.isArray(val)) { + if (!customEl || !customEl.setAttrs || !customEl.setAttrs(elem,val)) { + updateElementAttributes(elem,val); + } + if (!("value" in val)) { + return; + }else { + val = val.value; + if (typeof val == "object" && checkSpecialValue(val,elem)) return; + } + } + } + if (elem instanceof GroupManager) { + var group = elem; + group.begin(); + if (Array.isArray(val) ) { + var i = 0; + var cnt = val.length; + for (i = 0; i < cnt; i++) { + var id = val[i]["@id"] || i; + group.setValue(id, val[i]); + } + } + res = group.finish(); + out.push.apply(out,res); + } else { + function render_val(val) { + if (customEl) { + return customEl.setValue(elem,val); + } else { + return updateBasicElement(elem, val); + } + } + if (val !== undefined) { + if (isPromise(val)) res = val.then(render_val); + else res = render_val(val); + } + if (res) + out.push(res);; + } + } + }); + return out; + + } + + for (var itm in data) { + var elemArr = this.findElements(itm); + if (elemArr) { + var val = data[itm]; + if (isPromise(val)) { + results.push(val.then(processItem.bind(this,itm,elemArr))); + } else { + var r = processItem(itm,elemArr,val); + results.push.apply(results,r); + } + } + } + return Promise.all(results); + } + + var event_handlers = new WeakMap(); + + function updateElementAttributes (elem,val) { + for (var itm in val) { + if (itm == "value") continue; + if (itm == "classList" && typeof val[itm] == "object") { + for (var x in val[itm]) { + if (val[itm][x]) elem.classList.add(x); + else elem.classList.remove(x); + } + } else if (itm.substr(0,1) == "!") { + var name = itm.substr(1); + var fn = val[itm]; + var eh = event_handlers.get(elem); + if (!eh) eh = {}; + if (eh[name]) { + var reg = eh[name]; + elem.removeEventListener(name,reg); + } + eh[name] = fn; + elem.addEventListener(name, fn); + event_handlers.set(elem,eh); + } else if (itm.substr(0,1) == ".") { + var name = itm.substr(1); + var obj = elem; + var nextobj; + var idx; + var subkey; + while ((idx = name.indexOf(".")) != -1) { + subkey = name.substr(0,idx); + nextobj = obj[subkey]; + if (nextobj == undefined) { + if (v !== undefined) nextobj = obj[subkey] = {}; + else return; + } + name = name.substr(idx+1); + obj = nextobj; + } + var v = val[itm]; + if ( v === undefined) { + delete obj[name]; + } else { + obj[name] = v; + } + } else if (val[itm]===null) { + elem.removeAttribute(itm); + } else { + elem.setAttribute(itm, val[itm].toString()) + } + } + } + + function updateInputElement(elem, val) { + var type = elem.getAttribute("type"); + if (type == "checkbox" || type == "radio") { + if (typeof (val) == "boolean") { + elem.checked = !(!val); + } else if (Array.isArray(val)) { + elem.checked = val.indexOf(elem.value) != -1; + } else if (typeof (val) == "string") { + elem.checked = elem.value == val; + } + } else if (type == "date" && typeof val == "object" && val instanceof Date) { + elem.valueAsDate = val; + } else { + elem.value = val; + } + } + + + function updateSelectElement(elem, val) { + if (typeof val == "object") { + var curVal = elem.value; + View.clearContent(elem); + if (Array.isArray(val)) { + var i = 0; + var l = val.length; + while (i < l) { + var opt = document.createElement("option"); + opt.appendChild(document.createTextNode(val[i].toString())); + elem.appendChild(opt); + i++; + } + } else { + for (var itm in val) { + var opt = document.createElement("option"); + opt.appendChild(document.createTextNode(val[itm].toString())); + opt.setAttribute("value",itm); + elem.appendChild(opt); + } + } + elem.value = curVal; + } else { + elem.value = val; + } + } + + function updateBasicElement (elem, val) { + View.clearContent(elem); + if (val !== null && val !== undefined) { + elem.appendChild(document.createTextNode(val)); + } + } + + ///Reads data from the elements + /** + * For each named element, the field is created in result Object. If there + * are multiple values for the name, they are put to the array. + * + * Because many named elements are purposed to only display values and not enter + * values, you can mark such elements as data-readonly="1" + */ + View.prototype.readData = function(keys) { + if (typeof keys == "undefined") { + keys = Object.keys(this.byName); + } + var res = {}; + var me = this; + keys.forEach(function(itm) { + var elemArr = me.findElements(itm); + elemArr.forEach(function(elem){ + if (elem) { + if (elem instanceof GroupManager) { + var x = elem.readData(); + if (res[itm] === undefined) res[itm] = x; + else x.forEach(function(c){res[itm].push(c);}); + } else if (!elem.dataset || !elem.dataset.readonly) { + var val; + var eltype = elem.tagName; + if (elem.dataset.type) eltype = elem.dataset.type; + var eltypeuper = eltype.toUpperCase(); + if (View.customElements[eltypeuper]) { + val = View.customElements[eltypeuper].getValue(elem, res[itm]); + } else { + val = readBasicElement(elem,res[itm]); + } + if (typeof val != "undefined") { + res[itm] = val; + } + } + } + }); + }); + return res; + } + + function readInputElement(elem, curVal) { + var type = elem.getAttribute("type"); + if (type == "checkbox") { + if (!elem.hasAttribute("value")) { + return elem.checked; + } else { + if (!Array.isArray(curVal)) { + curVal = []; + } + if (elem.checked) { + curVal.push(elem.value); + } + return curVal; + } + } else if (type == "radio") { + if (elem.checked) return elem.value; + else return curVal; + } else if (type == "number") { + return elem.valueAsNumber; + } else if (type == "date") { + return elem.valueAsDate; + } else { + return elem.value; + } + } + function readSelectElement(elem) { + return elem.value; + } + + function readBasicElement(elem) { + var group = elem.template_js_group; + if (group) { + return group.readData(); + } else { + if (elem.contentEditable == "true" ) { + if (elem.dataset.format == "html") + return elem.innerHTML; + else + return elem.innerText; + } + } + } + + ///Registers custrom element + /** + * @param tagName name of the tag + * @param customElementObject new CustomElementEvents(setFunction(),getFunction()) + */ + View.regCustomElement = function(tagName, customElementObject) { + var upper = tagName.toUpperCase(); + View.customElements[upper] = customElementObject; + } + + ///Creates root View in current page + /** + * @param visibility of the view. Because the default value is View.HIDDEN, if called + * without arguments the view will be hidden and must be shown by the function show() + */ + View.createPageRoot = function(visibility /* = View.HIDDEN */) { + var elem = document.createElement(View.topLevelViewName); + document.body.appendChild(elem) + var view = new View(elem); + view.setVisibility(visibility); + return view; + } + + View.topLevelViewName = "div"; + + ///Creates view from template + /** + * @param id of template. The template must by a single-root template or extra tag will be created + * If you need to create from multi-root template, you need to specify definition of parent element + * @param def parent element definition, it could be single tag name, or object, which + * specifies "tag" as tagname and "attrs" which contains key=value attributes + * + * @return newly created view + */ + View.fromTemplate = function(id, def) { + var t = loadTemplate(id); + if (t.nodeType == Node.DOCUMENT_FRAGMENT_NODE) { + var x = t.firstElementChild; + if (x != null && x.nextElementSibling == null) { + t = x; + } else { + var el = createElement(def); + el.appendChild(t); + t = el; + } + } + return new View(t); + } + + View.createFromTemplate = View.fromTemplate; + + View.createEmpty = function(tagName, attrs) { + if (tagName === undefined) tagName = "div"; + var elem = document.createElement(tagName); + if (attrs) { + for (var v in attrs) { + elem.setAttribute(v, attrs[v]); + } + } + return new View(elem); + } + + function CustomElementEvents(setval,getval,setattrs) { + this.setValue = setval; + this.getValue = getval; + this.setAttrs = setattrs; + + } + + View.customElements = { + "INPUT":{ + "setValue":updateInputElement, + "getValue":readInputElement, + }, + "TEXTAREA":{ + "setValue":updateInputElement, + "getValue":readInputElement, + }, + "SELECT":{ + "setValue":updateSelectElement, + "getValue":readSelectElement, + }, + "IMG":{ + "setValue":function(elem,val) { + elem.setAttribute("src",val); + }, + "getValue":function(elem) { + elem.getAttribute("src"); + } + }, + "IFRAME":{ + "setValue":function(elem,val) { + elem.setAttribute("src",val); + }, + "getValue":function(elem) { + elem.getAttribute("src"); + } + } + }; + + ///Lightbox style, mostly color and opacity + View.lightbox_style = "background-color:black;opacity:0.25"; + ///Lightbox class, if defined, style is ignored + View.lightbox_class = ""; + + + + return { + "View":View, + "loadTemplate":loadTemplate, + "CustomElement":CustomElementEvents, + "once":once, + "delay":delay, + "Animation":Animation, + "removeElement":removeElement, + "addElement":addElement, + "waitForRender":waitForRender, + "waitForRemove":waitForRemove, + "waitForDOMUpdate":waitForDOMUpdate + }; + +}(); + + diff --git a/www/res/trading.png b/www/res/trading.png new file mode 100644 index 00000000..afb1966b Binary files /dev/null and b/www/res/trading.png differ diff --git a/www/res/undo.png b/www/res/undo.png new file mode 100644 index 00000000..24b039c3 Binary files /dev/null and b/www/res/undo.png differ diff --git a/www/sw.js b/www/sw.js index 76ace549..5e223aa5 100644 --- a/www/sw.js +++ b/www/sw.js @@ -1,5 +1,5 @@ var CACHE = 'cache-update-and-refresh'; -//serial 112312315 +//serial 23opwkpo232 self.addEventListener('install', function(evt) { console.log('The service worker is being installed.'); @@ -10,10 +10,13 @@ self.addEventListener('install', function(evt) { './index.html', './res/style.css', './res/code.js', + './res/common.js', './res/calculator.svg', './res/donate.svg', './res/donate_sml.svg', './res/logo.png', + './res/setup.png', + './res/chart.css', './res/icon64.png', 'https://fonts.googleapis.com/css?family=Ruda&display=swap', 'https://fonts.gstatic.com/s/ruda/v10/k3kfo8YQJOpFqnYdaObJ.woff2' @@ -35,6 +38,7 @@ self.addEventListener('fetch', function(evt) { if (evt.request.method != 'GET') return; if (evt.request.url.indexOf("report.json") != -1) return; + if (evt.request.url.indexOf("/admin/") != -1) return; var p = fromCache(evt.request); var q = p.then(function(x) {return x;}, function() {