diff --git a/.github/hooks/.prepare-commit-msg b/.github/hooks/.prepare-commit-msg new file mode 100755 index 000000000..bcfbba951 --- /dev/null +++ b/.github/hooks/.prepare-commit-msg @@ -0,0 +1,16 @@ +#!/bin/sh + +SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') + +BTC=$(./hello.sh 2> /dev/null | cowsay) + +printf "$(cat <<\CARLESGITTIP + +████ ███ To request new features or in case this commit breaks something for you, +████ ███ please, create a new github issue with all possible information for me, +▓███▀█▄ but never share your API Keys! +▒▓██ ███ +░▒▓█ ███ %s +CARLESGITTIP +)" "$SOB +$BTC" >> "$1"; diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..76a458bd9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,58 @@ +name: test + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@main + - uses: actions/setup-node@main + + - name: configure-g++ + run: | + sudo apt-get install g++-12 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 100 + sudo update-alternatives --install /usr/bin/x86_64-linux-gnu-g++ x86_64-linux-gnu-g++ /usr/bin/x86_64-linux-gnu-g++-12 100 + + - name: configure-gcov + run: | + sudo apt-get install libcapture-tiny-perl libdatetime-perl + sudo update-alternatives --install /usr/bin/gcov gcov /usr/bin/x86_64-linux-gnu-gcov-12 100 + + - name: configure-test + run: | + sudo apt-get install cowsay + sudo npm install -g npm-check-updates + + - name: install + run: KUNITS=1 make install lib K <<< 1 + + - name: check + run: | + make check | tr -d "#" | cowsay -nW80 -fduck + (cd /var/lib/K && ncu) | sed '/^\s*$/d' | cowsay -nW80 -fduck + + - name: test + run: | + make test-c | cowsay -nW160 -felephant || : + make test | sed '/^\s*$/d' | sed '/====/d' | cowsay -nW160 -fbud-frogs && test ${PIPESTATUS[0]} -eq 0 + + - name: coverage + run: | + lcov -o lcov.info -c -d . --ignore-errors inconsistent --gcov-tool /usr/bin/gcov || : #> /dev/null 2>&1 + lcov -o lcov.info -r lcov.info '/usr/*' '*/include/*' || : #> /dev/null 2>&1 + lcov -l lcov.info | cowsay -nW80 -fdefault + + - uses: coverallsapp/github-action@main + continue-on-error: true + with: + path-to-lcov: lcov.info + github-token: ${{ secrets.GITHUB_TOKEN }} + + - uses: codacy/codacy-coverage-reporter-action@master + continue-on-error: true + with: + coverage-reports: lcov.info + project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} diff --git a/.gitignore b/.gitignore index e0a6fbfef..af508d53c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,10 @@ -bower* -.DS_Store -node_modules -typings -*.js.map -*.idea -.baseDir.ts -tribeca/ -*.log.* -test/src -.tscache -tribeca.json -.vscode +/* +!/doc +!/etc +!/src +!/test +!/COPYING +!/LICENSE +!/Makefile +!/README.md +!/.git* \ No newline at end of file diff --git a/COPYING b/COPYING new file mode 100644 index 000000000..400f166bb --- /dev/null +++ b/COPYING @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Carles Tubio + +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. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index cbda9e3bd..000000000 --- a/Dockerfile +++ /dev/null @@ -1,66 +0,0 @@ -FROM node:5 -RUN apt-get update - -RUN apt-get install -y git - -RUN git clone https://github.com/michaelgrosner/tribeca.git - -WORKDIR tribeca - -RUN npm install -g grunt-cli typings forever -RUN npm install -RUN typings install -RUN grunt compile - -EXPOSE 3000 5000 - -# General config properties. Properties with `NULL` should be replaced with your own exchange account information. -ENV TRIBECA_MODE dev -ENV EXCHANGE null -ENV TradedPair BTC/USD -ENV WebClientUsername NULL -ENV WebClientPassword NULL -ENV WebClientListenPort 3000 -# IP to access mongo instance. If you are on a mac, run `boot2docker ip` and replace `tribeca-mongo`. -ENV MongoDbUrl mongodb://tribeca-mongo:27017/tribeca - -# DEV -## HitBtc -ENV HitBtcPullUrl http://demo-api.hitbtc.com -ENV HitBtcOrderEntryUrl ws://demo-api.hitbtc.com:8080 -ENV HitBtcMarketDataUrl ws://demo-api.hitbtc.com:80 -ENV HitBtcSocketIoUrl https://demo-api.hitbtc.com:8081 -ENV HitBtcApiKey NULL -ENV HitBtcSecret NULL -ENV HitBtcOrderDestination HitBtc -## Coinbase -ENV CoinbaseRestUrl https://api-public.sandbox.exchange.coinbase.com -ENV CoinbaseWebsocketUrl https://ws-feed-public.sandbox.exchange.coinbase.com -ENV CoinbasePassphrase NULL -ENV CoinbaseApiKey NULL -ENV CoinbaseSecret NULL -ENV CoinbaseOrderDestination Coinbase -## OkCoin -ENV OkCoinWsUrl wss://real.okcoin.com:10440/websocket/okcoinapi -ENV OkCoinHttpUrl https://www.okcoin.com/api/v1/ -ENV OkCoinApiKey NULL -ENV OkCoinSecretKey NULL -ENV OkCoinOrderDestination OkCoin -## Bitfinex -ENV BitfinexHttpUrl https://api.bitfinex.com/v1 -ENV BitfinexKey NULL -ENV BitfinexSecret NULL -ENV BitfinexOrderDestination Bitfinex - -# PROD - values provided for reference. -## HitBtc -#ENV HitBtcPullUrl http://api.hitbtc.com -#ENV HitBtcOrderEntryUrl wss://api.hitbtc.com:8080 -#ENV HitBtcMarketDataUrl ws://api.hitbtc.com:80 -#ENV HitBtcSocketIoUrl https://api.hitbtc.com:8081 -## Coinbase -#ENV CoinbaseRestUrl https://api.exchange.coinbase.com -#ENV CoinbaseWebsocketUrl wss://ws-feed.exchange.coinbase.com - -WORKDIR tribeca/service -CMD ["forever", "main.js"] diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 011613fbf..000000000 --- a/Gruntfile.js +++ /dev/null @@ -1,82 +0,0 @@ -module.exports = function (grunt) { - "use strict"; - - var commonFiles = "src/common/*.ts"; - var serviceFiles = ["src/service/**/*.ts", commonFiles]; - var adminFiles = ["src/admin/**/*.ts", commonFiles]; - var html = "src/static/**"; - - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - - watch: { - service: { - files: serviceFiles, - tasks: ['ts:service'] - }, - - client: { - files: adminFiles, - tasks: ['ts:admin', "copy", "browserify"] - }, - - static: { - files: html, - tasks: ['copy', "browserify"] - } - }, - - ts: { - options: { - sourceMap: false, - comments: false, // same as !removeComments. [true | false (default)] - declaration: false, // generate a declaration .d.ts file for every output js file. [true | false (default)] - fast: 'always' - }, - - service: { - src: serviceFiles, - outDir: 'tribeca', - options: { - target: 'es5', - module: 'commonjs' - } - }, - - admin: { - src: adminFiles, - outDir: 'tribeca/service/admin/js', - options: { - target: 'es5', - module: 'commonjs' - } - } - }, - - copy: { - main: { - expand: true, - cwd: "src/static", - src: "**", - dest: "tribeca/service/admin" - } - }, - - browserify: { - dist: { - files: { - "tribeca/service/admin/js/admin/bundle.min.js": ["tribeca/service/admin/js/admin/client.js"] - }, - } - } - }); - - grunt.loadNpmTasks("grunt-ts"); - grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks('grunt-contrib-copy'); - grunt.loadNpmTasks('grunt-browserify'); - - var compile = ["ts", "copy", "browserify"]; - grunt.registerTask("compile", compile); - grunt.registerTask("default", compile.concat(["watch"])); -}; diff --git a/LICENSE.md b/LICENSE similarity index 89% rename from LICENSE.md rename to LICENSE index 9f7c57b51..b07d6d29a 100644 --- a/LICENSE.md +++ b/LICENSE @@ -1,5 +1,5 @@ -Internet Systems Consortium license -=================================== +Internet Systems Consortium (ISC) license +========================================= Copyright (c) `2015`, `Michael Grosner` diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..efa24c1d3 --- /dev/null +++ b/Makefile @@ -0,0 +1,343 @@ +K ?= K.sh +MAJOR = 0 +MINOR = 7 +PATCH = 0 +BUILD = 55 + +OBLIGATORY = DISCLAIMER: This is strict non-violent software: \n$\ + if you hurt other living creatures, please stop; \n$\ + otherwise remove all copies of the software now. + +PERMISSIVE = This is free software: the UI and quoting engine are open source, \n$\ + feel free to hack both as you need. \n$\ + This is non-free software: built-in gateway exchange integrations \n$\ + are licensed by/under the law of my grandma (since last century), \n$\ + feel free to crack all as you need. + +SOURCE := $(filter-out trading-bot,$(notdir $(wildcard src/bin/*))) trading-bot +CARCH = x86_64-linux-gnu \ + arm-linux-gnueabihf \ + aarch64-linux-gnu \ + x86_64-apple-darwin23.5 \ + x86_64-w64-mingw32 + +CHOST ?= $(or $(findstring $(shell test -n "`command -v g++`" && g++ -dumpmachine), \ + $(CARCH)),$(subst build-,,$(firstword $(wildcard build-*)))) + +KHOST := $(shell echo $(CHOST) \ + | sed 's/-\([a-z_0-9]*\)-\(linux\)$$/-\2-\1/' \ + | sed 's/\([a-z_0-9]*\)-\([a-z_0-9]*\)-.*/\2-\1/' \ + | sed 's/^w64/win64/' ) +KBUILD := build-$(KHOST) +KHOME := $(if ${SYSTEMROOT},$(word 1,$(subst :, ,${SYSTEMROOT})):/,$(if \ + $(findstring $(CHOST),$(lastword $(CARCH))),C:/,/var/lib/))K + +ERR = *** K require g++ v12 or greater, but it was not found. +HINT := consider a symlink at /usr/bin/$(CHOST)-g++ pointing to your g++ executable +TPUT = $(if $(shell echo $${TERM}),$(shell tput $(1))) +STEP = $(call TPUT,setaf 2)$(call TPUT,setab 0)Building $(1)..$(call TPUT,sgr0) +SUDO = $(shell test -n "`command -v sudo`" && echo sudo) + +KARGS := -std=c++23 -O3 -pthread \ + -D'K_CHOST="$(KHOST)"' -D'K_SOURCE="K-$(KSRC)"' \ + -D'K_BUILD="v$(MAJOR).$(MINOR).$(PATCH)+$(BUILD)"' \ + -D'K_STAMP="$(shell date "+%Y-%m-%d %H:%M:%S")"' \ + -D'K_HOME="$(KHOME)"' -D'K_HEAD="$(shell \ + git rev-parse HEAD 2>/dev/null || echo HEAD \ + )"' -I$(KBUILD)/include \ + $(addprefix $(KBUILD)/lib/, \ + K-$(KHOST).a \ + libcurl.a libssl.a libcrypto.a libz.a libsqlite3.a \ + cacert_embed.o \ + ) \ + $(wildcard $(addprefix $(KBUILD)/lib/, \ + K-$(KSRC)-client.o \ + libuv.a \ + )) \ + $(addprefix -include , $(realpath \ + $(addprefix src/bin/$(KSRC)/$(KSRC)., \ + $(addsuffix .S, disk) \ + $(addsuffix .h, ansi) \ + ) \ + $(addprefix src/lib/Krypto.ninja-, \ + $(addsuffix .S, disk) \ + $(addsuffix .h, lang data apis bots) \ + ))) \ + -D'DEBUG_FRAMEWORK="Krypto.ninja-test.h"' \ + -D'DEBUG_SCENARIOS=<$(or \ + $(realpath src/bin/$(KSRC)/$(KSRC).test.h), \ + /dev/null \ + )>' \ + -D'using_Makefile(x)=<$(abspath \ + src/bin/$(KSRC) \ + )/using_\#\#x>' \ + -D'using_data=$(KSRC).data.h' \ + -D'using_main=$(KSRC).main.h' \ +-D'OBLIGATORY_analpaper_SOFTWARE_LICENSE="$(OBLIGATORY)"'\ +-D'PERMISSIVE_analpaper_SOFTWARE_LICENSE="$(PERMISSIVE)"' + +all K: $(SOURCE) + +hlep hepl help: + # # + # Available commands inside K top level directory: # + # make help - show this help # + # # + # make - compile K sources # + # make K - compile K sources # + # KALL=1 make K - compile K sources # + # make +portfolios - compile K sources # + # make hello-world - compile K sources # + # make scaling-bot - compile K sources # + # make stable--bot - compile K sources # + # make trading-bot - compile K sources # + # # + # make lib - compile K dependencies # + # KALL=1 make lib - compile K dependencies # + # make packages - provide K dependencies # + # make install - install K application # + # make docker - install K application # + # make reinstall - upgrade K application # + # make doc - compile K documentation # + # make test - run tests # + # make test-c - run static tests # + # # + # make list - show K instances # + # make start - start K instance # + # make startall - start K instances # + # make stop - stop K instance # + # make stopall - stop K instances # + # make restart - restart K instance # + # make restartall - restart K instances # + # # + # make diff - show commits and versions # + # make changelog - show commits # + # make upgrade - show commits and reinstall # + # # + # make download - download K src precompiled # + # make clean - remove external src files # + # KALL=1 make clean - remove external src files # + # make uninstall - remove /usr/local/bin/K-* # + # # + +doc test: + @$(MAKE) -sC $@ KHOME=$(KHOME) + +clean check lib: +ifdef KALL + unset KALL $(foreach chost,$(CARCH),&& $(MAKE) $@ CHOST=$(chost)) +else + $(if $(shell ver="`$(CHOST)-g++ -dumpversion | cut -d'-' -f1`" && test $${ver%%.*} -lt 12 && echo 1),$(warning $(ERR));$(error $(HINT))) + @$(MAKE) -C src/lib $@ CHOST=$(CHOST) KHOST=$(KHOST) KHOME=$(KHOME) +endif + +$(SOURCE): + $(info $(call STEP,$@)) + $(MAKE) $(shell ! test -d src/bin/$@/$@.client || echo client) src KSRC=$@ + +client: src/bin/$(KSRC)/$(KSRC).client + $(info $(call STEP,$(KSRC) $@)) + $(MAKE) -C src/lib/Krypto.ninja-client KHOME=$(KHOME) KCLIENT=$(realpath $<) + $(foreach chost,$(CARCH), \ + build=build-$(shell echo $(chost) | sed 's/-\([a-z_0-9]*\)-\(linux\)$$/-\2-\1/' | sed 's/\([a-z_0-9]*\)-\([a-z_0-9]*\)-.*/\2-\1/' | sed 's/^w64/win64/') \ + && ! test -d $${build} || (rm -rf $${build}/var/client && cp -R $(KHOME)/client $${build}/var/client \ + && $(MAKE) client.o CHOST=$(chost) chost=$(shell test -n "`command -v $(chost)-g++`" && echo $(chost)- || :) \ + && rm -rf $${build}/var/client) \ + ;) + rm -rf $(KHOME)/client + +client.o: src/bin/$(KSRC)/$(KSRC).disk.S + $(chost)g++ -Wa,-I,$(KBUILD)/var/client,-I,src/lib/Krypto.ninja-client \ + -include $^ -c src/lib/Krypto.ninja-disk.S -o $(KBUILD)/lib/K-$(KSRC)-$@ + +src: src/lib/Krypto.ninja-main.cxx src/bin/$(KSRC)/$(KSRC).main.h +ifdef KALL + unset KALL $(foreach chost,$(CARCH),&& $(MAKE) $@ CHOST=$(chost)) +else + $(info $(call STEP,$(KSRC) $@ $(CHOST))) + $(if $(shell ver="`$(CHOST)-g++ -dumpversion | cut -d'-' -f1`" && test $${ver%%.*} -lt 12 && echo 1),$(warning $(ERR));$(error $(HINT))) + @$(CHOST)-g++ --version + @mkdir -p $(KBUILD)/bin + $(MAKE) $(if $(findstring darwin,$(CHOST)),Darwin,$(if $(findstring mingw32,$(CHOST)),Win32,$(shell uname -s))) CHOST=$(CHOST) + @chmod +x $(KBUILD)/bin/K-$(KSRC)* + @$(if $(findstring $(CHOST),$(firstword $(CARCH))),$(MAKE) system_install -s) +endif + +Linux: src/lib/Krypto.ninja-main.cxx src/bin/$(KSRC)/$(KSRC).main.h +ifdef GITHUB_ACTIONS + @unset GITHUB_ACTIONS && $(MAKE) KCOV="--coverage" $@ +else ifdef KUNITS + @unset KUNITS && $(MAKE) KTEST="$(KCOV) -DCATCH_CONFIG_FAST_COMPILE test/unit_testing_framework.cxx" $@ +else ifndef KTEST + @$(MAKE) KTEST="-DNDEBUG" $@ +else + $(CHOST)-g++ -s $(KTEST) -o $(KBUILD)/bin/K-$(KSRC) \ + -static-libstdc++ -static-libgcc -rdynamic \ + $< $(KARGS) -z execstack -ldl -Wall -Wextra -Wno-psabi +endif + +Darwin: src/lib/Krypto.ninja-main.cxx src/bin/$(KSRC)/$(KSRC).main.h + $(CHOST)-g++ -s -DNDEBUG -o $(KBUILD)/bin/K-$(KSRC) -fvisibility=hidden -fvisibility-inlines-hidden \ + -msse4.1 -maes -mpclmul -mmacosx-version-min=10.13 -nostartfiles -rdynamic \ + $< $(KARGS) -ldl -framework SystemConfiguration -framework CoreFoundation + +Win32: src/lib/Krypto.ninja-main.cxx src/bin/$(KSRC)/$(KSRC).main.h + $(CHOST)-g++-posix -s -DNDEBUG -o $(KBUILD)/bin/K-$(KSRC).exe \ + -DCURL_STATICLIB -DSIGUSR1=SIGABRT -DSIGPIPE=SIGABRT \ + $< $(KARGS) -static -lstdc++ -lgcc -lole32 -lbcrypt -lcrypt32 \ + -lpsapi -luserenv -liphlpapi -lwldap32 -lws2_32 -ldbghelp + +download: + curl -L https://github.com/ctubio/Krypto-trading-bot/releases/download/$(MAJOR).$(MINOR).x/K-$(MAJOR).$(MINOR).$(PATCH).$(BUILD)-$(KHOST).tar.gz | tar xz + @$(MAKE) system_install -s + @test -n "`ls *.sh 2>/dev/null`" || (cp etc/K.sh.dist K.sh && chmod +x K.sh && echo && echo NEW CONFIG FILE created at: && LS_COLORS="ex=40;92" CLICOLOR="Yes" ls $(shell ls --color > /dev/null 2>&1 && echo --color) -lah K.sh && echo) + +packages: + @test -n "`command -v apt-get`" && sudo apt-get -y install g++ build-essential automake autoconf libtool libxml2 libxml2-dev zlib1g-dev python curl gzip screen doxygen graphviz \ + || (test -n "`command -v yum`" && sudo yum -y install gcc-c++ automake autoconf libtool libxml2 libxml2-devel python curl gzip screen) \ + || (test -n "`command -v brew`" && (xcode-select --install || :) && (brew install automake autoconf libxml2 zlib python curl gzip screen proctools doxygen graphviz || brew upgrade || :)) \ + || (test -n "`command -v pacman`" && $(SUDO) pacman --noconfirm -S --needed base-devel libxml2 zlib curl python gzip) + +uninstall: + @$(foreach bin,$(addprefix /usr/local/bin/,$(notdir $(wildcard $(KBUILD)/bin/K-*))), $(SUDO) rm -v $(bin);) + +system_install: + $(info Checking if sudo is allowed at /usr/local/bin.. $(shell $(SUDO) mkdir -p /usr/local/bin && $(SUDO) ls -ld /usr/local/bin > /dev/null 2>&1 && echo OK || echo ERROR)) + $(info Checking if /usr/local/bin is already in your PATH.. $(if $(shell echo $$PATH | grep /usr/local/bin),OK)) + $(if $(shell echo $$PATH | grep /usr/local/bin),,$(info $(subst ..,,$(subst Building ,,$(call STEP,Warning! you MUST add /usr/local/bin to your PATH!))))) + $(info ) + $(info List of installed K binaries:) + @$(SUDO) cp -f $(wildcard $(KBUILD)/bin/K-$(KSRC)*) /usr/local/bin + @LS_COLORS="ex=40;92" CLICOLOR="Yes" ls $(shell ls --color > /dev/null 2>&1 && echo --color) -lah $(addprefix /usr/local/bin/,$(notdir $(wildcard $(KBUILD)/bin/K-$(KSRC)*))) + @echo + @$(SUDO) mkdir -p $(KHOME) + @$(SUDO) chown $(shell id -u) $(KHOME) + +install: + @seq `expr $${COLUMNS:-21} / 2` | sed 's/.*/=/' | xargs echo \ + && echo " _ __" \ + && echo "| |/ / v$(MAJOR).$(MINOR).$(PATCH)+$(BUILD)" \ + && echo "| ' /" && echo "| . \\ Select your (beloved) architecture" \ + && echo "|_|\\_\\ to download pre-compiled binaries:" && echo \ + && echo $(CARCH) | tr ' ' "\n" | cat -n && echo && echo "(Hint! uname says \"`uname -sm`\")" && echo \ + && read -p "[$(shell seq -s \\ `echo $(CARCH) | wc -w`)]: " chost && (test -n "`echo $(CARCH) | cut -d ' ' -f$${chost}`" \ + && ($(MAKE) download CHOST=`echo $(CARCH) | cut -d ' ' -f$${chost}`) || (echo && echo Unknown option selected.. abort.)) + +docker: download + @sed -i "/Usage/,+$(shell expr `cat K.sh | wc -l` - 16)d" K.sh + +reinstall: + @test -d .git && ((test -n "`git diff`" && (echo && echo !!Local changes will be lost!! press CTRL-C to abort. && echo && sleep 5) || :) \ + && git fetch && git merge FETCH_HEAD || (git reset FETCH_HEAD && git checkout .)) || curl -O krypto.ninja/Makefile + @$(MAKE) install + @echo && echo ..done! Please restart any running instance. + +screen-help: + $(if $(shell test -z "`command -v screen`" && echo 1),$(warning Please install screen using the package manager of your system.);$(error screen command not found)) + +list: screen-help + @screen -list || : + +restartall: + @$(MAKE) stopall -s + @sleep 3 + @$(MAKE) startall -s + @$(MAKE) list -s + +stopall: + ls -1 *.sh | cut -d / -f2 | cut -d \* -f1 | grep -v ^_ | xargs -I % $(MAKE) K=% stop -s + +startall: + ls -1 *.sh | cut -d / -f2 | cut -d \* -f1 | grep -v ^_ | xargs -I % sh -c 'sleep 2;$(MAKE) K=% start -s' + @$(MAKE) list -s + +restart: + @$(MAKE) stop -s + @sleep 3 + @$(MAKE) start -s + @$(MAKE) list -s + +stop: screen-help + @screen -XS $(K) quit && echo STOP $(K) DONE || : + +start: screen-help + @test -n "`screen -list | grep "\.$(K) ("`" \ + && (echo $(K) is already running.. && screen -list) \ + || (screen -dmS $(K) ./$(K) && echo START $(K) DONE) + +screen: screen-help + @test -n "`screen -list | grep "\.$(K) ("`" && ( \ + echo Detach screen hotkey: holding CTRL hit A then D \ + && sleep 2 && screen -r $(K)) || screen -list || : + +diff: .git + @_() { echo $$2 $$3 version: `git rev-parse $$1`; }; git remote update && _ @ Local running && _ @{u} Latest remote + @$(MAKE) changelog -s + +upgrade: .git diff + @_() { git rev-parse $$1; }; test `_ @` != `_ @{u}` && $(MAKE) reinstall || : + +changelog: .git + @_() { echo `git rev-parse $$1`; }; echo && git --no-pager log --graph --oneline @..@{u} && test `_ @` != `_ @{u}` || echo No need to upgrade, both versions are equal. + +test-c: +ifndef KSRC + @$(foreach src,$(SOURCE),$(MAKE) -s $@ KSRC=$(src);) +else + @pvs-studio-analyzer credentials PVS-Studio Free FREE-FREE-FREE-FREE > /dev/null 2>&1 + @pvs-studio-analyzer analyze -e src/bin/$(KSRC)/$(KSRC).test.h -e src/lib/Krypto.ninja-test.h -e $(KBUILD)/include --source-file test/static_code_analysis.cxx --cl-params $(KARGS) test/static_code_analysis.cxx 2> /dev/null && \ + (echo $(KSRC) `plog-converter -a GA:1,2 -t tasklist -o report.tasks PVS-Studio.log | tail -n+8 | sed '/Total messages/d'` && cat report.tasks | sed '/Help: The documentation/d' && rm report.tasks) || : + -@egrep ₿ src test -lR | xargs -r sed -i 's/₿/u20BF/g' + -@clang-tidy -header-filter=$(realpath src) -checks='modernize-*, -modernize-use-trailing-return-type, -modernize-use-nodiscard, -clang-diagnostic-unknown-warning-option, -modernize-avoid-c-arrays, -modernize-return-braced-init-list' test/static_code_analysis.cxx -- $(subst ++23,++20,$(KARGS)) 2> /dev/null + -@egrep u20BF src test -lR | xargs -r sed -i 's/u20BF/₿/g' + @rm -f PVS-Studio.log > /dev/null 2>&1 +endif + +push: + @date=`date` && (git diff || :) && git status && read -p "KMOD: " KMOD \ + && git add . && git commit -S -m "$${KMOD}" \ + && ((KALL=1 $(MAKE) K release && git push) || git reset HEAD^1) \ + && echo "\007" && echo $${date} && date + +MAJOR: + @sed -i "s/^\(MAJOR *=\).*$$/\1 $(shell expr $(MAJOR) + 1)/" Makefile + @sed -i "s/^\(MINOR *=\).*$$/\1 0/" Makefile + @sed -i "s/^\(PATCH *=\).*$$/\1 0/" Makefile + @sed -i "s/^\(BUILD *=\).*$$/\1 0/" Makefile + @$(MAKE) push + +MINOR: + @sed -i "s/^\(MINOR *=\).*$$/\1 $(shell expr $(MINOR) + 1)/" Makefile + @sed -i "s/^\(PATCH *=\).*$$/\1 0/" Makefile + @sed -i "s/^\(BUILD *=\).*$$/\1 0/" Makefile + @$(MAKE) push + +PATCH: + @sed -i "s/^\(PATCH *=\).*$$/\1 $(shell expr $(PATCH) + 1)/" Makefile + @sed -i "s/^\(BUILD *=\).*$$/\1 0/" Makefile + @$(MAKE) push + +BUILD: + @sed -i "s/^\(BUILD *=\).*$$/\1 $(shell expr $(BUILD) + 1)/" Makefile + @$(MAKE) push + +release: +ifdef KALL + unset KALL $(foreach chost,$(CARCH),&& $(MAKE) $@ CHOST=$(chost)) +else ifndef KTARGZ + @$(MAKE) KTARGZ="K-$(MAJOR).$(MINOR).$(PATCH).$(BUILD)-$(KHOST).tar.gz" $@ +else + @tar -cvzf $(KTARGZ) $(KBUILD)/bin/K-* $(KBUILD)/lib/K-* LICENSE COPYING README.md Makefile doc etc test src \ + && curl -s -n -H "Content-Type:application/octet-stream" -H "Authorization: token ${KRELEASE}" \ + --data-binary "@$(PWD)/$(KTARGZ)" "https://uploads.github.com/repos/ctubio/Krypto-trading-bot/releases/$(shell curl -s \ + https://api.github.com/repos/ctubio/Krypto-trading-bot/releases/latest | grep id | head -n1 | cut -d ' ' -f4 | cut -d ',' -f1 \ + )/assets?name=$(KTARGZ)" && rm -v $(KTARGZ) +endif + +md5: src + find src/lib -type f -exec md5sum "{}" + > src/lib.md5 + +asandwich: + @test "`whoami`" = "root" && echo OK || echo make it yourself! + +.PHONY: all K $(SOURCE) hlep hepl help doc test src client client.o clean check lib download screen-help list screen start stop restart startall stopall restartall packages system_install uninstall install docker reinstall diff upgrade changelog test-c push MAJOR MINOR PATCH BUILD release md5 asandwich diff --git a/README.md b/README.md index 1f9fa0986..2f28bf285 100644 --- a/README.md +++ b/README.md @@ -1,107 +1,491 @@ -# tribeca +

self reminder:
patience is the mother of science


\*\*\* REFUGEES WELCOME! \*\*\*
     \*\*\* FATAL ROUTES? \*\*\* +
+ +[![Release](https://img.shields.io/github/release/ctubio/Krypto-trading-bot.svg)](https://github.com/ctubio/Krypto-trading-bot/releases) +[![Platform](https://img.shields.io/badge/platform-unix--like-111111.svg)](https://www.gnu.org/) +[![g0t0 Counter](https://tinyurl.com/g0t0search)](https://tinyurl.com/g0t0docs) +[![Code Size](https://img.shields.io/github/languages/code-size/ctubio/Krypto-trading-bot.svg)](https://github.com/ctubio/Krypto-trading-bot) +[![Software License](https://img.shields.io/badge/license-ISC-551a8b.svg)](https://raw.githubusercontent.com/ctubio/Krypto-trading-bot/master/LICENSE) +[![Software License](https://img.shields.io/badge/license-MIT-551a8b.svg)](https://raw.githubusercontent.com/ctubio/Krypto-trading-bot/master/COPYING) + +[`K`](https://github.com/ctubio/Krypto-trading-bot) is a family of (very customizable) very low latency [market making](https://github.com/ctubio/Krypto-trading-bot/blob/master/doc/MANUAL.md#what-is-market-making) trading bots with a fully featured [web interface](https://github.com/ctubio/Krypto-trading-bot#web-ui).
It can place or cancel orders on [compatible exchanges](https://github.com/ctubio/Krypto-trading-bot#compatible-exchanges) in less than a few milliseconds per order on a decent machine. + +If you don't want to configure or hardcode your own trading strategies in your own machine,
+you can fund liquidity pools of automated market makers at [tinyman.org](https://tinyman.org/) (or at any other defi out there),
just remember: +- never write on any defi website your private keys (you have to sign transactions, not to share your wallet keys) +- never tell anyone on any chat your private keys (if you have questions, use a public forum and reject impostors) + +### Latest version at https://github.com/ctubio/Krypto-trading-bot + +[![Build Status](https://github.com/ctubio/Krypto-trading-bot/workflows/test/badge.svg)](https://github.com/ctubio/Krypto-trading-bot/actions) +[![Coverage Status](https://img.shields.io/coveralls/ctubio/Krypto-trading-bot/master.svg)](https://coveralls.io/r/ctubio/Krypto-trading-bot?branch=master) +[![Quality Status](https://img.shields.io/badge/review-clang--tidy%20+%20pvs-4cc61e.svg)](https://www.codacy.com/gh/ctubio/Krypto-trading-bot/dashboard) +[![Open Issues](https://img.shields.io/github/issues/ctubio/Krypto-trading-bot.svg)](https://github.com/ctubio/Krypto-trading-bot/issues) +[![Last Commit](https://img.shields.io/github/last-commit/ctubio/Krypto-trading-bot.svg)](https://github.com/ctubio/Krypto-trading-bot) +[![Downloads Last Releases](https://img.shields.io/github/downloads/ctubio/Krypto-trading-bot/total.svg?label=downloads%20last%20releases)](https://github.com/ctubio/Krypto-trading-bot) + +Our bots run on unix-like systems. Persistence is achieved through a built-in server-less SQLite C++ interface.
Data transfers are directly done from your machine to the exchange using the latest CURL and OpenSSL versions.
Installation in a dedicated [Debian](https://cdimage.debian.org/cdimage/release/current/), [Raspberry](https://www.raspberrypi.com/software/), [Red Hat](https://developers.redhat.com/products/rhel/download), [CentOS](https://www.centos.org/download/) or macOS instance without Docker is recommended. + +The web UI is compatible with most web browsers/resolutions, but Brave or Firefox at 1600px are recommended.
Doesn't require configuration of any web server (unless installed behind your own reverse proxy). + +
K-trading-bot (web UI + CLI) +to control a fully configurable high frequency trading engine, with all features suggested by the community:
+ +![trading-bot UI Preview](https://user-images.githubusercontent.com/1634027/44740469-d5c7ff00-aafa-11e8-9252-73b9c1283adb.png) +
+ +
K-+portfolios (web UI + CLI) +to show all balances from one exchange, with buttons to create, edit or cancel orders and links to go to markets:
+ +![+portfolios UI Preview](https://github.com/user-attachments/assets/e0e3ce7d-3388-45a9-a559-4ba239a9e880) +
+ +
K-hello-world (CLI) +to print the current value of a given currency to stdout:
+ +
+ _________________________________________
+/ Hello, WORLD!                           \
+|                                         |
+\ pssst.. 1.00000000 BTC = 56683.49 EUR.  /
+ -----------------------------------------
+        \   ^__^
+         \  (oo)\_______
+            (__)\       )\/\
+                ||----w |
+                ||     ||
+
+
+ +
K-scaling-bot, K-stable--bot (CLI) +to easy mod and start developing a new custom bot. +
+ +### Compatible Exchanges + +All currency pairs are supported (use `--list` argument to see all currently tradable pairs on a given exchange). + +
under maintenanceunder development or abandoned
Spot TradingCoinbase (fees)
REST + 2 WebSockets

Binance (fees)
Binance.US (fees)
REST + 1 WebSocket

BitMEX (fees)
REST + 1 WebSocket
Kraken (fees)
REST + 2 WebSockets

KuCoin (fees)
REST + 1 WebSocket

Bitfinex (fees)
Ethfinex (fees)
REST + 1 WebSocket
Gate.io (fees)
REST + 1 WebSocket

HitBTC (fees)
Bequant (fees)
REST + 2 WebSockets

Poloniex (fees)
REST + 1 WebSocket
Margin Tradingnonenone
+ +If you ask me, [](https://advanced.coinbase.com/join/KAME9XG) is the best and most secure by far, so here is my [referral link](https://advanced.coinbase.com/join/KAME9XG) for both of us to enjoy. + +In case you are looking for referral links to other exchanges, feel free to post a [new issue](https://github.com/ctubio/Krypto-trading-bot/issues/new?title=Referral%20link%20for%20%5Bexchange%5D) asking to other active users. + +## README +- Documentation + - [README](#readme) + - [MANUAL](https://github.com/ctubio/Krypto-trading-bot/blob/master/doc/MANUAL.md) +- Installation + - [Docker Installation](#docker-installation) + - [Windows Installation](#windows-installation) + - [Manual GIT Installation](#manual-git-installation) + - [Manual ZIP Installation](#manual-zip-installation) + - [Configuration After Manual Installation](#configuration-after-manual-installation) + - [Upgrade to the latest commit](#upgrade-to-the-latest-commit) + - [Multiple instances party time](#multiple-instances-party-time) +- Information + - [Compatible Exchanges](#compatible-exchanges) + - [Application Usage](#application-usage) + - [Web UI](#web-ui) + - [Databases](#databases) + - [Charts](#charts) + - [Cloud Hosting](#cloud-hosting) +- Development + - [Build notes](#build-notes) + - [Changelogs](#changelog) +- Humans and Milk Mammals + - [Unlock](#unlock) + - [Donations](#donations) + - [General Discussion](#general-discussion) + - [Very Special Thanks](#very-special-thanks-to) + - [Help](#help) + - [Issues](#issues) -[![Join the chat at https://gitter.im/michaelgrosner/tribeca](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/michaelgrosner/tribeca?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +### Docker Installation -`tribeca` is a very low latency cryptocurrency [market making](https://github.com/michaelgrosner/tribeca/wiki#what-is-market-making) trading bot with a full featured [web client](https://github.com/michaelgrosner/tribeca#web-ui), [backtester](https://github.com/michaelgrosner/tribeca/wiki#how-can-i-test-new-trading-strategies), and supports direct connectivity to [several cryptocoin exchanges](https://github.com/michaelgrosner/tribeca#configuration). On modern hardware, it can react to market data by placing and canceling orders in under a millisecond. +See [etc/Dockerfile](https://github.com/ctubio/Krypto-trading-bot/tree/master/etc#dockerfile) file. -![Web UI Preview](https://raw.githubusercontent.com/michaelgrosner/tribeca/master/docs/web_ui_preview.png) +### Windows Installation -Runs on the latest node.js (v5 or greater). Persistence is acheived using mongodb. Installation is recommended via Docker, but manual installation is also supported. +Before starting with a manual installation, ensure your target machine has Windows 7 or greater and [MSYS2](https://www.msys2.org/) installed. -### Docker Installation +Use MSYS2 Terminal to install `make` (with command `pacman -S make`), then proceed as usual with the installation. + +### Manual GIT Installation + +0. Ensure you agree to install collaborative non-free software (see [Unlock](#unlock) section). + +1. Ensure your target machine has `git` and `make` installed. + +2. Download it wherever you want (feel free to customize the suggested folder name K) and execute the installer: +``` + $ git clone ssh://git@github.com/ctubio/Krypto-trading-bot K + $ cd K + $ make install +``` + +3. Open and edit the config file `K.sh` in your favorite text editor: +``` + $ vim K.sh +``` + +To upgrade anytime see [Upgrade to the latest commit](#upgrade-to-the-latest-commit) section. + +### Manual ZIP Installation + +0. Ensure you agree to install collaborative non-free software (see [Unlock](#unlock) section). + +1. Ensure your target machine has `curl` and `make` installed. + +2. Download it wherever you want (feel free to customize the suggested folder name K) and execute the installer: +``` + $ mkdir K + $ cd K + $ curl -O krypto.ninja/Makefile + $ make install +``` + +3. Open and edit the config file `K.sh` in your favorite text editor: +``` + $ vim K.sh +``` + +To upgrade anytime to the latest release just run `make reinstall`. + +### Configuration After Manual Installation -1. Please install [docker](https://www.docker.com/) for your system before preceeding. Requires at least Docker 1.7.1. Mac/Windows only: Ensure boot2docker or docker-machine is set up, depending on Docker version. See [the docs](https://docs.docker.com/installation/mac/) for more help. +See [etc/K.sh.dist](https://github.com/ctubio/Krypto-trading-bot/blob/master/etc/K.sh.dist) file or better your own copy of `K.sh` file located in the top level path. -2. Set up mongodb. If you do not have a mongodb instance already running: `docker run -p 27017:27017 --name tribeca-mongo -d mongo`. +It just contains a few variables with examples. The very end of the file contains the code that starts the bot. -3. Copy the repository [Dockerfile](https://raw.githubusercontent.com/michaelgrosner/tribeca/master/Dockerfile) into a text editor. Change the environment variables to match your desired [configuration](https://github.com/michaelgrosner/tribeca#configuration). Input your exchange connectivity information, account information, and mongoDB credentials. +Once your config file is ready, you can execute it to start the bot: +``` + $ ./K.sh +``` -4. Save the Dockerfile, preferably in a secure location and in an empty directory. Build the image from the Dockerfile `docker build -t tribeca .` +Alternatively use `make start` to run `K.sh` in the background using [screen](https://kb.iu.edu/d/acuy) (to see the output, attach the screen with `make screen` [or run all at once with `make start screen`]). -5. Run the container `docker run -p 3000:3000 --link tribeca-mongo:mongo --name tribeca -d tribeca`. If you run `docker ps`, you should see tribeca and mongo containers running. +Feel free to run `make stop` or `make restart` anytime, and don't forget to [read the fucking manual](https://github.com/ctubio/Krypto-trading-bot/blob/master/doc/MANUAL.md). -### Manual Installation +Troubleshooting: -1. Ensure your target machine has node v5 (or greater) and mongoDB v3 or greater. Also, ensure Typescript 1.7.5, grunt, typings, and, optionally, forever are installed (`npm install -g grunt-cli typescript typings forever`). + * If there is no wallet data on a given exchange, double-check the currency symbols with `--list` argument. -2. Clone the repository. + Optional: -3. In the cloned repository directory, `npm install` and then `tsd install` to pull in all dependencies. + * See at least once `./K.sh --help` to trade or `make help` to develop. -4. Compile typescript to javascript via `grunt compile`. + * Use your own HTTP Basic Authentication credentials with `--user` and `--pass` arguments. -5. cd to the outputted JS files, in `tribeca/service`. + * Use your own SSL certificate with `--ssl-crt` and `--ssl-key` arguments.
Otherwise, the insecure built-in certificate is fully featured, but you may need to authorise it in your browser.
If you want to generate your own certificate see [SSL for internal usage](https://www.akadia.com/services/ssh_test_certificate.html).
In case you really want to use plain HTTP, use `--without-ssl` argument. -6. Create a `tribeca.json` file based off the provided `sample-dev-tribeca.json` or `sample-prod-tribeca.json` files and save it in the current directory. Modify the config keys (see [configuration](https://github.com/michaelgrosner/tribeca#configuration) section) and point the instance towards the running mongoDB instance. +### Upgrade to the latest commit -7. Set environmental variable TRIBECA_CONFIG_FILE to full path of tribeca.json +If you upgrade while having any instance running in the background, you will need to manually restart it using `make restart` or `make restartall` to start using the latest version. -8. Run `forever start main.js` to start the app. +#### Upgrade under Manual ZIP Installation: -### Configuration +Please run `make reinstall` to download the upgraded source and executable files. - * EXCHANGE - - 1. `coinbase` - uses the WebSocket API. Ensure the Coinbase-specific properties have been set with your correct account information if you are using the sandbox or live-trading environment. - - 2. `hitbtc` - WebSocket + socket.io API. Ensure the HitBtc-specific properties have been set with your correct account information if you are using the dev or prod environment. - - 3. `okcoin` - Websocket.Ensure the OKCoin-specific properties have been set with your correct account information. Production environment only. - - 4. `bitfinex` REST API only. Ensure the Bitfinex-specific properties have been filled out. REST API is not suitable to millisecond latency trading. Production environment only. - - 5. `null` - Test in-memory exchange. No exchange-specific config needed. - - * TRIBECA_MODE - - 1. `prod` - - 2. `dev` - - * MongoDbUrl - If you are on OS X, change "tribeca-mongo" in the URL to the output of `boot2docker ip` on your host machine. If you are running an existing mongoDB instance, replace the URL with the existing instance's URL. If you are running from a Linux machine and set up mongo in step 1, you should not have to modify anything. - - * TradedPair - The following currency pairs are supported on these exchanges: - - 1. `BTC/USD` - Coinbase, HitBtc, OkCoin, Null - - 2. `BTC/EUR` - Coinbase, HitBtc, Null - - 3. `BTC/GBP` - Coinbase, Null - - * WebClientUsername and WebClientPassword - Username and password for [web UI](https://github.com/michaelgrosner/tribeca#web-ui) access. If kept as `NULL`, no the web client will not require authentication (Not recommended at all!!) +#### Upgrade under Manual GIT Installation: -Input your exchange connectivity information, account information, and API keys in the config properties for the exchange you intend on trading on. +Feel free anytime to check if there are new upgrades with `make diff`. + +Once you decide that it is time to upgrade, execute `make upgrade` (or directly `make reinstall` to skip the validation of new commits). + +If you only use `git` to pull the latest source files from the remote branch, you will still need to upgrade or recompile your executable files. + +To not upgrade but instead recompile your own modified source files, use `make lib K` or just `make` (see [Build notes](#build-notes)). + +### Multiple instances party time + +Please note, an "instance" is in fact a `*.sh` config file; using a single machine with a single installation, you can run as many instances as `*.sh` files you have (limited by the available free RAM). + +You can list the current running instances with `make list`. + +If you haven't defined a config file, `make start`, `make screen`, `make stop` and `make restart` will use the default config file `K.sh`. + +To run multiple instances using a collection of config files: + +1. Create a new config file with `cp etc/K.sh.dist X.sh && chmod +x X.sh` (use `X.sh` or any name but keep `.sh` extension). + +2. Edit the new config file `vim X.sh` + +3. Run the new instance with `./X.sh` or to run in the background, use `K=X.sh make start`. To attach to the new instance's screen, use `K=X.sh make screen`. To stop the new instance, use `K=X.sh make stop` and to restart it, use `K=X.sh make restart`. The environment variable `K` specifies the filename of the config file that you want to use. + +4. Open in the web browser the different pages of the ports of the different running instances, or display the UI of all instances together in a single page using the MATRYOSHKA link in the footer (that can be predefined using the optional argument `--matryoshka=URL`). + +After multiple config files are setup, to control them all together instead of one by one, the commands `make startall`, `make stopall` and `make restartall` are also available, just remember that config files with a filename starting with underscore symbol "_" will be skipped. ### Application Usage -1. Open your web browser to connect to port 3000 of the machine running tribeca. If you're running tribeca locally on Mac/Windows on Docker, replace "localhost" with the address returned by `boot2docker ip`. +1. Open your web browser to connect to port `3000` (or your configured port number) of the machine running K. Using `localhost` or one of the public or private IPs of your machine (if you're running on Docker, use the IP address returned by `boot2docker ip`). -2. Read up on how to use tribeca and market making in the [wiki](https://github.com/michaelgrosner/tribeca/wiki). +2. Read up on how to use K and market making in the [manual](https://github.com/ctubio/Krypto-trading-bot/blob/master/doc/MANUAL.md). -3. Set up trading parameters to your liking in the web UI. Click the "BTC/USD" button so it is green to start making markets. +3. Use the web UI to change the quoting parameters. Click the big "BTC/USD" button to start making markets. Click it again to stop. When the button is green, the bot is actively placing orders. ### Web UI -Once `tribeca` is up and running, visit port `3000` of the machine on which it is running to view the admin view. There are inputs for quoting parameters, grids to display market orders, market trades, your trades, your order history, your positions, and a big button with the currency pair you are trading. When you're ready, click that button green to begin sending out quotes. The UI uses a healthy mixture of socket.io and angularjs. +Once `K` is up and running, visit port `3000` (or your configured port number) to access the UI (i.e. [https://localhost:3000](https://localhost:3000)). There are inputs for quoting parameters, grids to display market orders, market trades, your trades, your order history, your positions, and a big button with the currency pair you are trading. When you're ready, click that button green to begin sending out quotes. The UI uses angularjs hydrated with websockets observed with reactivexjs. + +### Databases + +Each currency pair of each exchange will use a different sqlite database file with [WAL mode](https://www.sqlite.org/wal.html) enabled. + +All database files are located at `/var/lib/K/db/K-*.db*`, outside the download folder to survive wild `rm -rf path/to/K` or reinstalls. + +You can copy any group of `*.db*` files to another machine when migrating or as a backup. + +If a database does not exist, the application will create it on boot; otherwise, it will use the existing one. + +To explore each database you can use https://github.com/sqlitebrowser/sqlitebrowser or a similar tool. + +To set a different database filename or to set an [in-memory database](https://sqlite.org/inmemorydb.html), use `--database=FILE` argument (see `--help`). + +Even if using an in-memory database, the quoting parameters are always loaded from and saved into the file database. + +### Charts + +The metrics are not saved anywhere, it is just UI data collected with a visibility retention of `n` hours (where `n` is the value of `profit` quoting parameter), to display over time: + + * Market Fair Value with High and Low Prices + * Trades Complete + * Target Position for BTC currency (TBP) + * Target Position for Fiat currency + * STDEV and EWMA values for Quote Protection and APR + * Amount available in wallet for buy + * Amount held in open trades for buy + * Amount available in wallet for sell + * Amount held in open trades for sell + * Total amount available and held at both sides in BTC currency + * Total amount available and held at both sides in Fiat currency + +### Cloud Hosting + +If you ask me, [](https://www.dreamhost.com/r.cgi?475987/cloud/) is a very nice web hosting company (awesome support team, awesome servers). Feel free to use this referral link to get a discount subtracted from my referral earnings (i'm a user since 2008). + +### Build notes + +Make sure your build machine has [node](https://nodejs.org/en/download/package-manager/) installed, also ensure `make lib` provides all dependencies without errors. + +To rebuild the application, see `make help` and choose a target (just `make` may be what you are looking for). + +Test units are executed before the application exits, only if the application was compiled with `KUNITS=1 make`. + +Otherwise, just `make` without the environment var `KUNITS` produces an application that simply exits on exit. + +A quick test runner therefore is `./K.sh --version` or the alias `make test` or all at once with `KUNITS=1 make K test`. + +To pipe the output to stdout, execute the application in the foreground with `--naked` argument. + +For more information consider to follow the *white rabbit*, but its dangerous to go alone, take this: + +c sandbox: [wandbox.org](https://wandbox.org) + +js sandbox: [jsfiddle.net](https://jsfiddle.net) + +ws sandbox: [app.gosandy.io](https://app.gosandy.io/) + +
Release v0.7.x Changelog + +Updated Coinbase integration to Advanced Trade API. + +
+ +
Release v0.6.x Changelog + +Added Hello World bot, Portfolios bot, Scaling bot and Stable bot. + +Added Binance, Kraken, KuCoin, Gate.io and BitMEX API. + +
+ +
Release v0.5.x Changelog -### REST API +Updated exchange integrations as simple libcurl wrappers. -Tribeca also exposes a REST API of all it's data. It's all the same data you would get via the Web UI, just a bit easier to connect up to via other applications. Visit `http://localhost:3000/data/md` for the current market data, for instance. +
-### TODO +
Release v0.4.x Changelog -TODO: +Added main KryptoNinja class derived from all other classes and ready to be extended. -1. Add new exchanges +Added C++ OOP everywhere. -2. Add new, smarter trading strategies (as always!) +Added test units. -3. Support for currency pairs which do not trade in $0.01 increments (LTC, DOGE) +Added --interface=IP argument to bind outgoing traffic to a specific network interface. -4. More documentation +Added Ethfinex ~~and FCoin~~ API. -5. More performant UI +Added build-in document root to stop reading files from disk. + +Added build chain for win32. + +~~Updated OKEx websocket to binary data.~~ + +Added build chain for OSX v10.13. +
+ +
Release v0.3.x Changelog + +Updated HitBTC API v2. + +Added ZIP installation steps for non-git-lovers. + +Added HamelinRat quoting mode and Trend safety thanks to b-seite and serzhiio contributions. + +Added command-line arguments. + +Updated quoting engine and gateways without nodejs. + +Added Makefile to replace npm scripts. + +~~Added PNG files as configuration files.~~ + +Added built-in C++ WWW Server to replace expressjs and socketio. + +Added built-in SQLite C++ interface to replace external mongodb server. + +Added Poloniex API. +
+ +
Release v0.2.x Changelog + +Updated application name to K because of Kira. + +Added nodejs7, typescript2, angular4 and reactivexjs. + +Added cleanup of bandwidth, source code, dependencies and installation steps. + +Added many quoting parameters thanks to Camille92 genius suggestions. + +Added support for multiple instances/config files with nested matryoshka UI. + +Added npm scripts, david-dm, travis-ci, coveralls and codacy. + +Added historical charts to replace grafana. + +Added C++ math functions. + +Updated OKCoin API (since https://www.okcoin.com/t-354.html). + +Updated Bitfinex API v2. + +Added Coinbase FIX API. + +~~Added Korbit API.~~ + +Added new quoting styles PingPong, Boomerang, AK-47. + +Added cleanup of database records, memory usage and log recording. + +Added audio notices, realtime wallet display, and grafana integration. + +Added https, dark theme and new UI elements. + +Added a bit of love to Kira. +
+ +
Release v0.1.0 Changelog + +see the upstream project [michaelgrosner/tribeca](https://github.com/michaelgrosner/tribeca). +
+ +### Unlock + +The bot is unlocked for collaborators and contributors (feel free to make acceptable Pull Requests for already opened issues or for anything you consider useful, and let me know the BTC Payment Address for the bot that you wish to unlock in the description of the PR, and I will credit it for you). + +While locked, the orderbook will be in realtime 121 seconds, and later it will be updated only once every 121 seconds. + +Anonymous users can also unlock any API Key by paying 0.00121000 BTC to the address displayed on exit. + +Once unlocked you may use different bots or currency pairs or reinstall on a different machine with the same unlocked API Key. However, if you want to use more than one exchange, you will need to pay again to unlock the API Key for each exchange. + +Otherwise if you choose to not support further development by ctubio, just keep running some old commit and do not upgrade (any commit prior to v0.3.0 was completely unlocked). + +Please don't open issues asking how much % less the bot generates with `--free-version`; it is relative to your trading strategy, the market conditions, and the bot's performance. ### Donations -If you would like to support this project, please consider donating to 1BDpAKp8qqTA1ASXxK2ekk8b8metHcdTxj +nope, this project doesn't have maintenance costs. but you can donate to your favorite developer today!
(or tomorrow!) + +or see the upstream project [michaelgrosner/tribeca](https://github.com/michaelgrosner/tribeca). + +or donate your time with programming or financial suggestions in the IRC channel [#krypto.ninja](https://kiwiirc.com/client/irc.libera.chat:6697/?theme=cli#krypto.ninja) at irc.libera.chat on port 6697 (SSL), or 6667 (plain); or feel free to make any question, but questions technically are not donations. + +### General Discussion + +[IRC](https://kiwiirc.com/client/irc.libera.chat:6697/?theme=cli#krypto.ninja) is awesome! + +But if you dislike it.. consider to join the [discord server](https://discord.gg/jAX7GEzcWD). Or you can DM [ctubio on reddit](https://www.reddit.com/user/ctubio) privately. + +Otherwise, here on GitHub, just create a [new discussion](https://github.com/ctubio/Krypto-trading-bot/discussions) permanently readable by everybody. + +### Very Special Thanks to: + +- https://github.com/michaelgrosner/tribeca (https://github.com/michaelgrosner) +- https://curl.haxx.se (https://github.com/bagder) +- https://github.com/michaelgrosner/tribeca (https://github.com/michaelgrosner) +- https://github.com/uNetworking (https://github.com/alexhultman) +- https://github.com/michaelgrosner/tribeca (https://github.com/michaelgrosner) +- https://nlohmann.github.io/json (https://github.com/nlohmann) +- https://github.com/michaelgrosner/tribeca (https://github.com/michaelgrosner) +- http://invisible-island.net +- https://github.com/michaelgrosner/tribeca (https://github.com/michaelgrosner) +- https://www.sqlite.org +- https://github.com/michaelgrosner/tribeca (https://github.com/michaelgrosner) +- but Most Special Thanks goes to [your mother](https://youtu.be/YDafHsyyTNk). + +### Help + +If you need installation or usage support, please create a [new discussion](https://github.com/ctubio/Krypto-trading-bot/discussions/new). + +### Issues + +To request new features open a [new issue](https://github.com/ctubio/Krypto-trading-bot/issues/new?title=Feature%20request) and explain your improvement as you consider. + +To report errors open a [new issue](https://github.com/ctubio/Krypto-trading-bot/issues/new?title=Error%20report) only after collecting all possible relevant log messages. + +Pull Requests are welcome, but adhere to the Contributor License Agreement: +- Your biological and technological distinctiveness will be added to our own. Resistance is futile. + +### like yesterday, since 0day and ∞ + +![bcn](https://user-images.githubusercontent.com/1634027/29495722-1d924018-85c5-11e7-8d61-d83f5716ae9e.jpg) + +#### every new day we sing: + +

If love is so nice, tell me why are you so sad?
If love is so nice, tell me, oh tell me why are you hurt so bad?
One Love! get ready!

+

Now feel this drumbeat as it beats within,
playin' a riddim, resisting against the system:

+ + - https://youtu.be/g--fsK6aLf8 + - https://youtu.be/BncXzyjdREc + - https://youtu.be/uEqxj58g6To + - https://youtu.be/SS9DJX8gTKk + - https://youtu.be/vu6WXLQT5r8 + - https://youtu.be/e8ULyjcSukM + - https://youtu.be/Rom4qWtEkMA + - https://youtu.be/InNk4Z-BGc8 + - https://youtu.be/xPg_e_3cK-E + - https://youtu.be/KKpcQIfIAi8 + - https://youtu.be/pZAmer0EmMQ + - https://youtu.be/50aXt1ctmUU + - https://youtu.be/vofff0Ei3kk + - https://youtu.be/4Ois3zB7SJ4 + - https://youtu.be/_wGDcWD1E1A + - https://youtu.be/VOgFZfRVaww + - https://youtu.be/1iZdJNH3Z1o + - https://youtu.be/_e5hvHL2WTg + - https://youtu.be/jQhtEYfax5c + - add your song here (please open a [new issue](https://github.com/ctubio/Krypto-trading-bot/issues/new?title=Today,%20I%20sing) to share your link) +

+

+We have already enough policemen,
if you like adventures choose to be a brave firefighter. +




+ +



Violence should not be the answer to those who
are asking for freedom.




+ +




+

diff --git a/doc/Dopyfile b/doc/Dopyfile new file mode 100644 index 000000000..6056b3d6c --- /dev/null +++ b/doc/Dopyfile @@ -0,0 +1,6 @@ +@INCLUDE = Doxyfile +OUTPUT_DIRECTORY = /var/lib/K/doc +HTML_OUTPUT = html5 +GENERATE_HTML = NO +GENERATE_XML = YES +XML_PROGRAMLISTING = NO diff --git a/doc/Doxyfile b/doc/Doxyfile new file mode 100644 index 000000000..5a27ed633 --- /dev/null +++ b/doc/Doxyfile @@ -0,0 +1,30 @@ +PROJECT_NAME = Krypto.ninja +PROJECT_NUMBER = +PROJECT_BRIEF = +PROJECT_LOGO = +OUTPUT_DIRECTORY = /var/lib/K/doc +HTML_OUTPUT = html4 +INPUT = ../src/lib +STRIP_FROM_PATH = ../src +TAB_SIZE = 2 +OPTIMIZE_OUTPUT_FOR_C = YES +EXTRACT_ALL = YES +RECURSIVE = YES +SOURCE_BROWSER = YES +INLINE_SOURCES = YES +REFERENCED_BY_RELATION = YES +HTML_COLORSTYLE_HUE = 220 +HTML_COLORSTYLE_SAT = 100 +HTML_COLORSTYLE_GAMMA = 80 +HTML_TIMESTAMP = YES +GENERATE_TREEVIEW = YES +EXT_LINKS_IN_WINDOW = NO +SEARCHENGINE = YES +EXTERNAL_PAGES = YES +UML_LOOK = YES +UML_LIMIT_NUM_FIELDS = 10 +TEMPLATE_RELATIONS = YES +INCLUDED_BY_GRAPH = YES +CALL_GRAPH = NO +CALLER_GRAPH = NO +GENERATE_LATEX = NO diff --git a/doc/MANUAL.md b/doc/MANUAL.md new file mode 100644 index 000000000..08b76ff13 --- /dev/null +++ b/doc/MANUAL.md @@ -0,0 +1,259 @@ +# What is Market Making? + +Market making is a trading strategy where the trader simultaneously places both buy and sell orders in an attempt to profit from the bid-ask spread. Market makers stand ready to both buy and sell from other traders, thus providing liquidity to the market. + +The strategy is appealing to traders because it doesn't require traders to take a directional view of the market - there's money to be made when the market goes up and when the market goes down. It's also heavily incentivized by exchanges looking for liquidity and volume - many exchange operators will pay you to make markets on their exchanges. + +## An example + +Let's consider a simplified market. Let's say there are three traders: Alice, Bob, and Tim. Alice is looking to sell some of her Bitcoins and Bob is looking to convert some of his USD into Bitcoin. Neither one are savvy about markets or cryptocurrency - they use Bitcoin, but aren't going to lose sleep over trying to get the absolute best prices. Tim is operating **Krypto-trading-bot**. Now lets say that the price BTC to USD is $100. Tim could configure **Krypto-trading-bot** to send in a buy order for 1 BTC at $95 and a sell order for 1 BTC at $105. Tim would hope that Bob would come along and buy the offered sell order at $105 and Alice would come and sell BTC at $95 - netting Tim $10. + +But what if that doesn't happen? What if the price of BTC/USD jumps to $103? Now Tim's buy order seems really uncompetitive at $95 - Alice doesn't want to sell for that little. And Bob could get a pretty good deal by getting the BTC at only two extra dollars. To prevent this scenario, Tim's **Krypto-trading-bot** would readjust the orders by cancelling the $95-$105 orders and placing a new set of orders - also known as making a market - at $98-$108. + +Like in any market these days, Bob and Alice probably aren't humans clicking buttons. They certainly aren't humans shouting on a floor in lower Manhattan. More likely, they are also computerized algorithms capable of placing orders in milliseconds. To survive as a market maker, you need to be faster than those algorithms to make a profit. + +# So, how does **Krypto-trading-bot** work? + +As previously mentioned, market making is really the art of figuring out the price of something, then making a market around that price. So how do we know what is the real price of Bitcoin? Well... we don't. And of course the price of Bitcoin now might be radically different than the price in a day, in an hour, or even in a second from now. The best we can do is to build an estimate, or fair value, of the price of Bitcoin. In **Krypto-trading-bot**, we consume the market data from the exchange we are sending orders into as a starting point. That includes the best bids and offers and most recent trades (aka market trades) by other participants in the market. + +From our fair value, we then need to make a market around that price. Back to our hypothetical example with Bob and Alice: we could make our market as $95-$105, we could have also made it $99.99-$101.01, or we could have also done $47-$332.21. So how do we decide? That's where the quoting parameters come into play. Those parameters dictate how wide of a market to make (wide=askPrice-bidPrice) and what size we want our quotes in the market to be. The procedure for coming up with profitable parameters is both an art and a science - there is no one size fits all formula. + +When **Krypto-trading-bot** figures out a suitable market, **Krypto-trading-bot** will then send in the buy and sell orders. Hopefully it's able to buy for less then sell for more, and repeat many times per day. Sometimes that's not always the case - sometimes there are genuinely more buyers than sellers for the prices you are setting. Often this comes when the market is moving very fast in one direction. Luckily, **Krypto-trading-bot** will prevent you from selling too fast without finding corresponding buyers and will stop sending orders in the imbalanced side. + +# How do I see what's going on? + +Navigate to the Web UI as described in the install process. You should see a screen like: + +![](https://user-images.githubusercontent.com/1634027/44740610-2f302e00-aafb-11e8-8066-e4362320c62e.png) + +* Market Data and Quotes - this is perhaps the most important screen in the app. + + * The top row in blue with "q" is the quote that you generate via the Quoting Parameters supplied. If the text is grey, the generated quotes are not in the market, and green when they are. + + * "FV" is the fair value price calculated by **Krypto-trading-bot** as the starting point for the generated quote. + + * The "mkt" rows are the best bids and offers on the exchange you are connected to. + +* Trades - Trades done by your exchange account. "side" is the side which your order was sent as. "val" is the total value of the trade, which is price * size +/- total exchange fee + +* Market Trades - Trades done by all participants in the market. "ms" is the side that was the make side (provided liquidity). The columns starting with "q" are your quotes at the time of the trade, the columns starting with "m" are the best bid and offer information at the time of the trade. + +* Quoting Parameters - All of the parameters needed to generate a quote. See the section "How do I control **Krypto-trading-bot**' quotes?" for each field's description. + +* Positions - Shows holdings of each currency pair you are using. + +* Order List - Shows order statuses of each order sent to the exchange. + + * "Cxl" - clicking the red button will attempt to cancel the order. + +# How do I control **Krypto-trading-bot**' quotes? + +In the web UI, there are three rows of panels with cryptic looking names and editable textboxes. Those are the quoting parameters, the knobs which we can turn to affect how **Krypto-trading-bot** will trade. + +* `%` - If enabled, the values of `bidSize`, `askSize`, `tbp`, `pDiv` and `range` will be a percentage related to the total funds (available + held in both sides); useful when the very same funds are used in multiple markets, so the quantity of the funds is highly variable, then may be useful to work with percentages. + +* `mode` - Sets the quoting mode + + * `Join` - Sets our quote to be at the best bid and the best offered price, If the BBO is narrower than `width`, set our bid quote at `FV - width / 2` and ask quote at `FV + width / 2`. + + * `Top` - Same as `Join`, but if the code can better the best bid or offer by a penny while respecting the `width`, set that as the quote so we will then be at the top of the market. + + * `Mid` - Set our bid quote at `FV - width / 2` and ask quote at `FV + width / 2` + + * `Inverse Join` - Set the quote at the BBO if the BBO is narrower than `width`, otherwise make the quote so wide that no one will trade with it. + + * `Inverse Top` - Same as `Inverse Join` but make our orders jump to the very top of the order book. + + * `HamelinRat` - Follow the Colossus of the market. Unlike other modes, it does not calculate the quote spread based on fair value, instead it looks for the biggest order in the market levels and places the quote right before it. + + * `Depth` - Use `width` as `depth`. Unlike other modes, it does not calculate the quote spread based on fair value, instead it walks over all current open orders in the book and places the quote right after `depth` quantity, at both sides. + +* `safety`- Sets a quoting Safety + + * `PingPong` - Always respect the calculated `widthPong` from the last sold or bought `size`, if any. + + * `PingPoing` - Same as `PingPong` but do not respect always the `widthPong` to make new Pong trades, instead if the `fair value` has moved in a opposite `widthPong` direction, Ping trades will be restarted with 0 price as safety. For example if last buy was at 100 and withpong is 10, last buy safety will be ignored/restarted if price becomes 90. + + * `Boomerang` - Same as `PingPong` but the calculated `widthPong` for new Pongs is based on any best matching (using `pongAt`) previous sold or bought `size`, if any. + + * `AK-47` - Same as `Boomerang` but allows multiple orders at the same time in both sides. To avoid old trades, on every new trade **Krypto-trading-bot** will cancel all previous trades if those are worst. + + * `bullets` - Maximum amount of trades placed in each side (only affects `AK-47`). + + * `range` - Minimum width between `bullets` in USD (ex. a value of .3 is 30 cents; only affects `AK-47`). + +* `pingAt` (Pongs are always placed in both sides, only affects `PingPong`, `Boomerang` and `AK-47`) + + * `BothSides` - Place new Pings in both sides. + + * `BidSide` - Place new Pings only in Bid side, and therefore in Ask side only Pongs will be placed. + + * `AskSide` - Place new Pings only in Ask side, and therefore in Bid side only Pongs will be placed. + + * `DepletedSide` - Place new Pings only in the opposite side with not enough funds to continue trading. + + * `DepletedBidSide` - Place new Pings only in the Ask side if there are not enough funds to continue trading in the Bid side. + + * `DepletedAskSide` - Place new Pings only in the Bid side if there are not enough funds to continue trading in the Ask side. + + * `StopPings` - Only place new Pongs based on the history of Pings, without placing new Pings. + +* `pongAt` (only affects `Boomerang` and `AK-47`) + + * `ShortPingFair` - Place new Pongs based on the lowest margin Ping in history respecting the `widthPong` from the `fair value`. + + * `LongPingFair` - Place new Pongs based on the highest margin Ping in history respecting the `widthPong` from the `fair value`. + + * `ShortPingAggresive` - Place new Pongs based on the lowest margin Ping in history without respecting the `widthPong` from the `fair value`. + + * `LongPingAggresive` - Place new Pongs based on the highest margin Ping in history without respecting the `widthPong` from the `fair value`. + +* `bw?` - Enable Best Width to place orders avoiding "hollows" in the book, while accomodating new orders right near to existent orders in the book, without leaving "hollows" in between. + +* `bwSize` - If Best Width is enabled, set the total size of trades in the book to ignore. Set to 0 to only ignore "hollows". Useful for ignoring very small trades in the book. + +* `%w?` - If enabled, the values of `width` or `widthPing` and `widthPong` will be a percentage related to the `fair value`; useful when calculating profits subtracting exchange's fees (that usually are percentages too). + +* `width` and `widthPing` - Minimum width (spread) of our quote in USD (ex. a value of .3 is 30 cents). With the exception for when `apr` is checked and the system is aggressively rebalancing positions after they get out of whack, `width` will always be respected. + +* `widthPong` - Minimum width (spread) of our quote in USD (ex. a value of .3 is 30 cents). Used only if previous Pings exists in the opposite side. + +* `orderPctTot` - If `%` is enabled, specify the method for calculation of `bidSize` and `askSize` as percentages. + + * `Value` - Percentage is taken of the total funds (available funds + held in both sides). For example, if 20% is set, and the total funds is $100, then the maximum bid size is $20. This has a similar effect as to non-percentage-based sizes, but allows for bid sizes to adjust to funds quantity. + + * `Side` - Percentage is taken of funds only on one side. For example, if 20% is set, and quote funds are worth 20$, and base funds are worth $80, then buys will placed for 4$ and sells will placed for $16. This allows trading to continue even when funds on one side are heavily depleted, but results in held balances trending towards being evenly distributed. + + * `TBPValue` - Percentage is taken of the total funds as in `Value`, but either the sell size or buy size is shrunk proportional to the value of the TBP, such that the balances will move towards being bought or sold depending on which side the TBP is. + + * `TBPSide` - Percentage is taken of funds only on one side as in `Side`, but balance is taken relative to the `pDiv` extents such that they are not crossed, and either the sell size or buy size is shrunk proportional to the distance from the TBP, such that balances will tend to migrate towards the TBP. + + * `exp` - If `TBPSide` is used for `orderPctTot`, this specifies the exponent to raise the size multipliers by. The higher the number, the more the sizes will shrink as balance passes the TBP. + +* `bidSize` - Maximum bid size of our quote in BTC (ex. a value of 1.5 is 1.5 bitcoins). If `%` is enabled, then this is the maximum bid size as a % as specified in `orderPctTot`. With the exception for when `apr` is checked and the system is aggressively rebalancing positions after they get out of whack. + +* `askSize` - Maximum ask size of our quote in BTC (ex. a value of 1.5 is 1.5 bitcoins). If `%` is enabled, then this is the maximum ask size as a % as specified by `orderPctTot`. With the exception for when `apr` is checked and the system is aggressively rebalancing positions after they get out of whack. + +* `maxBidSize?` and `maxAskSize?` - Use `bidSize` and `askSize` as minimums and automatically find the maximum possible `size` based on the current "Target Base Position" (just as having enabled `apr` on `Size` but even before your position diverges more than `pDiv`). + +* `fv` - Sets the fair value calculation mode + + * `BBO` - `FV = ([topBid price] + [topAsk price]) / 2.0` + + * `wBBO` - `FV = ([topBid price]*[topBid size] + [topAsk price]*[topAsk size]) / ([topAsk size] + [topBid size])` + + * `rwBBO` - `FV = ([topBid price]*[topAsk size] + [topAsk price]*[topBid size]) / ([topAsk size] + [topBid size])` + +* `apMode` + + * `Manual` - **Krypto-trading-bot** will not try to automatically manage positions, instead you will need to manually set `tbp`. + + * `EWMA_LS` - **Krypto-trading-bot** will use a `long` minute and `short` minute exponential weighted moving average calculation to buy up BTC when the `short` minute line crosses over the `long` minute line, and sell BTC when the reverse happens. The EWMA values are currently exposed in the stats. + + * `EWMA_LMS` - **Krypto-trading-bot** will use a `long` minute, `medium` minute and `short` minute exponential weighted moving average calculation, together with the simple moving average of the last 3 `fair value` values, to buy up BTC when the `short` minute line crosses over the `long` minute line, and sell BTC when the reverse happens. + + * `EWMA_4` - **Krypto-trading-bot** will use a `medium` minute and `small` minute EWMA calculation to buy when the `small` minute line crosses over the `medium` minute line, and sell when the reverse happens. Additionally sets the `tbp` to 0% if the `verylong` EWMA minute line crosses over the `long` EWMA minute line. + + * `short` - Used when `apMode` is `EWMA_LS`, `EWMA_LMS` or `EWMA_4`. Sets the periods of EWMA Short to automatically manage positions. + * `medium` - Only used when `apMode` is `EWMA_LMS` or `EWMA_4`. Sets the periods of EWMA Medium to automatically manage positions. + * `long` - Used when `apMode` is `EWMA_LS`, `EWMA_LMS` or `EWMA_4`. Sets the periods of EWMA Long to automatically manage positions. + * `verylong` - Only used when `apMode` is `EWMA_4`. Sets the periods of EWMA VeryLong to automatically manage positions. + +* `sensibility` - Threshold removed from each period, affects EWMA Long, Medium and Short. The decimal value of `sensibility` must be betweem 0 and 1. + +* `tbp` - Only used when `apMode` is `Manual`. Sets a static "Target Base Position" for **Krypto-trading-bot** to stay near. In manual position mode, **Krypto-trading-bot** will still try to respect `pDiv` and not make your position fluctuate by more than that value. So if you have 10 BTC to trade, set `tbp = 3`, set `apMode = Manual`, and `pDiv = 1`, your holding of BTC will never be less than 2 or greater than 4. + +* `pDivMode` - Only used when `apMode` is not `Manual` mode. Sets the strategy of dynamically adjusting the `pDiv` depending on the divergence from 50% of Base Value. + + * `Manual` - No dynamic adjusting of `pDiv`. + + * `Linear` - Linear calculation between `pDiv` and `pDivMin`. + + * `Sine` - Calculation between `pDiv` and `pDivMin` on a sine curve. + + * `SQRT` - Square root calculation between `pDiv` and `pDivMin`. + + * `Switch` - If `tbp` is more than 90% or less than 10%, `pDivMin` is taken, otherwhise `pDiv`. + +* `pDiv` - If your "Target Base Position" diverges more from this value, **Krypto-trading-bot** will stop sending orders to stop too much directional trading. So if you have 10 BTC to trade, "Target Base Position" is reporting 5, and `pDiv` is set to 3, your holding of BTC will never be less than 2 or greater than 8. + +* `pDivMin` - Only used when `pDivMode` is not `Manual`. It defines the minimal `pDiv` for the dynamic positon divergence. + +* `apr` - If you're in a state where **Krypto-trading-bot** has stopped sending orders because your position has diverged too far from Target Base Position, this setting will much more aggressively try to fix that discrepancy by placing orders much larger than `size` and at prices much more aggressive than `width` normally allows (see `pongAt` option). It's a bit risky to use this setting. + + * `Off` - **Krypto-trading-bot** will not try to aggressively try to stabilize the target based position. + + * `Size` - **Krypto-trading-bot** will aggressively make use of bigger `size` values (x3 `size` or half of the diverged target base position, whatever is smaller). + + * `SizeWidth` - Same as `Size` but also will aggressively make use of smaller `width` values (respecting always aggressive `pongAt` option and `widthPong`). + +* `aprFactor` - Defines the value with which the `size` is multiplicated when `apr` is in functional state. + +* `sop` - Super opportunities, if enabled and if the market width is `sopWidth` times bigger than the `width` set, it multiplies `sopTrades` to `trades` and/or `sopSize` to `size`, in both sides at the same time. + +* `sopWidth` - Is the value with the market width is multiplicated to define the activation point for Super opportunities. + +* `sopTrades` - Multiplicates `trades` to rise the possible Trades per Minute if `sop` is in `Trades` or `tradesSize` state. + +* `sopSize` - Multiplicates `width` if `sop` is in `Size` or `tradesSize` state. + +* `trades` - Often, only buying or selling many times in a short timeframe indicates that there is going to be a price swing. `trades` and `/sec` are used to limitate ping trades: If you successfully complete more orders than `trades` in `/sec` seconds, the bot will stop sending more buy orders until either `/sec` seconds has passed, or you have sold enough at a higher cost to make all those buy orders profitable. The number of trades is reported by side in the UI; "BuyTS", "SellTS", and "TotTS". If "BuyTS" goes above `trades`, the bot will stop sending buy orders, and the same for sells. For example, if `trades` is 2 and `/sec` is 1800 (half an hour): + +Time | Side | Price | Size | BuyTS | SellTS | Notes +-------- |------|------:|-----:|------:|-------:|------------------------------------------------: +12:00:01 | Buy | 10 | 1 | 1 | 0 | +12:00:02 | Buy | 10 | 0.5 | 1.5 | 0 | Partial fills of `size` get counted fractionally +12:00:03 | Sell | 11 | 0.75 | 0.75 | 0 | Sell for more decrements the imbalance +12:00:05 | Sell | 5 | 0.75 | 0.75 | 0 | Sell for less than the other buys doesn't help +12:00:06 | Buy | 10 | 0.5 | 1.75 | 0 | +12:00:07 | Buy | 10 | 0.5 | 2.75 | 0 | Stop sending buy orders until 12:30:07! + +* `/sec` - see `trades`. + +* `ewmaPrice?` - Use a quote protection of `periods` smoothed line of the fair value to limit the price while sending new orders. + +* `ewmaWidth?` - Use a quote protection of `periods` smoothed line of the width (between the top bid and the top ask) to limit the widthPing while sending new orders. + +* `periodsᵉʷᵐᵃ` - Maximum amount of values collected in the sequences used to calculate the `ewmaPrice?` and `ewmaWidth?` quote protection. After collect sequentially every 1 minute the value of the `fair value`, and before place new orders, a limit will be always applied to the new orders price using a `ewma` calculation, taking into account only the last `periods` periods in each sequence. + +* `ewmaTrend?` - Use a trend protection of double `periods` (Ultra+Micro) smoothed lines of the price to limit uptrend sells and downtrend buys. + +* `threshold` - When trend stregth is above positive threshold value bot stops selling, when strength below negative threshold value bot stops buying. + +* `ultra` - Time in minutes to define Ultra EMA + +* `micro` - Time in minutes to define Micro EMA + +* `stdev` + + * `Off` - Do not limit the price of new orders. + + * `OnFV` - Use a quote protection of STDEV, calculated from a sequence of `fair value` values during `periods` periods of 1 second, to limit the price equally on both sides while sending new orders. + + * `OnFVAPROff` - Same as `OnFV` when `apr` is `Off` or when the system is not aggressively rebalancing positions; otherwise if is rebalancing, is same as `Off`. + + * `OnTops` - Use a quote protection STDEV, calculated from a unique sequence of both `best bid` and `best ask` values in the market order book during `periods * 2` periods of 1 second, to limit the price equally on both sides while sending new orders. + + * `OnTopsAPROff` - Same as `OnTops` when `apr` is `Off` or when the system is not aggressively rebalancing positions; otherwise if one side is rebalancing, is same as `Off` for that side. + + * `OnTop` - Use a quote protection STDEV, calculated from two sequences of the `best bid` (first sequence) and also of the `best ask` (second sequence) value in the market order book during `periods` periods of 1 second, to limit the price independently on each side while sending new orders. + + * `OnTopAPROff` - Same as `OnTop` when `apr` is `Off` or when the system is not aggressively rebalancing positions; otherwise if one side is rebalancing, is same as `Off` for that side. + +* `periodsˢᵗᵈᶜᵛ` - Maximum amount of values collected in the sequences used to calculate the STDEV, each side may have its own STDEV calculation with the same amount of `periods`. After collect sequentially every 1 second the values of the `fair value`, `last bid` and also of the `last ask` from the market order book, and before place new orders, a limit will be always applied to the new orders price using a calculation of the STDEV, taking into account only the last `periods` periods in each sequence. + +* `factor` - Multiplier used to increase or decrease the value of the selected `stdev` calculation, a `factor` of 1 does effectively nothing. + +* `BB?` - Enable Bollinger Bands with upper and lower bands calculated from the result of the selected `stdev` above or below its own moving average of `periods`. + +* `cxl?` - Enable a timeout of 5 minutes to cancel all orders that exist as open in the exchange (in case you found yourself with zombie orders in the exchange, because the API integration have bugs or because the connection is interrupted). + +* `lifetime` - Enable a timeout of `lifetime` milliseconds to keep orders open (otherwise open orders can be replaced anytime required). + +* `profit` - Timeframe in hours to calculate the display of Profit (under wallet values) and also interval in hour to remove data points from the Stats, for example a `profit` of 0.5 will compare the current wallet values and the values from half hour ago to display the +/- % of increment between both and will remove data from the Stats older than half an hour. + +* `Kmemory` - Timeout in days for Pings (yet unmatched trades) and/or Pongs (K trades) to remain in memory, a value of `0` keeps the history in memory forever; a positive value remove only Pongs after `Kmemory` days; but a negative value remove both Pings and Pongs after `Kmemory` days (for example a value of `-2` will keep a history of trades no longer than 2 days without matter if Pings are not matched by Pongs; or a value of `-0.25` will do so but limited to 6h). + +* `delayUI` - Relax the display of UI data by `delayUI` seconds. Set a value of 0 (zero) to display UI data in realtime, but this may penalize the communication with the exchange if you end up sending too much frequent UI data (like in low latency environments with super fast market data updates; at home is OK in realtime because the latency of **Krypto-trading-bot** with the exchange tends to be higher than the latency of **Krypto-trading-bot** with your browser). + +* `audio?` - plays a sound for each new trade (ping-pong modes have 2 sounds for each type of trade). diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 000000000..684f3340a --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,16 @@ +KDOC = $(KHOME)/doc + +all doc: clean + @mkdir -p $(KDOC) + doxygen + @test -d $(KDOC)/html.css || git clone git://github.com/mosra/m.css $(KDOC)/html.css + python3.6 $(KDOC)/html.css/documentation/doxygen.py Dopyfile + @rm -rf $(KDOC)/xml + +clean: + @rm -vrf $(KDOC)/html{4,5} + +CLEAN: + @rm -vrf $(KDOC)/html* + +.PHONY: all doc clean CLEAN diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 000000000..f5cbae5c6 --- /dev/null +++ b/doc/README.md @@ -0,0 +1 @@ +

  I accuse Spain of stealing years of life to innocent and honorable people,
    and to confront with violence and all state powers the peaceful ideals
    of democratic freedom and independence of Catalonia.



freedom to live in peace is a human right.
free NOW all the catalan hostages in prison!



take note: we are not afraid of your police.

diff --git a/doc/THANKS b/doc/THANKS new file mode 100644 index 000000000..11eba7c57 --- /dev/null +++ b/doc/THANKS @@ -0,0 +1,14 @@ +Today, is a beautiful day. + +Sorted by amount of contributions: + + author: Michael Grosner + source: https://github.com/michaelgrosner/tribeca + + author: Carles Tubio + source: https://github.com/ctubio/tribeca + + author: Camille92 + source: https://github.com/Camille92/tribeca + +04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73 diff --git "a/doc/\360\237\216\201/CODE_OF_CONDUCT.md" "b/doc/\360\237\216\201/CODE_OF_CONDUCT.md" new file mode 100644 index 000000000..0b3bcf6e6 --- /dev/null +++ "b/doc/\360\237\216\201/CODE_OF_CONDUCT.md" @@ -0,0 +1 @@ + diff --git "a/doc/\360\237\216\201/K.png" "b/doc/\360\237\216\201/K.png" new file mode 100644 index 000000000..35d9353f8 Binary files /dev/null and "b/doc/\360\237\216\201/K.png" differ diff --git a/docs/tribeca_main.png b/docs/tribeca_main.png deleted file mode 100644 index f9615a6f7..000000000 Binary files a/docs/tribeca_main.png and /dev/null differ diff --git a/docs/web_ui_preview.png b/docs/web_ui_preview.png deleted file mode 100644 index d79445bd5..000000000 Binary files a/docs/web_ui_preview.png and /dev/null differ diff --git a/etc/Dockerfile b/etc/Dockerfile new file mode 100644 index 000000000..eb7a741dd --- /dev/null +++ b/etc/Dockerfile @@ -0,0 +1,29 @@ +FROM debian:bullseye-slim + + +RUN apt-get update \ + && apt-get install --no-install-recommends -y git sudo make ca-certificates curl g++ + +# Feel free to choose the branch you want to build: +RUN git clone -b master https://github.com/ctubio/Krypto-trading-bot.git /K + +WORKDIR /K + +RUN make docker && rm -rf /var/lib/apt/lists/ + +EXPOSE 3000 5000 + +# See examples and descriptions of the +# following variables at etc/K.sh.dist. + +ENV OPTIONAL_ARGUMENTS --colors --port 3000 + +ENV API_EXCHANGE NULL +ENV API_CURRENCY BTC/USD +ENV API_KEY NULL +ENV API_SECRET NULL +ENV API_KEY_ID NULL + +ENV K_BINARY_FILE K-trading-bot + +CMD ["./K.sh", "--naked", "--without-ssl"] diff --git a/etc/K.sh.dist b/etc/K.sh.dist new file mode 100644 index 000000000..bf8379ffa --- /dev/null +++ b/etc/K.sh.dist @@ -0,0 +1,110 @@ +#!/usr/bin/env sh +# █▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀█ +# ▓ ████ ███ Usage? use current filename of THIS file: █ +# ▒ ████ ███ $ ./K.sh --help █ +# ▓███▀█▄ !Feel free to copy/move/rename THIS file. █ +# ▒ ▒▓██ ███ !Feel free to un/comment any examples but █ +# ▓ ░▒▓█ ███ do not define multiple times a variable. █ +# █ configurable! executable! K.sh initialization file █ +# █▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▀▄▄██▄▌ ▌▐ ▐▄██▄▄▀▄▄▄▄▄▄▄▄▄▄▄▄▄▄█ +# █▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▄▀▀▀▀▀▌▄ ▄▐▀▀▀▀▀▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀█ +# █▌ Today, is a beautiful day.▐▌.btw, on THIS file.. ▐█ +# ██ I. Define another file to wrap and run.... F F ██ +# ██ II. Define hardcoded optional arguments.... E R ██ +# ██ III. Define super secret API credentials.... E E ██ +# ██ IV. Run a file (I) until CTRL+C or kill -9. L E ██ +# ██▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄.▄!▄!▄██ +# ▌___ . +# ▌ I ▐________________________________________________. +# ▌¯¯¯ . +# █ K_BINARY_FILE . +# ██ - Allows one executable file available on any PATH. +# ██ . +#K_BINARY_FILE="K-+portfolios" +#K_BINARY_FILE="K-hello-world" +#K_BINARY_FILE="K-scaling-bot" +#K_BINARY_FILE="K-stable--bot" +K_BINARY_FILE="K-trading-bot" +# ██ . +# ██▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +# ▌____ . +# ▌ II ▐_______________________________________________. +# ▌¯¯¯¯ . +# █ OPTIONAL_ARGUMENTS . +# ██ - Allows a list of arguments (see usage at --help). +# ██ (to avoid to write it always in the command-line). +# ██ . +#OPTIONAL_ARGUMENTS="--colors --autobot --port 3000" +OPTIONAL_ARGUMENTS="--colors --naked --heartbeat" +# ██ . +# ██▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +# ▌_____ . +# ▌ III ▐______________________________________________. +# ▌¯¯¯¯¯ . +# █ API_EXCHANGE . +# ██ - Allows only one of the following exchanges below. +# ██ . +#API_EXCHANGE="COINBASE" +#API_EXCHANGE="BINANCE" +#API_EXCHANGE="BINANCE_US" +#API_EXCHANGE="KRAKEN" +#API_EXCHANGE="BITMEX" +#API_EXCHANGE="KUCOIN" +#API_EXCHANGE="BITFINEX" +#API_EXCHANGE="BITFINEX_MARGIN" +#API_EXCHANGE="ETHFINEX" +#API_EXCHANGE="ETHFINEX_MARGIN" +#API_EXCHANGE="GATEIO" +#API_EXCHANGE="HITBTC" +#API_EXCHANGE="BEQUANT" +#API_EXCHANGE="POLONIEX" +# ▌____________________________________________________. +# █ API_CURRENCY . +# ██ - Allows any currency pair (with format "AAA/ZZZ"). +# ██ (see the website of the exchange for all symbols). +# ██ . +#API_CURRENCY="BTC/EUR" +# ▌____________________________________________________. +# █ API_KEY . +# ██ - Allows any valid API KEY (never share!) . +# ██ (see the website of the exchange please) . +# ██ (on COINBASE is called API KEY NAME, and . +# ██ is only visible while creating the keys) . +# ██ . +#API_KEY="cdp_exampleapikey" +# ▌____________________________________________________. +# █ API_SECRET . +# ██ - Allows any valid API SECRET (never share!) . +# ██ (see the website of the exchange thank you) . +# ██ (must be one line, as UUID or EC PKEY cert) . +# ██ . +#API_SECRET="-----BEGIN EC PRIVATE KEY-----\nexampleapisecret\n-----END EC PRIVATE KEY-----\n" +# ▌____________________________________________________. +# █ API_KEY_ID . +# ██ - Allows any valid API KEY ID (never share!) . +# ██ (only COINBASE/KUCOIN must have API_KEY_ID) . +# ██ (can be safely ignored for all other exchanges) . +# ██ (see the website of COINBASE/KUCOIN for + info) . +# ██ (from COINBASE it can be copy&pasted anytime, but. +# ██ on KUCOIN is named PASSPHRASE because of reasons). +# ██ . +#API_KEY_ID="exampleapikeyid" +# ██ . +# ██▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +# ▌____ . +# ▌ IV ▐______________________________________________ . +# ▌¯¯¯¯ . + `#gdb -ex run --args` `#valgrind` \ + $K_BINARY_FILE \ + --title ${0##*/} \ + --exchange ${API_EXCHANGE:-""} \ + --currency ${API_CURRENCY:-""} \ + --apikey ${API_KEY:-""} \ + --secret "${API_SECRET:-""}" \ + --apikeyid ${API_KEY_ID:-""} \ + $OPTIONAL_ARGUMENTS "$@" ; +# ▌ ___________________________________________________. +# █ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯. +# ██ - That's all. To create multiple *.sh files: . +# ██ $ cp etc/K.sh.dist X.sh && chmod +x X.sh . +# ██▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄:wq diff --git a/etc/README.md b/etc/README.md new file mode 100644 index 000000000..4cf62d9f3 --- /dev/null +++ b/etc/README.md @@ -0,0 +1,35 @@ +### K.sh.dist +Used on install to initialize `./K.sh` file, feel free to add your own hardcoded arguments to your own `./K.sh` file after install. + +### ../src/lib/Krypto.ninja-client/www/.bomb.gzip +Used by `--whitelist` argument to attempt to crash UI clients from alien IPs not whitelisted; no need to open. + +### Dockerfile +To run K.sh with Docker, please make use of the [Dockerfile](https://raw.githubusercontent.com/ctubio/Krypto-trading-bot/master/etc/Dockerfile): + +1. Install [docker](https://www.docker.com/) for your system before proceeding. Requires at least Docker 1.7.1. Mac/Windows only: Ensure boot2docker or docker-machine is set up, depending on Docker version. See [the docs](https://docs.docker.com/installation/mac/) for more help. + +2. Copy the file [Dockerfile](https://raw.githubusercontent.com/ctubio/Krypto-trading-bot/master/etc/Dockerfile) into a text editor and edit the environment variables (named `API_*`) to match your desired configuration. + +3. Save your new Dockerfile, preferably in a secure location and in an empty directory. Then build the images and run the containers: +``` + $ cd path/to/Dockerfile + $ docker build --no-cache -t ksh . + $ docker run -p 3000:3000 --name Ksh -t -d ksh +``` +If you want to ensure that your data is persisted, mount a local folder into the container's `/data` folder: +``` +$ docker run -p 3000:3000 -v /path/to/data:/data --name Ksh -t -d ksh +``` + +If you run `docker ps`, you should see K container running. + +### Vagrantfile +To build your own portable development environment install [VirtualBox](https://www.virtualbox.org/wiki/Downloads) and [vagrant](https://www.vagrantup.com/downloads.html), then: +``` + $ cd path/to/K + $ cp etc/Vagrantfile Vagrantfile + $ vagrant up + $ vagrant ssh +``` +See more info at [PR #425](https://github.com/ctubio/Krypto-trading-bot/pull/425). \ No newline at end of file diff --git a/etc/Vagrantfile b/etc/Vagrantfile new file mode 100755 index 000000000..dece58eae --- /dev/null +++ b/etc/Vagrantfile @@ -0,0 +1,16 @@ +Vagrant.configure("2") do |config| + config.vm.box = "ubuntu/xenial64" + + config.vm.synced_folder ".", "/home/ubuntu/Krypto-trading-bot" + + config.vm.provision "shell", inline: <<-SHELL + apt-get update + apt-get install build-essential software-properties-common -y + add-apt-repository ppa:ubuntu-toolchain-r/test -y + apt-get update + apt-get install gcc-snapshot -y + apt-get update + apt-get install gcc-8 g++-8 -y + update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-8 60 --slave /usr/bin/g++ g++ /usr/bin/g++-8 + SHELL +end diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json deleted file mode 100644 index 1e9c189ff..000000000 --- a/npm-shrinkwrap.json +++ /dev/null @@ -1,3445 +0,0 @@ -{ - "name": "tribeca", - "version": "2.0.0", - "dependencies": { - "abbrev": { - "version": "1.0.7", - "from": "abbrev@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz" - }, - "accepts": { - "version": "1.3.2", - "from": "accepts@>=1.3.1 <1.4.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.2.tgz" - }, - "acorn": { - "version": "1.2.2", - "from": "acorn@>=1.0.3 <2.0.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz" - }, - "after": { - "version": "0.8.1", - "from": "after@0.8.1", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.1.tgz" - }, - "agent-base": { - "version": "2.0.1", - "from": "agent-base@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-2.0.1.tgz", - "dependencies": { - "semver": { - "version": "5.0.3", - "from": "semver@>=5.0.1 <5.1.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz" - } - } - }, - "agentkeepalive": { - "version": "2.0.5", - "from": "agentkeepalive@2.0.5", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-2.0.5.tgz" - }, - "amdefine": { - "version": "1.0.0", - "from": "amdefine@>=0.0.4", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" - }, - "angular": { - "version": "1.5.3", - "from": "angular@1.5.3", - "resolved": "https://registry.npmjs.org/angular/-/angular-1.5.3.tgz" - }, - "angular-ui-bootstrap": { - "version": "1.2.5", - "from": "angular-ui-bootstrap@1.2.5", - "resolved": "https://registry.npmjs.org/angular-ui-bootstrap/-/angular-ui-bootstrap-1.2.5.tgz" - }, - "angularjs": { - "version": "0.0.1", - "from": "angularjs@0.0.1", - "resolved": "https://registry.npmjs.org/angularjs/-/angularjs-0.0.1.tgz" - }, - "ansi-regex": { - "version": "2.0.0", - "from": "ansi-regex@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" - }, - "ansi-styles": { - "version": "2.2.1", - "from": "ansi-styles@>=2.2.1 <3.0.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" - }, - "any-promise": { - "version": "1.1.0", - "from": "any-promise@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.1.0.tgz" - }, - "anymatch": { - "version": "1.3.0", - "from": "anymatch@>=1.3.0 <2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz" - }, - "archy": { - "version": "1.0.0", - "from": "archy@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz" - }, - "argparse": { - "version": "0.1.16", - "from": "argparse@>=0.1.11 <0.2.0", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", - "dependencies": { - "underscore.string": { - "version": "2.4.0", - "from": "underscore.string@>=2.4.0 <2.5.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz" - } - } - }, - "arr-diff": { - "version": "2.0.0", - "from": "arr-diff@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz" - }, - "arr-flatten": { - "version": "1.0.1", - "from": "arr-flatten@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.0.1.tgz" - }, - "array-filter": { - "version": "0.0.1", - "from": "array-filter@>=0.0.0 <0.1.0", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz" - }, - "array-flatten": { - "version": "1.1.1", - "from": "array-flatten@1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" - }, - "array-map": { - "version": "0.0.0", - "from": "array-map@>=0.0.0 <0.1.0", - "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz" - }, - "array-reduce": { - "version": "0.0.0", - "from": "array-reduce@>=0.0.0 <0.1.0", - "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz" - }, - "array-uniq": { - "version": "1.0.2", - "from": "array-uniq@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.2.tgz" - }, - "array-unique": { - "version": "0.2.1", - "from": "array-unique@>=0.2.1 <0.3.0", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz" - }, - "arraybuffer.slice": { - "version": "0.0.6", - "from": "arraybuffer.slice@0.0.6", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz" - }, - "arrify": { - "version": "1.0.1", - "from": "arrify@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" - }, - "asn1": { - "version": "0.2.3", - "from": "asn1@>=0.2.3 <0.3.0", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" - }, - "asn1.js": { - "version": "4.5.2", - "from": "asn1.js@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.5.2.tgz" - }, - "assert": { - "version": "1.3.0", - "from": "assert@>=1.3.0 <1.4.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.3.0.tgz" - }, - "assert-plus": { - "version": "0.2.0", - "from": "assert-plus@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" - }, - "astw": { - "version": "2.0.0", - "from": "astw@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/astw/-/astw-2.0.0.tgz" - }, - "async": { - "version": "0.1.22", - "from": "async@>=0.1.22 <0.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz" - }, - "async-each": { - "version": "1.0.0", - "from": "async-each@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.0.tgz" - }, - "aws-sign2": { - "version": "0.6.0", - "from": "aws-sign2@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" - }, - "aws4": { - "version": "1.3.2", - "from": "aws4@>=1.2.1 <2.0.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.3.2.tgz", - "dependencies": { - "lru-cache": { - "version": "4.0.1", - "from": "lru-cache@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.1.tgz" - } - } - }, - "backo2": { - "version": "1.0.2", - "from": "backo2@1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz" - }, - "balanced-match": { - "version": "0.3.0", - "from": "balanced-match@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz" - }, - "base64-arraybuffer": { - "version": "0.1.2", - "from": "base64-arraybuffer@0.1.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.2.tgz" - }, - "base64-js": { - "version": "1.1.2", - "from": "base64-js@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.1.2.tgz" - }, - "base64id": { - "version": "0.1.0", - "from": "base64id@0.1.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-0.1.0.tgz" - }, - "basic-auth": { - "version": "1.0.3", - "from": "basic-auth@1.0.3", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.0.3.tgz" - }, - "basic-auth-connect": { - "version": "1.0.0", - "from": "basic-auth-connect@1.0.0", - "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz" - }, - "benchmark": { - "version": "1.0.0", - "from": "benchmark@1.0.0", - "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-1.0.0.tgz" - }, - "better-assert": { - "version": "1.0.2", - "from": "better-assert@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz" - }, - "binary-extensions": { - "version": "1.4.0", - "from": "binary-extensions@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.4.0.tgz" - }, - "bindings": { - "version": "1.2.1", - "from": "bindings@>=1.2.0 <1.3.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz" - }, - "bl": { - "version": "1.0.3", - "from": "bl@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz" - }, - "blob": { - "version": "0.0.4", - "from": "blob@0.0.4", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz" - }, - "bluebird": { - "version": "3.3.4", - "from": "bluebird@>=3.1.1 <4.0.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.3.4.tgz" - }, - "bn.js": { - "version": "4.11.1", - "from": "bn.js@>=4.1.1 <5.0.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.1.tgz" - }, - "body-parser": { - "version": "1.15.0", - "from": "body-parser@1.15.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.15.0.tgz" - }, - "boom": { - "version": "2.10.1", - "from": "boom@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" - }, - "boxen": { - "version": "0.3.1", - "from": "boxen@>=0.3.1 <0.4.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-0.3.1.tgz" - }, - "brace-expansion": { - "version": "1.1.3", - "from": "brace-expansion@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.3.tgz" - }, - "braces": { - "version": "1.8.3", - "from": "braces@>=1.8.2 <2.0.0", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.3.tgz" - }, - "brorand": { - "version": "1.0.5", - "from": "brorand@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.0.5.tgz" - }, - "browser-pack": { - "version": "6.0.1", - "from": "browser-pack@>=6.0.1 <7.0.0", - "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.0.1.tgz" - }, - "browser-resolve": { - "version": "1.11.1", - "from": "browser-resolve@>=1.11.0 <2.0.0", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.1.tgz" - }, - "browserify": { - "version": "13.0.0", - "from": "browserify@>=13.0.0 <14.0.0", - "resolved": "https://registry.npmjs.org/browserify/-/browserify-13.0.0.tgz", - "dependencies": { - "glob": { - "version": "5.0.15", - "from": "glob@>=5.0.15 <6.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz" - } - } - }, - "browserify-aes": { - "version": "1.0.6", - "from": "browserify-aes@>=1.0.4 <2.0.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.0.6.tgz" - }, - "browserify-cipher": { - "version": "1.0.0", - "from": "browserify-cipher@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz" - }, - "browserify-des": { - "version": "1.0.0", - "from": "browserify-des@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz" - }, - "browserify-rsa": { - "version": "4.0.1", - "from": "browserify-rsa@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz" - }, - "browserify-sign": { - "version": "4.0.0", - "from": "browserify-sign@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.0.tgz" - }, - "browserify-zlib": { - "version": "0.1.4", - "from": "browserify-zlib@>=0.1.2 <0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz" - }, - "bson": { - "version": "0.4.22", - "from": "bson@>=0.4.21 <0.5.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-0.4.22.tgz" - }, - "buffer": { - "version": "4.5.1", - "from": "buffer@>=4.1.0 <5.0.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.5.1.tgz", - "dependencies": { - "isarray": { - "version": "1.0.0", - "from": "isarray@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - } - } - }, - "buffer-xor": { - "version": "1.0.3", - "from": "buffer-xor@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz" - }, - "bufferutil": { - "version": "1.2.1", - "from": "bufferutil@1.2.1", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-1.2.1.tgz" - }, - "builtin-status-codes": { - "version": "2.0.0", - "from": "builtin-status-codes@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-2.0.0.tgz" - }, - "bunyan": { - "version": "1.8.0", - "from": "bunyan@1.8.0", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.0.tgz" - }, - "bytes": { - "version": "2.2.0", - "from": "bytes@2.2.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz" - }, - "callsite": { - "version": "1.0.0", - "from": "callsite@1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz" - }, - "capture-stack-trace": { - "version": "1.0.0", - "from": "capture-stack-trace@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz" - }, - "caseless": { - "version": "0.11.0", - "from": "caseless@>=0.11.0 <0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" - }, - "chalk": { - "version": "1.1.3", - "from": "chalk@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" - }, - "chokidar": { - "version": "1.4.3", - "from": "chokidar@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.4.3.tgz" - }, - "cipher-base": { - "version": "1.0.2", - "from": "cipher-base@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.2.tgz" - }, - "clone": { - "version": "1.0.2", - "from": "clone@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz" - }, - "code-point-at": { - "version": "1.0.0", - "from": "code-point-at@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.0.tgz" - }, - "coffee-script": { - "version": "1.3.3", - "from": "coffee-script@>=1.3.3 <1.4.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz" - }, - "collections": { - "version": "3.0.0", - "from": "collections@3.0.0", - "resolved": "https://registry.npmjs.org/collections/-/collections-3.0.0.tgz" - }, - "colors": { - "version": "0.6.2", - "from": "colors@>=0.6.2 <0.7.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz" - }, - "columnify": { - "version": "1.5.4", - "from": "columnify@>=1.5.2 <2.0.0", - "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz" - }, - "combine-source-map": { - "version": "0.7.1", - "from": "combine-source-map@>=0.7.1 <0.8.0", - "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.7.1.tgz" - }, - "combined-stream": { - "version": "1.0.5", - "from": "combined-stream@>=1.0.5 <1.1.0", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz" - }, - "commander": { - "version": "2.9.0", - "from": "commander@>=2.9.0 <3.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz" - }, - "component-bind": { - "version": "1.0.0", - "from": "component-bind@1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz" - }, - "component-emitter": { - "version": "1.1.2", - "from": "component-emitter@1.1.2", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz" - }, - "component-inherit": { - "version": "0.0.3", - "from": "component-inherit@0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz" - }, - "compressible": { - "version": "2.0.7", - "from": "compressible@>=2.0.7 <2.1.0", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.7.tgz" - }, - "compression": { - "version": "1.6.1", - "from": "compression@1.6.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.6.1.tgz" - }, - "concat-map": { - "version": "0.0.1", - "from": "concat-map@0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - }, - "concat-stream": { - "version": "1.5.1", - "from": "concat-stream@>=1.5.1 <1.6.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.1.tgz" - }, - "configstore": { - "version": "2.0.0", - "from": "configstore@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-2.0.0.tgz", - "dependencies": { - "graceful-fs": { - "version": "4.1.3", - "from": "graceful-fs@>=4.1.2 <5.0.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz" - } - } - }, - "connect": { - "version": "3.4.1", - "from": "connect@3.4.1", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.4.1.tgz" - }, - "console-browserify": { - "version": "1.1.0", - "from": "console-browserify@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz" - }, - "constants-browserify": { - "version": "1.0.0", - "from": "constants-browserify@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz" - }, - "content-disposition": { - "version": "0.5.1", - "from": "content-disposition@0.5.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.1.tgz" - }, - "content-type": { - "version": "1.0.1", - "from": "content-type@>=1.0.1 <1.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz" - }, - "convert-source-map": { - "version": "1.1.3", - "from": "convert-source-map@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz" - }, - "cookie": { - "version": "0.1.5", - "from": "cookie@0.1.5", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.5.tgz" - }, - "cookie-signature": { - "version": "1.0.6", - "from": "cookie-signature@1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" - }, - "core-util-is": { - "version": "1.0.2", - "from": "core-util-is@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" - }, - "create-ecdh": { - "version": "4.0.0", - "from": "create-ecdh@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz" - }, - "create-error-class": { - "version": "2.0.1", - "from": "create-error-class@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-2.0.1.tgz" - }, - "create-hash": { - "version": "1.1.2", - "from": "create-hash@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.2.tgz" - }, - "create-hmac": { - "version": "1.1.4", - "from": "create-hmac@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.4.tgz" - }, - "cryptiles": { - "version": "2.0.5", - "from": "cryptiles@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" - }, - "crypto-browserify": { - "version": "3.11.0", - "from": "crypto-browserify@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.0.tgz" - }, - "csproj2ts": { - "version": "0.0.7", - "from": "csproj2ts@0.0.7", - "resolved": "https://registry.npmjs.org/csproj2ts/-/csproj2ts-0.0.7.tgz", - "dependencies": { - "es6-promise": { - "version": "2.3.0", - "from": "es6-promise@>=2.0.1 <3.0.0", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-2.3.0.tgz" - }, - "lodash": { - "version": "3.10.1", - "from": "lodash@>=3.3.1 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz" - } - } - }, - "dashdash": { - "version": "1.13.0", - "from": "dashdash@>=1.10.1 <2.0.0", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.13.0.tgz", - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "from": "assert-plus@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - } - } - }, - "date-now": { - "version": "0.1.4", - "from": "date-now@>=0.1.4 <0.2.0", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz" - }, - "dateformat": { - "version": "1.0.2-1.2.3", - "from": "dateformat@1.0.2-1.2.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz" - }, - "debug": { - "version": "2.2.0", - "from": "debug@>=2.2.0 <2.3.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz" - }, - "deep-extend": { - "version": "0.4.1", - "from": "deep-extend@>=0.4.0 <0.5.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz" - }, - "defaults": { - "version": "1.0.3", - "from": "defaults@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz" - }, - "defined": { - "version": "1.0.0", - "from": "defined@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz" - }, - "delayed-stream": { - "version": "1.0.0", - "from": "delayed-stream@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - }, - "depd": { - "version": "1.1.0", - "from": "depd@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz" - }, - "deps-sort": { - "version": "2.0.0", - "from": "deps-sort@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz" - }, - "des.js": { - "version": "1.0.0", - "from": "des.js@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz" - }, - "destroy": { - "version": "1.0.4", - "from": "destroy@>=1.0.4 <1.1.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" - }, - "detect-indent": { - "version": "4.0.0", - "from": "detect-indent@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz" - }, - "detective": { - "version": "4.3.1", - "from": "detective@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/detective/-/detective-4.3.1.tgz" - }, - "diff": { - "version": "1.4.0", - "from": "diff@1.4.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz" - }, - "diffie-hellman": { - "version": "5.0.2", - "from": "diffie-hellman@>=5.0.0 <6.0.0", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz" - }, - "domain-browser": { - "version": "1.1.7", - "from": "domain-browser@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz" - }, - "dot-prop": { - "version": "2.4.0", - "from": "dot-prop@>=2.3.0 <3.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-2.4.0.tgz" - }, - "dtrace-provider": { - "version": "0.6.0", - "from": "dtrace-provider@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz" - }, - "duplexer2": { - "version": "0.1.4", - "from": "duplexer2@>=0.1.2 <0.2.0", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz" - }, - "ecc-jsbn": { - "version": "0.1.1", - "from": "ecc-jsbn@>=0.0.1 <1.0.0", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz" - }, - "ee-first": { - "version": "1.1.1", - "from": "ee-first@1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" - }, - "elliptic": { - "version": "6.2.3", - "from": "elliptic@>=6.0.0 <7.0.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.2.3.tgz" - }, - "engine.io": { - "version": "1.6.8", - "from": "engine.io@1.6.8", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.6.8.tgz", - "dependencies": { - "accepts": { - "version": "1.1.4", - "from": "accepts@1.1.4", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.1.4.tgz" - }, - "mime-db": { - "version": "1.12.0", - "from": "mime-db@>=1.12.0 <1.13.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz" - }, - "mime-types": { - "version": "2.0.14", - "from": "mime-types@>=2.0.4 <2.1.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz" - }, - "negotiator": { - "version": "0.4.9", - "from": "negotiator@0.4.9", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.9.tgz" - } - } - }, - "engine.io-client": { - "version": "1.6.8", - "from": "engine.io-client@1.6.8", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.6.8.tgz" - }, - "engine.io-parser": { - "version": "1.2.4", - "from": "engine.io-parser@1.2.4", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.2.4.tgz", - "dependencies": { - "has-binary": { - "version": "0.1.6", - "from": "has-binary@0.1.6", - "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.6.tgz" - } - } - }, - "error-ex": { - "version": "1.3.0", - "from": "error-ex@>=1.2.0 <2.0.0", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.0.tgz" - }, - "es6-promise": { - "version": "0.1.2", - "from": "es6-promise@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-0.1.2.tgz" - }, - "escape-html": { - "version": "1.0.3", - "from": "escape-html@>=1.0.3 <1.1.0", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" - }, - "escape-string-regexp": { - "version": "1.0.5", - "from": "escape-string-regexp@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - }, - "esprima": { - "version": "1.0.4", - "from": "esprima@>=1.0.2 <1.1.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz" - }, - "etag": { - "version": "1.7.0", - "from": "etag@>=1.7.0 <1.8.0", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz" - }, - "eventemitter2": { - "version": "0.4.14", - "from": "eventemitter2@>=0.4.13 <0.5.0", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz" - }, - "events": { - "version": "1.1.0", - "from": "events@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.0.tgz" - }, - "evp_bytestokey": { - "version": "1.0.0", - "from": "evp_bytestokey@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz" - }, - "exit": { - "version": "0.1.2", - "from": "exit@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" - }, - "expand-brackets": { - "version": "0.1.5", - "from": "expand-brackets@>=0.1.4 <0.2.0", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz" - }, - "expand-range": { - "version": "1.8.1", - "from": "expand-range@>=1.8.1 <2.0.0", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.1.tgz" - }, - "express": { - "version": "4.13.4", - "from": "express@4.13.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.13.4.tgz", - "dependencies": { - "accepts": { - "version": "1.2.13", - "from": "accepts@>=1.2.12 <1.3.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz" - }, - "negotiator": { - "version": "0.5.3", - "from": "negotiator@0.5.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz" - }, - "qs": { - "version": "4.0.0", - "from": "qs@4.0.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz" - }, - "vary": { - "version": "1.0.1", - "from": "vary@>=1.0.1 <1.1.0", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz" - } - } - }, - "extend": { - "version": "3.0.0", - "from": "extend@>=3.0.0 <3.1.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" - }, - "extglob": { - "version": "0.3.2", - "from": "extglob@>=0.3.1 <0.4.0", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz" - }, - "extsprintf": { - "version": "1.0.2", - "from": "extsprintf@1.0.2", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" - }, - "faye-websocket": { - "version": "0.10.0", - "from": "faye-websocket@>=0.10.0 <0.11.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz" - }, - "file-sync-cmp": { - "version": "0.1.1", - "from": "file-sync-cmp@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz" - }, - "filename-regex": { - "version": "2.0.0", - "from": "filename-regex@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.0.tgz" - }, - "fill-range": { - "version": "2.2.3", - "from": "fill-range@>=2.1.0 <3.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz" - }, - "filled-array": { - "version": "1.1.0", - "from": "filled-array@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/filled-array/-/filled-array-1.1.0.tgz" - }, - "finalhandler": { - "version": "0.4.1", - "from": "finalhandler@0.4.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.1.tgz" - }, - "findup-sync": { - "version": "0.1.3", - "from": "findup-sync@>=0.1.2 <0.2.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", - "dependencies": { - "glob": { - "version": "3.2.11", - "from": "glob@>=3.2.9 <3.3.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz" - }, - "lodash": { - "version": "2.4.2", - "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" - }, - "minimatch": { - "version": "0.3.0", - "from": "minimatch@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz" - } - } - }, - "for-in": { - "version": "0.1.5", - "from": "for-in@>=0.1.5 <0.2.0", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.5.tgz" - }, - "for-own": { - "version": "0.1.4", - "from": "for-own@>=0.1.3 <0.2.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.4.tgz" - }, - "forever-agent": { - "version": "0.6.1", - "from": "forever-agent@>=0.6.1 <0.7.0", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" - }, - "form-data": { - "version": "1.0.0-rc4", - "from": "form-data@>=1.0.0-rc3 <1.1.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz", - "dependencies": { - "async": { - "version": "1.5.2", - "from": "async@>=1.5.2 <2.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" - } - } - }, - "forwarded": { - "version": "0.1.0", - "from": "forwarded@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz" - }, - "fresh": { - "version": "0.3.0", - "from": "fresh@0.3.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.3.0.tgz" - }, - "fsevents": { - "version": "1.0.11", - "from": "fsevents@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.0.11.tgz", - "dependencies": { - "ansi": { - "version": "0.3.1", - "from": "ansi@~0.3.1", - "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz" - }, - "ansi-regex": { - "version": "2.0.0", - "from": "ansi-regex@^2.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" - }, - "ansi-styles": { - "version": "2.2.1", - "from": "ansi-styles@^2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" - }, - "are-we-there-yet": { - "version": "1.1.2", - "from": "are-we-there-yet@~1.1.2", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz" - }, - "asn1": { - "version": "0.2.3", - "from": "asn1@>=0.2.3 <0.3.0", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" - }, - "assert-plus": { - "version": "0.2.0", - "from": "assert-plus@^0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" - }, - "async": { - "version": "1.5.2", - "from": "async@^1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" - }, - "aws-sign2": { - "version": "0.6.0", - "from": "aws-sign2@~0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" - }, - "aws4": { - "version": "1.3.2", - "from": "aws4@^1.2.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.3.2.tgz", - "dependencies": { - "lru-cache": { - "version": "4.0.1", - "from": "lru-cache@^4.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.1.tgz", - "dependencies": { - "pseudomap": { - "version": "1.0.2", - "from": "pseudomap@^1.0.1", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz" - }, - "yallist": { - "version": "2.0.0", - "from": "yallist@^2.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.0.0.tgz" - } - } - } - } - }, - "bl": { - "version": "1.0.3", - "from": "bl@~1.0.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz" - }, - "block-stream": { - "version": "0.0.8", - "from": "block-stream@*", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.8.tgz" - }, - "boom": { - "version": "2.10.1", - "from": "boom@2.x.x", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" - }, - "caseless": { - "version": "0.11.0", - "from": "caseless@~0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" - }, - "chalk": { - "version": "1.1.3", - "from": "chalk@^1.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" - }, - "combined-stream": { - "version": "1.0.5", - "from": "combined-stream@~1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz" - }, - "commander": { - "version": "2.9.0", - "from": "commander@^2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz" - }, - "core-util-is": { - "version": "1.0.2", - "from": "core-util-is@~1.0.0", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" - }, - "cryptiles": { - "version": "2.0.5", - "from": "cryptiles@2.x.x", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" - }, - "dashdash": { - "version": "1.13.0", - "from": "dashdash@>=1.10.1 <2.0.0", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.13.0.tgz", - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "from": "assert-plus@^1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - } - } - }, - "debug": { - "version": "2.2.0", - "from": "debug@~2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz" - }, - "deep-extend": { - "version": "0.4.1", - "from": "deep-extend@~0.4.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz" - }, - "delayed-stream": { - "version": "1.0.0", - "from": "delayed-stream@~1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - }, - "delegates": { - "version": "1.0.0", - "from": "delegates@^1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" - }, - "ecc-jsbn": { - "version": "0.1.1", - "from": "ecc-jsbn@>=0.0.1 <1.0.0", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz" - }, - "escape-string-regexp": { - "version": "1.0.5", - "from": "escape-string-regexp@^1.0.2", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - }, - "extend": { - "version": "3.0.0", - "from": "extend@~3.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" - }, - "extsprintf": { - "version": "1.0.2", - "from": "extsprintf@1.0.2", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" - }, - "forever-agent": { - "version": "0.6.1", - "from": "forever-agent@~0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" - }, - "form-data": { - "version": "1.0.0-rc4", - "from": "form-data@~1.0.0-rc3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz" - }, - "fstream": { - "version": "1.0.8", - "from": "fstream@^1.0.2", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.8.tgz" - }, - "fstream-ignore": { - "version": "1.0.3", - "from": "fstream-ignore@~1.0.3", - "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.3.tgz", - "dependencies": { - "minimatch": { - "version": "3.0.0", - "from": "minimatch@^3.0.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", - "dependencies": { - "brace-expansion": { - "version": "1.1.3", - "from": "brace-expansion@^1.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.3.tgz", - "dependencies": { - "balanced-match": { - "version": "0.3.0", - "from": "balanced-match@^0.3.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz" - }, - "concat-map": { - "version": "0.0.1", - "from": "concat-map@0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - } - } - } - } - } - } - }, - "gauge": { - "version": "1.2.7", - "from": "gauge@~1.2.5", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz" - }, - "generate-function": { - "version": "2.0.0", - "from": "generate-function@^2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" - }, - "generate-object-property": { - "version": "1.2.0", - "from": "generate-object-property@^1.1.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz" - }, - "graceful-fs": { - "version": "4.1.3", - "from": "graceful-fs@^4.1.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz" - }, - "graceful-readlink": { - "version": "1.0.1", - "from": "graceful-readlink@>= 1.0.0", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" - }, - "har-validator": { - "version": "2.0.6", - "from": "har-validator@~2.0.6", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz" - }, - "has-ansi": { - "version": "2.0.0", - "from": "has-ansi@^2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" - }, - "has-unicode": { - "version": "2.0.0", - "from": "has-unicode@^2.0.0", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.0.tgz" - }, - "hawk": { - "version": "3.1.3", - "from": "hawk@~3.1.0", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz" - }, - "hoek": { - "version": "2.16.3", - "from": "hoek@2.x.x", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" - }, - "http-signature": { - "version": "1.1.1", - "from": "http-signature@~1.1.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz" - }, - "inherits": { - "version": "2.0.1", - "from": "inherits@*", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" - }, - "ini": { - "version": "1.3.4", - "from": "ini@~1.3.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz" - }, - "is-my-json-valid": { - "version": "2.13.1", - "from": "is-my-json-valid@^2.12.4", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz" - }, - "is-property": { - "version": "1.0.2", - "from": "is-property@^1.0.0", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" - }, - "is-typedarray": { - "version": "1.0.0", - "from": "is-typedarray@~1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" - }, - "isarray": { - "version": "1.0.0", - "from": "isarray@~1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - }, - "isstream": { - "version": "0.1.2", - "from": "isstream@~0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" - }, - "jodid25519": { - "version": "1.0.2", - "from": "jodid25519@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz" - }, - "jsbn": { - "version": "0.1.0", - "from": "jsbn@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz" - }, - "json-schema": { - "version": "0.2.2", - "from": "json-schema@0.2.2", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" - }, - "json-stringify-safe": { - "version": "5.0.1", - "from": "json-stringify-safe@~5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" - }, - "jsonpointer": { - "version": "2.0.0", - "from": "jsonpointer@2.0.0", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" - }, - "jsprim": { - "version": "1.2.2", - "from": "jsprim@^1.2.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz" - }, - "lodash.pad": { - "version": "4.1.0", - "from": "lodash.pad@^4.1.0", - "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.1.0.tgz" - }, - "lodash.padend": { - "version": "4.2.0", - "from": "lodash.padend@^4.1.0", - "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.2.0.tgz" - }, - "lodash.padstart": { - "version": "4.2.0", - "from": "lodash.padstart@^4.1.0", - "resolved": "https://registry.npmjs.org/lodash.padstart/-/lodash.padstart-4.2.0.tgz" - }, - "lodash.repeat": { - "version": "4.0.0", - "from": "lodash.repeat@^4.0.0", - "resolved": "https://registry.npmjs.org/lodash.repeat/-/lodash.repeat-4.0.0.tgz" - }, - "lodash.tostring": { - "version": "4.1.2", - "from": "lodash.tostring@^4.0.0", - "resolved": "https://registry.npmjs.org/lodash.tostring/-/lodash.tostring-4.1.2.tgz" - }, - "mime-db": { - "version": "1.22.0", - "from": "mime-db@~1.22.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.22.0.tgz" - }, - "mime-types": { - "version": "2.1.10", - "from": "mime-types@~2.1.7", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.10.tgz" - }, - "minimist": { - "version": "0.0.8", - "from": "minimist@0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" - }, - "mkdirp": { - "version": "0.5.1", - "from": "mkdirp@>=0.3.0 <0.4.0||>=0.4.0 <0.5.0||>=0.5.0 <0.6.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" - }, - "ms": { - "version": "0.7.1", - "from": "ms@0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" - }, - "node-pre-gyp": { - "version": "0.6.25", - "from": "node-pre-gyp@0.6.25", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.25.tgz", - "dependencies": { - "nopt": { - "version": "3.0.6", - "from": "nopt@~3.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "dependencies": { - "abbrev": { - "version": "1.0.7", - "from": "abbrev@1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz" - } - } - } - } - }, - "node-uuid": { - "version": "1.4.7", - "from": "node-uuid@~1.4.7", - "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz" - }, - "npmlog": { - "version": "2.0.3", - "from": "npmlog@~2.0.0", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-2.0.3.tgz" - }, - "oauth-sign": { - "version": "0.8.1", - "from": "oauth-sign@~0.8.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.1.tgz" - }, - "once": { - "version": "1.3.3", - "from": "once@~1.3.3", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz" - }, - "pinkie": { - "version": "2.0.4", - "from": "pinkie@^2.0.0", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" - }, - "pinkie-promise": { - "version": "2.0.0", - "from": "pinkie-promise@^2.0.0", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.0.tgz" - }, - "process-nextick-args": { - "version": "1.0.6", - "from": "process-nextick-args@~1.0.6", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.6.tgz" - }, - "qs": { - "version": "6.0.2", - "from": "qs@~6.0.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.0.2.tgz" - }, - "rc": { - "version": "1.1.6", - "from": "rc@~1.1.0", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz", - "dependencies": { - "minimist": { - "version": "1.2.0", - "from": "minimist@^1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz" - } - } - }, - "readable-stream": { - "version": "2.0.6", - "from": "readable-stream@^2.0.0 || ^1.1.13", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz" - }, - "request": { - "version": "2.69.0", - "from": "request@2.x", - "resolved": "https://registry.npmjs.org/request/-/request-2.69.0.tgz" - }, - "rimraf": { - "version": "2.5.2", - "from": "rimraf@~2.5.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz", - "dependencies": { - "glob": { - "version": "7.0.3", - "from": "glob@^7.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz", - "dependencies": { - "inflight": { - "version": "1.0.4", - "from": "inflight@^1.0.4", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.4.tgz", - "dependencies": { - "wrappy": { - "version": "1.0.1", - "from": "wrappy@1", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" - } - } - }, - "inherits": { - "version": "2.0.1", - "from": "inherits@2", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" - }, - "minimatch": { - "version": "3.0.0", - "from": "minimatch@2 || 3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", - "dependencies": { - "brace-expansion": { - "version": "1.1.3", - "from": "brace-expansion@^1.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.3.tgz", - "dependencies": { - "balanced-match": { - "version": "0.3.0", - "from": "balanced-match@^0.3.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz" - }, - "concat-map": { - "version": "0.0.1", - "from": "concat-map@0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - } - } - } - } - }, - "once": { - "version": "1.3.3", - "from": "once@^1.3.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", - "dependencies": { - "wrappy": { - "version": "1.0.1", - "from": "wrappy@1", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" - } - } - }, - "path-is-absolute": { - "version": "1.0.0", - "from": "path-is-absolute@^1.0.0", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" - } - } - } - } - }, - "semver": { - "version": "5.1.0", - "from": "semver@~5.1.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz" - }, - "sntp": { - "version": "1.0.9", - "from": "sntp@1.x.x", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" - }, - "sshpk": { - "version": "1.7.4", - "from": "sshpk@^1.7.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.7.4.tgz" - }, - "string_decoder": { - "version": "0.10.31", - "from": "string_decoder@~0.10.x", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - }, - "stringstream": { - "version": "0.0.5", - "from": "stringstream@~0.0.4", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz" - }, - "strip-ansi": { - "version": "3.0.1", - "from": "strip-ansi@^3.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - }, - "strip-json-comments": { - "version": "1.0.4", - "from": "strip-json-comments@~1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz" - }, - "supports-color": { - "version": "2.0.0", - "from": "supports-color@^2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" - }, - "tar": { - "version": "2.2.1", - "from": "tar@~2.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz" - }, - "tar-pack": { - "version": "3.1.3", - "from": "tar-pack@~3.1.0", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.1.3.tgz" - }, - "tough-cookie": { - "version": "2.2.2", - "from": "tough-cookie@~2.2.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz" - }, - "tunnel-agent": { - "version": "0.4.2", - "from": "tunnel-agent@~0.4.1", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.2.tgz" - }, - "tweetnacl": { - "version": "0.14.3", - "from": "tweetnacl@>=0.13.0 <1.0.0", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.3.tgz" - }, - "uid-number": { - "version": "0.0.6", - "from": "uid-number@~0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz" - }, - "util-deprecate": { - "version": "1.0.2", - "from": "util-deprecate@~1.0.1", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - }, - "verror": { - "version": "1.3.6", - "from": "verror@1.3.6", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz" - }, - "wrappy": { - "version": "1.0.1", - "from": "wrappy@1", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" - }, - "xtend": { - "version": "4.0.1", - "from": "xtend@^4.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" - } - } - }, - "function-bind": { - "version": "1.1.0", - "from": "function-bind@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz" - }, - "gaze": { - "version": "1.0.0", - "from": "gaze@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.0.0.tgz" - }, - "generate-function": { - "version": "2.0.0", - "from": "generate-function@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" - }, - "generate-object-property": { - "version": "1.2.0", - "from": "generate-object-property@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz" - }, - "getobject": { - "version": "0.1.0", - "from": "getobject@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz" - }, - "glob": { - "version": "6.0.4", - "from": "glob@>=6.0.1 <7.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz" - }, - "glob-base": { - "version": "0.3.0", - "from": "glob-base@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz" - }, - "glob-parent": { - "version": "2.0.0", - "from": "glob-parent@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz" - }, - "globule": { - "version": "0.2.0", - "from": "globule@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/globule/-/globule-0.2.0.tgz", - "dependencies": { - "glob": { - "version": "3.2.11", - "from": "glob@>=3.2.7 <3.3.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", - "dependencies": { - "minimatch": { - "version": "0.3.0", - "from": "minimatch@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz" - } - } - }, - "lodash": { - "version": "2.4.2", - "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" - }, - "minimatch": { - "version": "0.2.14", - "from": "minimatch@>=0.2.11 <0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" - } - } - }, - "got": { - "version": "5.5.0", - "from": "got@>=5.0.0 <6.0.0", - "resolved": "https://registry.npmjs.org/got/-/got-5.5.0.tgz" - }, - "graceful-fs": { - "version": "1.2.3", - "from": "graceful-fs@>=1.2.0 <1.3.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz" - }, - "graceful-readlink": { - "version": "1.0.1", - "from": "graceful-readlink@>=1.0.0", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" - }, - "growl": { - "version": "1.8.1", - "from": "growl@1.8.1", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.8.1.tgz" - }, - "grunt": { - "version": "0.4.5", - "from": "grunt@0.4.5", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", - "dependencies": { - "glob": { - "version": "3.1.21", - "from": "glob@>=3.1.21 <3.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz" - }, - "iconv-lite": { - "version": "0.2.11", - "from": "iconv-lite@>=0.2.11 <0.3.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz" - }, - "inherits": { - "version": "1.0.2", - "from": "inherits@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz" - }, - "lodash": { - "version": "0.9.2", - "from": "lodash@>=0.9.2 <0.10.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz" - }, - "minimatch": { - "version": "0.2.14", - "from": "minimatch@>=0.2.12 <0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" - }, - "rimraf": { - "version": "2.2.8", - "from": "rimraf@>=2.2.8 <2.3.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" - } - } - }, - "grunt-browserify": { - "version": "5.0.0", - "from": "grunt-browserify@5.0.0", - "resolved": "https://registry.npmjs.org/grunt-browserify/-/grunt-browserify-5.0.0.tgz", - "dependencies": { - "async": { - "version": "1.5.2", - "from": "async@>=1.5.0 <2.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" - }, - "lodash": { - "version": "3.10.1", - "from": "lodash@>=3.10.1 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz" - } - } - }, - "grunt-contrib-copy": { - "version": "1.0.0", - "from": "grunt-contrib-copy@1.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz" - }, - "grunt-contrib-watch": { - "version": "1.0.0", - "from": "grunt-contrib-watch@1.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.0.0.tgz", - "dependencies": { - "async": { - "version": "1.5.2", - "from": "async@>=1.5.0 <2.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" - }, - "lodash": { - "version": "3.10.1", - "from": "lodash@>=3.10.1 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz" - } - } - }, - "grunt-legacy-log": { - "version": "0.1.3", - "from": "grunt-legacy-log@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", - "dependencies": { - "lodash": { - "version": "2.4.2", - "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" - }, - "underscore.string": { - "version": "2.3.3", - "from": "underscore.string@>=2.3.3 <2.4.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" - } - } - }, - "grunt-legacy-log-utils": { - "version": "0.1.1", - "from": "grunt-legacy-log-utils@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", - "dependencies": { - "lodash": { - "version": "2.4.2", - "from": "lodash@>=2.4.1 <2.5.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" - }, - "underscore.string": { - "version": "2.3.3", - "from": "underscore.string@>=2.3.3 <2.4.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" - } - } - }, - "grunt-legacy-util": { - "version": "0.2.0", - "from": "grunt-legacy-util@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", - "dependencies": { - "lodash": { - "version": "0.9.2", - "from": "lodash@>=0.9.2 <0.10.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz" - } - } - }, - "grunt-ts": { - "version": "5.4.0", - "from": "grunt-ts@5.4.0", - "resolved": "https://registry.npmjs.org/grunt-ts/-/grunt-ts-5.4.0.tgz", - "dependencies": { - "async-each": { - "version": "0.1.6", - "from": "async-each@>=0.1.5 <0.2.0", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-0.1.6.tgz" - }, - "chokidar": { - "version": "1.0.6", - "from": "chokidar@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.0.6.tgz" - }, - "fsevents": { - "version": "0.3.8", - "from": "fsevents@>=0.3.8 <0.4.0", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-0.3.8.tgz" - }, - "glob-parent": { - "version": "1.3.0", - "from": "glob-parent@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-1.3.0.tgz", - "dependencies": { - "is-glob": { - "version": "2.0.1", - "from": "is-glob@>=2.0.0 <3.0.0" - } - } - }, - "graceful-fs": { - "version": "4.1.3", - "from": "graceful-fs@>=4.1.2 <4.2.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz" - }, - "is-glob": { - "version": "1.1.3", - "from": "is-glob@>=1.1.3 <2.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-1.1.3.tgz" - }, - "lodash": { - "version": "2.4.1", - "from": "lodash@2.4.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.1.tgz" - }, - "minimatch": { - "version": "0.2.14", - "from": "minimatch@>=0.2.12 <0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" - }, - "ncp": { - "version": "0.5.1", - "from": "ncp@0.5.1", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz" - }, - "readable-stream": { - "version": "1.0.33", - "from": "readable-stream@>=1.0.26-2 <1.1.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz" - }, - "readdirp": { - "version": "1.4.0", - "from": "readdirp@>=1.3.0 <2.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-1.4.0.tgz" - }, - "rimraf": { - "version": "2.2.6", - "from": "rimraf@2.2.6", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz" - }, - "typescript": { - "version": "1.7.3", - "from": "typescript@1.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-1.7.3.tgz" - }, - "underscore": { - "version": "1.5.1", - "from": "underscore@1.5.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.1.tgz" - }, - "underscore.string": { - "version": "2.3.3", - "from": "underscore.string@2.3.3", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" - } - } - }, - "har-validator": { - "version": "2.0.6", - "from": "har-validator@>=2.0.6 <2.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz" - }, - "has": { - "version": "1.0.1", - "from": "has@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz" - }, - "has-ansi": { - "version": "2.0.0", - "from": "has-ansi@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" - }, - "has-binary": { - "version": "0.1.7", - "from": "has-binary@0.1.7", - "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz" - }, - "has-cors": { - "version": "1.1.0", - "from": "has-cors@1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz" - }, - "hash.js": { - "version": "1.0.3", - "from": "hash.js@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.0.3.tgz" - }, - "hawk": { - "version": "3.1.3", - "from": "hawk@>=3.1.0 <3.2.0", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz" - }, - "hoek": { - "version": "2.16.3", - "from": "hoek@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" - }, - "hooker": { - "version": "0.2.3", - "from": "hooker@>=0.2.3 <0.3.0", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz" - }, - "htmlescape": { - "version": "1.1.1", - "from": "htmlescape@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz" - }, - "http-errors": { - "version": "1.4.0", - "from": "http-errors@>=1.4.0 <1.5.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz" - }, - "http-proxy-agent": { - "version": "1.0.0", - "from": "http-proxy-agent@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-1.0.0.tgz" - }, - "http-signature": { - "version": "1.1.1", - "from": "http-signature@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz" - }, - "https-browserify": { - "version": "0.0.1", - "from": "https-browserify@>=0.0.0 <0.1.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz" - }, - "https-proxy-agent": { - "version": "1.0.0", - "from": "https-proxy-agent@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz" - }, - "iconv-lite": { - "version": "0.4.13", - "from": "iconv-lite@0.4.13", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz" - }, - "ieee754": { - "version": "1.1.6", - "from": "ieee754@>=1.1.4 <2.0.0", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.6.tgz" - }, - "imurmurhash": { - "version": "0.1.4", - "from": "imurmurhash@>=0.1.4 <0.2.0", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - }, - "indexof": { - "version": "0.0.1", - "from": "indexof@0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz" - }, - "inflight": { - "version": "1.0.4", - "from": "inflight@>=1.0.4 <2.0.0", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.4.tgz" - }, - "inherits": { - "version": "2.0.1", - "from": "inherits@2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" - }, - "ini": { - "version": "1.3.4", - "from": "ini@>=1.3.0 <1.4.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz" - }, - "inline-source-map": { - "version": "0.6.1", - "from": "inline-source-map@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.1.tgz" - }, - "insert-module-globals": { - "version": "7.0.1", - "from": "insert-module-globals@>=7.0.0 <8.0.0", - "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.0.1.tgz" - }, - "invariant": { - "version": "2.2.1", - "from": "invariant@>=2.2.0 <3.0.0", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.1.tgz" - }, - "ipaddr.js": { - "version": "1.0.5", - "from": "ipaddr.js@1.0.5", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz" - }, - "is-absolute": { - "version": "0.2.5", - "from": "is-absolute@>=0.2.3 <0.3.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.5.tgz" - }, - "is-arrayish": { - "version": "0.2.1", - "from": "is-arrayish@>=0.2.1 <0.3.0", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" - }, - "is-binary-path": { - "version": "1.0.1", - "from": "is-binary-path@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz" - }, - "is-buffer": { - "version": "1.1.3", - "from": "is-buffer@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.3.tgz" - }, - "is-dotfile": { - "version": "1.0.2", - "from": "is-dotfile@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.2.tgz" - }, - "is-equal-shallow": { - "version": "0.1.3", - "from": "is-equal-shallow@>=0.1.3 <0.2.0", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz" - }, - "is-extendable": { - "version": "0.1.1", - "from": "is-extendable@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" - }, - "is-extglob": { - "version": "1.0.0", - "from": "is-extglob@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz" - }, - "is-finite": { - "version": "1.0.1", - "from": "is-finite@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.1.tgz" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz" - }, - "is-glob": { - "version": "2.0.1", - "from": "is-glob@>=2.0.1 <3.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz" - }, - "is-my-json-valid": { - "version": "2.13.1", - "from": "is-my-json-valid@>=2.12.4 <3.0.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz" - }, - "is-npm": { - "version": "1.0.0", - "from": "is-npm@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz" - }, - "is-number": { - "version": "2.1.0", - "from": "is-number@>=2.1.0 <3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz" - }, - "is-obj": { - "version": "1.0.1", - "from": "is-obj@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz" - }, - "is-plain-obj": { - "version": "1.1.0", - "from": "is-plain-obj@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" - }, - "is-posix-bracket": { - "version": "0.1.0", - "from": "is-posix-bracket@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.0.tgz" - }, - "is-primitive": { - "version": "2.0.0", - "from": "is-primitive@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz" - }, - "is-property": { - "version": "1.0.2", - "from": "is-property@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" - }, - "is-redirect": { - "version": "1.0.0", - "from": "is-redirect@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz" - }, - "is-relative": { - "version": "0.2.1", - "from": "is-relative@>=0.2.1 <0.3.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.2.1.tgz" - }, - "is-retry-allowed": { - "version": "1.0.0", - "from": "is-retry-allowed@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.0.0.tgz" - }, - "is-stream": { - "version": "1.0.1", - "from": "is-stream@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.0.1.tgz" - }, - "is-typedarray": { - "version": "1.0.0", - "from": "is-typedarray@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" - }, - "is-unc-path": { - "version": "0.1.1", - "from": "is-unc-path@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-0.1.1.tgz" - }, - "is-utf8": { - "version": "0.2.1", - "from": "is-utf8@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz" - }, - "is-windows": { - "version": "0.1.1", - "from": "is-windows@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.1.1.tgz" - }, - "isarray": { - "version": "0.0.1", - "from": "isarray@0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" - }, - "isobject": { - "version": "2.0.0", - "from": "isobject@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.0.0.tgz" - }, - "isstream": { - "version": "0.1.2", - "from": "isstream@>=0.1.2 <0.2.0", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" - }, - "jade": { - "version": "0.26.3", - "from": "jade@0.26.3", - "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "dependencies": { - "commander": { - "version": "0.6.1", - "from": "commander@0.6.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz" - }, - "mkdirp": { - "version": "0.3.0", - "from": "mkdirp@0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" - } - } - }, - "jodid25519": { - "version": "1.0.2", - "from": "jodid25519@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz" - }, - "jquery": { - "version": "2.2.2", - "from": "jquery@2.2.2", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-2.2.2.tgz" - }, - "js-tokens": { - "version": "1.0.3", - "from": "js-tokens@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-1.0.3.tgz" - }, - "js-yaml": { - "version": "2.0.5", - "from": "js-yaml@>=2.0.5 <2.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz" - }, - "jsbn": { - "version": "0.1.0", - "from": "jsbn@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz" - }, - "json-schema": { - "version": "0.2.2", - "from": "json-schema@0.2.2", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" - }, - "json-stable-stringify": { - "version": "0.0.1", - "from": "json-stable-stringify@>=0.0.0 <0.1.0", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz" - }, - "json-stringify-safe": { - "version": "5.0.1", - "from": "json-stringify-safe@>=5.0.1 <5.1.0", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" - }, - "json3": { - "version": "3.2.6", - "from": "json3@3.2.6", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.2.6.tgz" - }, - "jsonify": { - "version": "0.0.0", - "from": "jsonify@>=0.0.0 <0.1.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" - }, - "jsonparse": { - "version": "1.2.0", - "from": "jsonparse@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz" - }, - "jsonpointer": { - "version": "2.0.0", - "from": "jsonpointer@2.0.0", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" - }, - "JSONStream": { - "version": "1.1.1", - "from": "JSONStream@>=1.0.3 <2.0.0", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.1.1.tgz" - }, - "jsprim": { - "version": "1.2.2", - "from": "jsprim@>=1.2.2 <2.0.0", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz" - }, - "kind-of": { - "version": "3.0.2", - "from": "kind-of@>=3.0.2 <4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.0.2.tgz" - }, - "labeled-stream-splicer": { - "version": "2.0.0", - "from": "labeled-stream-splicer@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz" - }, - "latest-version": { - "version": "2.0.0", - "from": "latest-version@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-2.0.0.tgz" - }, - "lexical-scope": { - "version": "1.2.0", - "from": "lexical-scope@>=1.2.0 <2.0.0", - "resolved": "https://registry.npmjs.org/lexical-scope/-/lexical-scope-1.2.0.tgz" - }, - "listify": { - "version": "1.0.0", - "from": "listify@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/listify/-/listify-1.0.0.tgz" - }, - "livereload-js": { - "version": "2.2.2", - "from": "livereload-js@>=2.2.0 <3.0.0", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.2.2.tgz" - }, - "lockfile": { - "version": "1.0.1", - "from": "lockfile@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.1.tgz" - }, - "lodash": { - "version": "4.6.1", - "from": "lodash@4.6.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.6.1.tgz" - }, - "lodash.memoize": { - "version": "3.0.4", - "from": "lodash.memoize@>=3.0.3 <3.1.0", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz" - }, - "loose-envify": { - "version": "1.1.0", - "from": "loose-envify@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.1.0.tgz" - }, - "lowercase-keys": { - "version": "1.0.0", - "from": "lowercase-keys@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz" - }, - "lru-cache": { - "version": "2.7.3", - "from": "lru-cache@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" - }, - "make-error": { - "version": "1.1.1", - "from": "make-error@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.1.1.tgz" - }, - "make-error-cause": { - "version": "1.1.0", - "from": "make-error-cause@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/make-error-cause/-/make-error-cause-1.1.0.tgz" - }, - "media-typer": { - "version": "0.3.0", - "from": "media-typer@0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" - }, - "merge-descriptors": { - "version": "1.0.1", - "from": "merge-descriptors@1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" - }, - "methods": { - "version": "1.1.2", - "from": "methods@>=1.1.2 <1.2.0", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" - }, - "micromatch": { - "version": "2.3.7", - "from": "micromatch@>=2.1.5 <3.0.0", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.7.tgz" - }, - "miller-rabin": { - "version": "4.0.0", - "from": "miller-rabin@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.0.tgz" - }, - "mime": { - "version": "1.3.4", - "from": "mime@1.3.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz" - }, - "mime-db": { - "version": "1.22.0", - "from": "mime-db@>=1.22.0 <1.23.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.22.0.tgz" - }, - "mime-types": { - "version": "2.1.10", - "from": "mime-types@>=2.1.10 <2.2.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.10.tgz" - }, - "minimalistic-assert": { - "version": "1.0.0", - "from": "minimalistic-assert@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz" - }, - "minimatch": { - "version": "3.0.0", - "from": "minimatch@>=2.0.0 <3.0.0||>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz" - }, - "minimist": { - "version": "0.0.8", - "from": "minimist@0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" - }, - "mkdirp": { - "version": "0.5.1", - "from": "mkdirp@>=0.5.1 <0.6.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" - }, - "module-deps": { - "version": "4.0.5", - "from": "module-deps@>=4.0.2 <5.0.0", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-4.0.5.tgz" - }, - "moment": { - "version": "2.12.0", - "from": "moment@2.12.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.12.0.tgz" - }, - "mongodb": { - "version": "2.1.11", - "from": "mongodb@2.1.11", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.1.11.tgz", - "dependencies": { - "es6-promise": { - "version": "3.0.2", - "from": "es6-promise@3.0.2", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz" - }, - "readable-stream": { - "version": "1.0.31", - "from": "readable-stream@1.0.31", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz" - } - } - }, - "mongodb-core": { - "version": "1.3.10", - "from": "mongodb-core@1.3.10", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-1.3.10.tgz" - }, - "ms": { - "version": "0.7.1", - "from": "ms@0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" - }, - "mv": { - "version": "2.1.1", - "from": "mv@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz" - }, - "nan": { - "version": "2.2.1", - "from": "nan@>=2.0.5 <3.0.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.2.1.tgz" - }, - "ncp": { - "version": "2.0.0", - "from": "ncp@>=2.0.0 <2.1.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz" - }, - "negotiator": { - "version": "0.6.0", - "from": "negotiator@0.6.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.0.tgz" - }, - "node-status-codes": { - "version": "1.0.0", - "from": "node-status-codes@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz" - }, - "node-uuid": { - "version": "1.4.7", - "from": "node-uuid@1.4.7", - "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz" - }, - "nopt": { - "version": "1.0.10", - "from": "nopt@>=1.0.10 <1.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz" - }, - "normalize-path": { - "version": "2.0.1", - "from": "normalize-path@>=2.0.1 <3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.0.1.tgz" - }, - "number-is-nan": { - "version": "1.0.0", - "from": "number-is-nan@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz" - }, - "oauth-sign": { - "version": "0.8.1", - "from": "oauth-sign@>=0.8.0 <0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.1.tgz" - }, - "object-assign": { - "version": "4.0.1", - "from": "object-assign@>=4.0.1 <5.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.0.1.tgz" - }, - "object-component": { - "version": "0.0.3", - "from": "object-component@0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz" - }, - "object.omit": { - "version": "2.0.0", - "from": "object.omit@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.0.tgz" - }, - "object.pick": { - "version": "1.1.2", - "from": "object.pick@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.1.2.tgz" - }, - "on-finished": { - "version": "2.3.0", - "from": "on-finished@>=2.3.0 <2.4.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" - }, - "on-headers": { - "version": "1.0.1", - "from": "on-headers@>=1.0.1 <1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz" - }, - "once": { - "version": "1.3.3", - "from": "once@>=1.3.0 <2.0.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz" - }, - "options": { - "version": "0.0.6", - "from": "options@>=0.0.5", - "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz" - }, - "os-browserify": { - "version": "0.1.2", - "from": "os-browserify@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.1.2.tgz" - }, - "os-homedir": { - "version": "1.0.1", - "from": "os-homedir@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.1.tgz" - }, - "os-tmpdir": { - "version": "1.0.1", - "from": "os-tmpdir@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.1.tgz" - }, - "osenv": { - "version": "0.1.3", - "from": "osenv@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.3.tgz" - }, - "outpipe": { - "version": "1.1.1", - "from": "outpipe@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz" - }, - "package-json": { - "version": "2.3.1", - "from": "package-json@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-2.3.1.tgz" - }, - "pako": { - "version": "0.2.8", - "from": "pako@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.8.tgz" - }, - "parents": { - "version": "1.0.1", - "from": "parents@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz" - }, - "parse-asn1": { - "version": "5.0.0", - "from": "parse-asn1@>=5.0.0 <6.0.0", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.0.0.tgz" - }, - "parse-glob": { - "version": "3.0.4", - "from": "parse-glob@>=3.0.4 <4.0.0", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz" - }, - "parse-json": { - "version": "2.2.0", - "from": "parse-json@>=2.2.0 <3.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz" - }, - "parsejson": { - "version": "0.0.1", - "from": "parsejson@0.0.1", - "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.1.tgz" - }, - "parseqs": { - "version": "0.0.2", - "from": "parseqs@0.0.2", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.2.tgz" - }, - "parseuri": { - "version": "0.0.4", - "from": "parseuri@0.0.4", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.4.tgz" - }, - "parseurl": { - "version": "1.3.1", - "from": "parseurl@>=1.3.1 <1.4.0", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz" - }, - "path-browserify": { - "version": "0.0.0", - "from": "path-browserify@>=0.0.0 <0.1.0", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz" - }, - "path-is-absolute": { - "version": "1.0.0", - "from": "path-is-absolute@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" - }, - "path-platform": { - "version": "0.11.15", - "from": "path-platform@>=0.11.15 <0.12.0", - "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz" - }, - "path-to-regexp": { - "version": "0.1.7", - "from": "path-to-regexp@0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" - }, - "pbkdf2": { - "version": "3.0.4", - "from": "pbkdf2@>=3.0.3 <4.0.0", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.4.tgz" - }, - "pinkie": { - "version": "2.0.4", - "from": "pinkie@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" - }, - "pinkie-promise": { - "version": "2.0.0", - "from": "pinkie-promise@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.0.tgz" - }, - "popsicle": { - "version": "5.0.1", - "from": "popsicle@>=5.0.0 <6.0.0", - "resolved": "https://registry.npmjs.org/popsicle/-/popsicle-5.0.1.tgz", - "dependencies": { - "async": { - "version": "0.9.2", - "from": "async@>=0.9.0 <0.10.0", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz" - }, - "combined-stream": { - "version": "0.0.7", - "from": "combined-stream@>=0.0.4 <0.1.0", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz" - }, - "delayed-stream": { - "version": "0.0.5", - "from": "delayed-stream@0.0.5", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz" - }, - "form-data": { - "version": "0.2.0", - "from": "form-data@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.2.0.tgz" - }, - "mime-db": { - "version": "1.12.0", - "from": "mime-db@>=1.12.0 <1.13.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz" - }, - "mime-types": { - "version": "2.0.14", - "from": "mime-types@>=2.0.3 <2.1.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz" - } - } - }, - "popsicle-proxy-agent": { - "version": "1.0.0", - "from": "popsicle-proxy-agent@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/popsicle-proxy-agent/-/popsicle-proxy-agent-1.0.0.tgz" - }, - "popsicle-retry": { - "version": "1.0.1", - "from": "popsicle-retry@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/popsicle-retry/-/popsicle-retry-1.0.1.tgz" - }, - "popsicle-status": { - "version": "1.0.2", - "from": "popsicle-status@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/popsicle-status/-/popsicle-status-1.0.2.tgz" - }, - "prepend-http": { - "version": "1.0.3", - "from": "prepend-http@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.3.tgz" - }, - "preserve": { - "version": "0.2.0", - "from": "preserve@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz" - }, - "process": { - "version": "0.11.2", - "from": "process@>=0.11.0 <0.12.0", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.2.tgz" - }, - "process-nextick-args": { - "version": "1.0.6", - "from": "process-nextick-args@>=1.0.6 <1.1.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.6.tgz" - }, - "promise-finally": { - "version": "2.1.0", - "from": "promise-finally@>=2.0.1 <3.0.0", - "resolved": "https://registry.npmjs.org/promise-finally/-/promise-finally-2.1.0.tgz" - }, - "proxy-addr": { - "version": "1.0.10", - "from": "proxy-addr@>=1.0.10 <1.1.0", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz" - }, - "pseudomap": { - "version": "1.0.2", - "from": "pseudomap@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz" - }, - "public-encrypt": { - "version": "4.0.0", - "from": "public-encrypt@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz" - }, - "punycode": { - "version": "1.4.1", - "from": "punycode@>=1.3.2 <2.0.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" - }, - "q": { - "version": "1.4.1", - "from": "q@1.4.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz" - }, - "qs": { - "version": "6.1.0", - "from": "qs@6.1.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz" - }, - "querystring": { - "version": "0.2.0", - "from": "querystring@0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" - }, - "querystring-es3": { - "version": "0.2.1", - "from": "querystring-es3@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz" - }, - "randomatic": { - "version": "1.1.5", - "from": "randomatic@>=1.1.3 <2.0.0", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.5.tgz" - }, - "randombytes": { - "version": "2.0.3", - "from": "randombytes@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.3.tgz" - }, - "range-parser": { - "version": "1.0.3", - "from": "range-parser@>=1.0.3 <1.1.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz" - }, - "raw-body": { - "version": "2.1.6", - "from": "raw-body@>=2.1.5 <2.2.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.6.tgz", - "dependencies": { - "bytes": { - "version": "2.3.0", - "from": "bytes@2.3.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.3.0.tgz" - } - } - }, - "rc": { - "version": "1.1.6", - "from": "rc@>=1.1.5 <2.0.0", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz", - "dependencies": { - "minimist": { - "version": "1.2.0", - "from": "minimist@>=1.2.0 <2.0.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz" - } - } - }, - "read-all-stream": { - "version": "3.1.0", - "from": "read-all-stream@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz" - }, - "read-only-stream": { - "version": "2.0.0", - "from": "read-only-stream@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz" - }, - "readable-stream": { - "version": "2.0.6", - "from": "readable-stream@>=2.0.2 <3.0.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "dependencies": { - "isarray": { - "version": "1.0.0", - "from": "isarray@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - } - } - }, - "readdirp": { - "version": "2.0.0", - "from": "readdirp@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.0.0.tgz", - "dependencies": { - "graceful-fs": { - "version": "4.1.3", - "from": "graceful-fs@>=4.1.2 <5.0.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz" - }, - "minimatch": { - "version": "2.0.10", - "from": "minimatch@>=2.0.10 <3.0.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz" - } - } - }, - "regex-cache": { - "version": "0.4.3", - "from": "regex-cache@>=0.4.2 <0.5.0", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz" - }, - "registry-url": { - "version": "3.0.3", - "from": "registry-url@>=3.0.3 <4.0.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.0.3.tgz" - }, - "repeat-element": { - "version": "1.1.2", - "from": "repeat-element@>=1.1.2 <2.0.0", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz" - }, - "repeat-string": { - "version": "1.5.4", - "from": "repeat-string@>=1.5.2 <2.0.0", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.5.4.tgz" - }, - "repeating": { - "version": "2.0.0", - "from": "repeating@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.0.tgz" - }, - "request": { - "version": "2.69.0", - "from": "request@2.69.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.69.0.tgz", - "dependencies": { - "qs": { - "version": "6.0.2", - "from": "qs@>=6.0.2 <6.1.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.0.2.tgz" - } - } - }, - "require_optional": { - "version": "1.0.0", - "from": "require_optional@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.0.tgz" - }, - "resolve": { - "version": "1.1.7", - "from": "resolve@>=1.1.6 <2.0.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz" - }, - "resolve-from": { - "version": "2.0.0", - "from": "resolve-from@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz" - }, - "rimraf": { - "version": "2.4.5", - "from": "rimraf@>=2.4.0 <2.5.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz" - }, - "ripemd160": { - "version": "1.0.1", - "from": "ripemd160@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-1.0.1.tgz" - }, - "safe-json-stringify": { - "version": "1.0.3", - "from": "safe-json-stringify@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.3.tgz" - }, - "sax": { - "version": "1.2.1", - "from": "sax@>=0.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz" - }, - "semver": { - "version": "5.1.0", - "from": "semver@>=5.0.1 <6.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz" - }, - "semver-diff": { - "version": "2.1.0", - "from": "semver-diff@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz" - }, - "send": { - "version": "0.13.1", - "from": "send@0.13.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.13.1.tgz", - "dependencies": { - "http-errors": { - "version": "1.3.1", - "from": "http-errors@>=1.3.1 <1.4.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz" - } - } - }, - "serve-static": { - "version": "1.10.2", - "from": "serve-static@>=1.10.2 <1.11.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.10.2.tgz" - }, - "sha.js": { - "version": "2.4.5", - "from": "sha.js@>=2.3.6 <3.0.0", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.5.tgz" - }, - "shasum": { - "version": "1.0.2", - "from": "shasum@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz" - }, - "shell-quote": { - "version": "1.5.0", - "from": "shell-quote@>=1.4.3 <2.0.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.5.0.tgz" - }, - "shortid": { - "version": "2.2.4", - "from": "shortid@2.2.4", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.4.tgz" - }, - "sigmund": { - "version": "1.0.1", - "from": "sigmund@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" - }, - "slide": { - "version": "1.1.6", - "from": "slide@>=1.1.5 <2.0.0", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz" - }, - "sntp": { - "version": "1.0.9", - "from": "sntp@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" - }, - "socket.io": { - "version": "1.4.5", - "from": "socket.io@1.4.5", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.4.5.tgz" - }, - "socket.io-adapter": { - "version": "0.4.0", - "from": "socket.io-adapter@0.4.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.4.0.tgz", - "dependencies": { - "socket.io-parser": { - "version": "2.2.2", - "from": "socket.io-parser@2.2.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.2.2.tgz", - "dependencies": { - "debug": { - "version": "0.7.4", - "from": "debug@0.7.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz" - } - } - } - } - }, - "socket.io-client": { - "version": "1.4.5", - "from": "socket.io-client@1.4.5", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.4.5.tgz", - "dependencies": { - "component-emitter": { - "version": "1.2.0", - "from": "component-emitter@1.2.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz" - } - } - }, - "socket.io-parser": { - "version": "2.2.6", - "from": "socket.io-parser@2.2.6", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.2.6.tgz", - "dependencies": { - "json3": { - "version": "3.3.2", - "from": "json3@3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz" - } - } - }, - "sort-keys": { - "version": "1.1.1", - "from": "sort-keys@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.1.tgz" - }, - "source-map": { - "version": "0.4.2", - "from": "source-map@0.4.2", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.2.tgz" - }, - "sshpk": { - "version": "1.7.4", - "from": "sshpk@>=1.7.0 <2.0.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.7.4.tgz" - }, - "statuses": { - "version": "1.2.1", - "from": "statuses@>=1.2.1 <2.0.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz" - }, - "stream-browserify": { - "version": "2.0.1", - "from": "stream-browserify@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz" - }, - "stream-combiner2": { - "version": "1.1.1", - "from": "stream-combiner2@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz" - }, - "stream-http": { - "version": "2.2.1", - "from": "stream-http@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.2.1.tgz" - }, - "stream-splicer": { - "version": "2.0.0", - "from": "stream-splicer@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz" - }, - "string_decoder": { - "version": "0.10.31", - "from": "string_decoder@>=0.10.0 <0.11.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - }, - "string-template": { - "version": "1.0.0", - "from": "string-template@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-1.0.0.tgz" - }, - "string-width": { - "version": "1.0.1", - "from": "string-width@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.1.tgz" - }, - "stringstream": { - "version": "0.0.5", - "from": "stringstream@>=0.0.4 <0.1.0", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz" - }, - "strip-ansi": { - "version": "3.0.1", - "from": "strip-ansi@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - }, - "strip-bom": { - "version": "2.0.0", - "from": "strip-bom@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz" - }, - "strip-json-comments": { - "version": "1.0.4", - "from": "strip-json-comments@>=1.0.4 <1.1.0", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz" - }, - "subarg": { - "version": "1.0.0", - "from": "subarg@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "dependencies": { - "minimist": { - "version": "1.2.0", - "from": "minimist@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz" - } - } - }, - "supports-color": { - "version": "2.0.0", - "from": "supports-color@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" - }, - "syntax-error": { - "version": "1.1.6", - "from": "syntax-error@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.1.6.tgz", - "dependencies": { - "acorn": { - "version": "2.7.0", - "from": "acorn@>=2.7.0 <3.0.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz" - } - } - }, - "thenify": { - "version": "3.2.0", - "from": "thenify@>=3.1.0 <4.0.0", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.2.0.tgz" - }, - "through": { - "version": "2.3.8", - "from": "through@>=2.2.7 <3.0.0", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" - }, - "through2": { - "version": "2.0.1", - "from": "through2@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz" - }, - "timed-out": { - "version": "2.0.0", - "from": "timed-out@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz" - }, - "timers-browserify": { - "version": "1.4.2", - "from": "timers-browserify@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz" - }, - "tiny-lr": { - "version": "0.2.1", - "from": "tiny-lr@>=0.2.1 <0.3.0", - "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-0.2.1.tgz", - "dependencies": { - "body-parser": { - "version": "1.14.2", - "from": "body-parser@>=1.14.0 <1.15.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.2.tgz", - "dependencies": { - "qs": { - "version": "5.2.0", - "from": "qs@5.2.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz" - } - } - }, - "http-errors": { - "version": "1.3.1", - "from": "http-errors@>=1.3.1 <1.4.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz" - }, - "qs": { - "version": "5.1.0", - "from": "qs@>=5.1.0 <5.2.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-5.1.0.tgz" - } - } - }, - "to-array": { - "version": "0.1.4", - "from": "to-array@0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz" - }, - "to-arraybuffer": { - "version": "1.0.1", - "from": "to-arraybuffer@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz" - }, - "touch": { - "version": "1.0.0", - "from": "touch@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-1.0.0.tgz" - }, - "tough-cookie": { - "version": "2.2.2", - "from": "tough-cookie@>=2.2.0 <2.3.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz" - }, - "tty-browserify": { - "version": "0.0.0", - "from": "tty-browserify@>=0.0.0 <0.1.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz" - }, - "tunnel-agent": { - "version": "0.4.2", - "from": "tunnel-agent@>=0.4.1 <0.5.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.2.tgz" - }, - "tweetnacl": { - "version": "0.14.3", - "from": "tweetnacl@>=0.13.0 <1.0.0", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.3.tgz" - }, - "type-is": { - "version": "1.6.12", - "from": "type-is@>=1.6.11 <1.7.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.12.tgz" - }, - "typedarray": { - "version": "0.0.6", - "from": "typedarray@>=0.0.5 <0.1.0", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" - }, - "typescript": { - "version": "1.8.9", - "from": "typescript@1.8.9", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-1.8.9.tgz" - }, - "typings": { - "version": "0.7.11", - "from": "typings@0.7.11", - "resolved": "https://registry.npmjs.org/typings/-/typings-0.7.11.tgz", - "dependencies": { - "minimist": { - "version": "1.2.0", - "from": "minimist@>=1.2.0 <2.0.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz" - } - } - }, - "typings-core": { - "version": "0.2.14", - "from": "typings-core@>=0.2.14 <0.3.0", - "resolved": "https://registry.npmjs.org/typings-core/-/typings-core-0.2.14.tgz", - "dependencies": { - "graceful-fs": { - "version": "4.1.3", - "from": "graceful-fs@>=4.1.2 <5.0.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz" - } - } - }, - "ultron": { - "version": "1.0.2", - "from": "ultron@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz" - }, - "umd": { - "version": "3.0.1", - "from": "umd@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.1.tgz" - }, - "unc-path-regex": { - "version": "0.1.1", - "from": "unc-path-regex@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.1.tgz" - }, - "underscore": { - "version": "1.7.0", - "from": "underscore@>=1.7.0 <1.8.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz" - }, - "underscore.string": { - "version": "2.2.1", - "from": "underscore.string@>=2.2.1 <2.3.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz" - }, - "unpipe": { - "version": "1.0.0", - "from": "unpipe@1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" - }, - "unzip-response": { - "version": "1.0.0", - "from": "unzip-response@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.0.tgz" - }, - "update-notifier": { - "version": "0.6.3", - "from": "update-notifier@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-0.6.3.tgz" - }, - "url": { - "version": "0.11.0", - "from": "url@>=0.11.0 <0.12.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "dependencies": { - "punycode": { - "version": "1.3.2", - "from": "punycode@1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz" - } - } - }, - "url-parse-lax": { - "version": "1.0.0", - "from": "url-parse-lax@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz" - }, - "utf-8-validate": { - "version": "1.2.1", - "from": "utf-8-validate@1.2.1", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-1.2.1.tgz" - }, - "utf8": { - "version": "2.1.0", - "from": "utf8@2.1.0", - "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.0.tgz" - }, - "util": { - "version": "0.10.3", - "from": "util@>=0.10.1 <0.11.0", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz" - }, - "util-deprecate": { - "version": "1.0.2", - "from": "util-deprecate@>=1.0.1 <1.1.0", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - }, - "utils-merge": { - "version": "1.0.0", - "from": "utils-merge@1.0.0", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz" - }, - "uuid": { - "version": "2.0.1", - "from": "uuid@>=2.0.1 <3.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz" - }, - "vary": { - "version": "1.1.0", - "from": "vary@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.0.tgz" - }, - "verror": { - "version": "1.3.6", - "from": "verror@1.3.6", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz" - }, - "vm-browserify": { - "version": "0.0.4", - "from": "vm-browserify@>=0.0.1 <0.1.0", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz" - }, - "watchify": { - "version": "3.7.0", - "from": "watchify@>=3.6.1 <4.0.0", - "resolved": "https://registry.npmjs.org/watchify/-/watchify-3.7.0.tgz" - }, - "wcwidth": { - "version": "1.0.0", - "from": "wcwidth@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.0.tgz" - }, - "weak-map": { - "version": "1.0.5", - "from": "weak-map@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.5.tgz" - }, - "websocket-driver": { - "version": "0.6.4", - "from": "websocket-driver@>=0.5.1", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.4.tgz" - }, - "websocket-extensions": { - "version": "0.1.1", - "from": "websocket-extensions@>=0.1.1", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.1.tgz" - }, - "which": { - "version": "1.0.9", - "from": "which@>=1.0.5 <1.1.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz" - }, - "widest-line": { - "version": "1.0.0", - "from": "widest-line@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-1.0.0.tgz" - }, - "wordwrap": { - "version": "1.0.0", - "from": "wordwrap@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" - }, - "wrappy": { - "version": "1.0.1", - "from": "wrappy@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" - }, - "write-file-atomic": { - "version": "1.1.4", - "from": "write-file-atomic@>=1.1.2 <2.0.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.1.4.tgz", - "dependencies": { - "graceful-fs": { - "version": "4.1.3", - "from": "graceful-fs@>=4.1.2 <5.0.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz" - } - } - }, - "ws": { - "version": "1.0.1", - "from": "ws@1.0.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.0.1.tgz" - }, - "xdg-basedir": { - "version": "2.0.0", - "from": "xdg-basedir@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz" - }, - "xml2js": { - "version": "0.4.16", - "from": "xml2js@>=0.4.5 <0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.16.tgz" - }, - "xmlbuilder": { - "version": "4.2.1", - "from": "xmlbuilder@>=4.1.0 <5.0.0", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz" - }, - "xmlhttprequest-ssl": { - "version": "1.5.1", - "from": "xmlhttprequest-ssl@1.5.1", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.1.tgz" - }, - "xtend": { - "version": "4.0.1", - "from": "xtend@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" - }, - "yallist": { - "version": "2.0.0", - "from": "yallist@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.0.0.tgz" - }, - "yeast": { - "version": "0.1.2", - "from": "yeast@0.1.2", - "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz" - }, - "zip-object": { - "version": "0.1.0", - "from": "zip-object@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/zip-object/-/zip-object-0.1.0.tgz" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 8a31e1cff..000000000 --- a/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "tribeca", - "version": "2.0.0", - "description": "A high frequency, market making cryptocurrency trading platform", - "main": "Gruntfile.js", - "directories": { - "test": "test" - }, - "dependencies": { - "agentkeepalive": "2.0.5", - "angular": "1.5.3", - "angular-ui-bootstrap": "1.2.5", - "angularjs": "0.0.1", - "basic-auth": "1.0.3", - "basic-auth-connect": "1.0.0", - "body-parser": "1.15.0", - "bufferutil": "1.2.1", - "bunyan": "1.8.0", - "collections": "3.0.0", - "compression": "1.6.1", - "connect": "3.4.1", - "express": "4.13.4", - "grunt": "0.4.5", - "grunt-browserify": "5.0.0", - "grunt-contrib-copy": "1.0.0", - "grunt-contrib-watch": "1.0.0", - "grunt-ts": "5.4.0", - "jquery": "2.2.2", - "lodash": "4.6.1", - "moment": "2.12.0", - "mongodb": "2.1.11", - "node-uuid": "1.4.7", - "q": "1.4.1", - "request": "2.69.0", - "shortid": "2.2.4", - "socket.io": "1.4.5", - "socket.io-client": "1.4.5", - "typescript": "1.8.9", - "typings": "0.7.11", - "utf-8-validate": "1.2.1", - "ws": "1.0.1" - }, - "devDependencies": { - "mocha": "2.4.5" - }, - "scripts": { - "test": "mocha" - }, - "repository": { - "type": "git", - "url": "git@github.com:michaelgrosner/tribeca.git" - }, - "author": "Michael Grosner", - "license": "ISC" -} diff --git a/sample-dev-tribeca.json b/sample-dev-tribeca.json deleted file mode 100644 index 0f4719328..000000000 --- a/sample-dev-tribeca.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "TRIBECA_MODE": "dev", - "EXCHANGE": "null", - "TradedPair": "BTC/USD", - "MongoDbUrl": "mongodb://localhost:27017/tribeca", - "WebClientUsername": "NULL", - "WebClientPassword": "NULL", - "WebClientListenPort": "3000", - - "HitBtcPullUrl": "http://demo-api.hitbtc.com", - "HitBtcOrderEntryUrl": "ws://demo-api.hitbtc.com:8080", - "HitBtcMarketDataUrl": "ws://demo-api.hitbtc.com:80", - "HitBtcSocketIoUrl": "https://demo-api.hitbtc.com:8081", - "HitBtcApiKey": "NULL", - "HitBtcSecret": "NULL", - "HitBtcOrderDestination": "HitBtc", - - "CoinbaseRestUrl": "https://api-public.sandbox.exchange.coinbase.com", - "CoinbaseWebsocketUrl": "https://ws-feed-public.sandbox.exchange.coinbase.com", - "CoinbasePassphrase": "NULL", - "CoinbaseApiKey": "NULL", - "CoinbaseSecret": "NULL", - "CoinbaseOrderDestination": "Coinbase", - - "OkCoinWsUrl": "wss://real.okcoin.com:10440/websocket/okcoinapi", - "OkCoinHttpUrl": "https://www.okcoin.com/api/v1/", - "OkCoinApiKey": "NULL", - "OkCoinSecretKey": "NULL", - "OkCoinOrderDestination": "OkCoin", - - "BitfinexHttpUrl": "https://api.bitfinex.com/v1", - "BitfinexKey": "NULL", - "BitfinexSecret": "NULL", - "BitfinexOrderDestination": "Bitfinex" -} diff --git a/sample-prod-tribeca.json b/sample-prod-tribeca.json deleted file mode 100644 index e915138dd..000000000 --- a/sample-prod-tribeca.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "TRIBECA_MODE": "prod", - "EXCHANGE": "null", - "TradedPair": "BTC/USD", - "MongoDbUrl": "mongodb://localhost:27017/tribeca", - "WebClientUsername": "NULL", - "WebClientPassword": "NULL", - "WebClientListenPort": "3000", - - "HitBtcPullUrl": "http://api.hitbtc.com", - "HitBtcOrderEntryUrl": "wss://api.hitbtc.com:8080", - "HitBtcMarketDataUrl": "ws://api.hitbtc.com:80", - "HitBtcSocketIoUrl": "https://api.hitbtc.com:8081", - "HitBtcApiKey": "NULL", - "HitBtcSecret": "NULL", - "HitBtcOrderDestination": "HitBtc", - - "CoinbaseRestUrl": "https://api.exchange.coinbase.com", - "CoinbaseWebsocketUrl": "https://ws-feed-public.sandbox.exchange.coinbase.com", - "CoinbasePassphrase": "NULL", - "CoinbaseApiKey": "NULL", - "CoinbaseSecret": "NULL", - "CoinbaseOrderDestination": "Coinbase", - - "OkCoinWsUrl": "wss://real.okcoin.com:10440/websocket/okcoinapi", - "OkCoinHttpUrl": "https://www.okcoin.com/api/v1/", - "OkCoinApiKey": "NULL", - "OkCoinSecretKey": "NULL", - "OkCoinOrderDestination": "OkCoin", - - "BitfinexHttpUrl": "https://api.bitfinex.com/v1", - "BitfinexKey": "NULL", - "BitfinexSecret": "NULL", - "BitfinexOrderDestination": "Bitfinex" -} diff --git a/src/.editorconfig b/src/.editorconfig new file mode 100644 index 000000000..b78c6f8f4 --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1,15 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +root = true + +[*.{ts,h}] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +# Note: The EditorConfig Notepad++ plugin supports the following EditorConfig properties: +# https://github.com/editorconfig/editorconfig-notepad-plus-plus#supported-properties \ No newline at end of file diff --git a/src/README.md b/src/README.md new file mode 100644 index 000000000..c11d3a227 --- /dev/null +++ b/src/README.md @@ -0,0 +1 @@ +

Imagine for one second, your home being bombed.
Just close your eyes, feel the fear nearby.

There are people who know exactly what it feels like.

Violence and war force them to jump into the sea,
looking for freedom, looking for peace,
but most of them only finding death.

So now we will stop looking away, and you will decide:



donate: openarms.es/en

or share: youtu.be/g3hvVGTuzUY

or give a little gift to your crush: regalosalvavidas.openarms.es/?ls=en





Now that we are together,
I will say what you and I know
and we often forget:

We have seen the fear
be the law for all.

We have seen the blood
-which only makes blood-
be the law of the world.

No.
I say no.
We say no.
We are not of that world.

We have seen hunger
be the bread
of the workers.

We have seen closed
in prison
men full of reason.

No.
I say no.
We say no.
We are not of that world.

No.
We say no.
We are not of that world.

diff --git a/src/admin/client.ts b/src/admin/client.ts deleted file mode 100644 index af48bf356..000000000 --- a/src/admin/client.ts +++ /dev/null @@ -1,140 +0,0 @@ -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// - -(global).jQuery = require("jquery"); -import angular = require("angular"); - -var ui_bootstrap = require("angular-ui-bootstrap"); -var ngGrid = require("../ui-grid.min"); -var bootstrap = require("../bootstrap.min"); - -import Models = require("../common/models"); -import moment = require("moment"); -import OrderList = require("./orderlist"); -import Trades = require("./trades"); -import Messaging = require("../common/messaging"); -import Shared = require("./shared_directives"); -import Pair = require("./pair"); -import MarketQuoting = require("./market-quoting"); -import MarketTrades = require("./market-trades"); -import Messages = require("./messages"); -import Position = require("./position"); -import Tbp = require("./target-base-position"); -import TradeSafety = require("./trade-safety"); - -interface MainWindowScope extends ng.IScope { - env : string; - connected : boolean; - order : DisplayOrder; - pair : Pair.DisplayPair; - exch_name : string; - pair_name : string; - cancelAllOrders(); -} - -class DisplayOrder { - side : string; - price : number; - quantity : number; - timeInForce : string; - orderType : string; - - availableSides : string[]; - availableTifs : string[]; - availableOrderTypes : string[]; - - private static getNames(enumObject : T) { - var names : string[] = []; - for (var mem in enumObject) { - if (!enumObject.hasOwnProperty(mem)) continue; - if (parseInt(mem, 10) >= 0) { - names.push(enumObject[mem]); - } - } - return names; - } - - private _fire : Messaging.IFire; - constructor(fireFactory : Shared.FireFactory, private _log : ng.ILogService) { - this.availableSides = DisplayOrder.getNames(Models.Side); - this.availableTifs = DisplayOrder.getNames(Models.TimeInForce); - this.availableOrderTypes = DisplayOrder.getNames(Models.OrderType); - - this._fire = fireFactory.getFire(Messaging.Topics.SubmitNewOrder); - } - - public submit = () => { - var msg = new Models.OrderRequestFromUI(this.side, this.price, this.quantity, this.timeInForce, this.orderType); - this._log.info("submitting order", msg); - this._fire.fire(msg); - }; -} - -var uiCtrl = ($scope : MainWindowScope, - $timeout : ng.ITimeoutService, - $log : ng.ILogService, - subscriberFactory : Shared.SubscriberFactory, - fireFactory : Shared.FireFactory) => { - - var cancelAllFirer = fireFactory.getFire(Messaging.Topics.CancelAllOrders); - $scope.cancelAllOrders = () => cancelAllFirer.fire(new Models.CancelAllOrdersRequest()); - - $scope.order = new DisplayOrder(fireFactory, $log); - $scope.pair = null; - - var onAdvert = (pa : Models.ProductAdvertisement) => { - $log.info("advert", pa); - $scope.connected = true; - $scope.env = pa.environment; - $scope.pair_name = Models.Currency[pa.pair.base] + "/" + Models.Currency[pa.pair.quote]; - $scope.exch_name = Models.Exchange[pa.exchange]; - $scope.pair = new Pair.DisplayPair($scope, subscriberFactory, fireFactory); - }; - - var reset = (reason : string) => { - $log.info("reset", reason); - $scope.connected = false; - $scope.pair_name = null; - $scope.exch_name = null; - - if ($scope.pair !== null) - $scope.pair.dispose(); - $scope.pair = null; - }; - reset("startup"); - - var sub = subscriberFactory.getSubscriber($scope, Messaging.Topics.ProductAdvertisement) - .registerSubscriber(onAdvert, a => a.forEach(onAdvert)) - .registerDisconnectedHandler(() => reset("disconnect")); - - $scope.$on('$destroy', () => { - sub.disconnect(); - $log.info("destroy client"); - }); - - $log.info("started client"); -}; - -var requires = ['ui.bootstrap', - 'ui.grid', - OrderList.orderListDirective, - Trades.tradeListDirective, - MarketQuoting.marketQuotingDirective, - MarketTrades.marketTradeDirective, - Messages.messagesDirective, - Position.positionDirective, - Tbp.targetBasePositionDirective, - TradeSafety.tradeSafetyDirective, - Shared.sharedDirectives]; - -angular.module('projectApp', requires) - .controller('uiCtrl', uiCtrl); \ No newline at end of file diff --git a/src/admin/market-quoting.ts b/src/admin/market-quoting.ts deleted file mode 100644 index 0d70960e4..000000000 --- a/src/admin/market-quoting.ts +++ /dev/null @@ -1,197 +0,0 @@ -/// -/// -/// - -import angular = require("angular"); -import Models = require("../common/models"); -import io = require("socket.io-client"); -import moment = require("moment"); -import Messaging = require("../common/messaging"); -import Shared = require("./shared_directives"); - -class Level { - bidPrice: number; - bidSize: number; - askPrice: number; - askSize: number; - - bidClass: string; - askClass: string; -} - -interface MarketQuotingScope extends ng.IScope { - levels: Level[]; - qBidSz: number; - qBidPx: number; - fairValue: number; - qAskPx: number; - qAskSz: number; - extVal: number; - - bidIsLive: boolean; - askIsLive: boolean; -} - -var MarketQuotingController = ($scope: MarketQuotingScope, - $log: ng.ILogService, - subscriberFactory: Shared.SubscriberFactory) => { - var clearMarket = () => { - $scope.levels = []; - }; - clearMarket(); - - var clearBid = () => { - $scope.qBidPx = null; - $scope.qBidSz = null; - }; - - var clearAsk = () => { - $scope.qAskPx = null; - $scope.qAskSz = null; - }; - - var clearQuote = () => { - clearBid(); - clearAsk(); - }; - - var clearFairValue = () => { - $scope.fairValue = null; - }; - - var clearQuoteStatus = () => { - $scope.bidIsLive = false; - $scope.askIsLive = false; - }; - - var clearExtVal = () => { - $scope.extVal = null; - }; - - var updateMarket = (update: Models.Market) => { - if (update == null) { - clearMarket(); - return; - } - - for (var i = 0; i < update.asks.length; i++) { - if (angular.isUndefined($scope.levels[i])) - $scope.levels[i] = new Level(); - $scope.levels[i].askPrice = update.asks[i].price; - $scope.levels[i].askSize = update.asks[i].size; - } - - for (var i = 0; i < update.bids.length; i++) { - if (angular.isUndefined($scope.levels[i])) - $scope.levels[i] = new Level(); - $scope.levels[i].bidPrice = update.bids[i].price; - $scope.levels[i].bidSize = update.bids[i].size; - } - - updateQuoteClass(); - }; - - var updateQuote = (quote: Models.TwoSidedQuote) => { - if (quote !== null) { - if (quote.bid !== null) { - $scope.qBidPx = quote.bid.price; - $scope.qBidSz = quote.bid.size; - } - else { - clearBid(); - } - - if (quote.ask !== null) { - $scope.qAskPx = quote.ask.price; - $scope.qAskSz = quote.ask.size; - } - else { - clearAsk(); - } - } - else { - clearQuote(); - } - - updateQuoteClass(); - }; - - var updateQuoteStatus = (status: Models.TwoSidedQuoteStatus) => { - if (status == null) { - clearQuoteStatus(); - return; - } - - $scope.bidIsLive = (status.bidStatus === Models.QuoteStatus.Live); - $scope.askIsLive = (status.askStatus === Models.QuoteStatus.Live); - updateQuoteClass(); - }; - - var updateQuoteClass = () => { - if (!angular.isUndefined($scope.levels) && $scope.levels.length > 0) { - var tol = .005; - for (var i = 0; i < $scope.levels.length; i++) { - var level = $scope.levels[i]; - - if (Math.abs($scope.qBidPx - level.bidPrice) < tol && $scope.bidIsLive) { - level.bidClass = 'success'; - } - else { - level.bidClass = 'active'; - } - - if (Math.abs($scope.qAskPx - level.askPrice) < tol && $scope.askIsLive) { - level.askClass = 'success'; - } - else { - level.askClass = 'active'; - } - } - } - }; - - var updateFairValue = (fv: Models.FairValue) => { - if (fv == null) { - clearFairValue(); - return; - } - - $scope.fairValue = fv.price; - }; - - var subscribers = []; - - var makeSubscriber = (topic: string, updateFn, clearFn) => { - var sub = subscriberFactory.getSubscriber($scope, topic) - .registerSubscriber(updateFn, ms => ms.forEach(updateFn)) - .registerDisconnectedHandler(clearFn); - subscribers.push(sub); - }; - - makeSubscriber(Messaging.Topics.MarketData, updateMarket, clearMarket); - makeSubscriber(Messaging.Topics.Quote, updateQuote, clearQuote); - makeSubscriber(Messaging.Topics.QuoteStatus, updateQuoteStatus, clearQuoteStatus); - makeSubscriber(Messaging.Topics.FairValue, updateFairValue, clearFairValue); - - $scope.$on('$destroy', () => { - subscribers.forEach(d => d.disconnect()); - $log.info("destroy market quoting grid"); - }); - - $log.info("started market quoting grid"); -}; - -export var marketQuotingDirective = "marketQuotingDirective"; - -angular - .module(marketQuotingDirective, ['ui.bootstrap', 'ui.grid', Shared.sharedDirectives]) - .directive("marketQuotingGrid", () => { - - return { - restrict: 'E', - replace: true, - transclude: false, - templateUrl: "market_display.html", - controller: MarketQuotingController - } - }); \ No newline at end of file diff --git a/src/admin/market-trades.ts b/src/admin/market-trades.ts deleted file mode 100644 index 0e4ea6ab7..000000000 --- a/src/admin/market-trades.ts +++ /dev/null @@ -1,131 +0,0 @@ -/// -/// -/// - -import angular = require("angular"); -import Models = require("../common/models"); -import io = require("socket.io-client"); -import moment = require("moment"); -import Messaging = require("../common/messaging"); -import Shared = require("./shared_directives"); - -class MarketTradeViewModel { - price: number; - size: number; - time: moment.Moment; - - qA: number; - qB: number; - qAz: number; - qBz: number; - - mA: number; - mB: number; - mAz: number; - mBz: number; - - make_side: string; - - constructor(trade: Models.MarketTrade) { - this.price = MarketTradeViewModel.round(trade.price); - this.size = MarketTradeViewModel.round(trade.size); - this.time = (moment.isMoment(trade.time) ? trade.time : moment(trade.time)); - - if (trade.quote != null) { - if (trade.quote.ask !== null) { - this.qA = MarketTradeViewModel.round(trade.quote.ask.price); - this.qAz = MarketTradeViewModel.round(trade.quote.ask.size); - } - - if (trade.quote.bid !== null) { - this.qB = MarketTradeViewModel.round(trade.quote.bid.price); - this.qBz = MarketTradeViewModel.round(trade.quote.bid.size); - } - } - - if (trade.ask != null) { - this.mA = MarketTradeViewModel.round(trade.ask.price); - this.mAz = MarketTradeViewModel.round(trade.ask.size); - } - - if (trade.bid != null) { - this.mB = MarketTradeViewModel.round(trade.bid.price); - this.mBz = MarketTradeViewModel.round(trade.bid.size); - } - - this.make_side = Models.Side[trade.make_side]; - } - - private static round(num: number) { - return Math.round(num * 100) / 100; - } -} - -interface MarketTradeScope extends ng.IScope { - marketTrades: MarketTradeViewModel[]; - marketTradeOptions: Object; -} - -var MarketTradeGrid = ($scope: MarketTradeScope, - $log: ng.ILogService, - subscriberFactory: Shared.SubscriberFactory, - uiGridConstants: any) => { - $scope.marketTrades = []; - $scope.marketTradeOptions = { - data: 'marketTrades', - showGroupPanel: false, - rowHeight: 20, - headerRowHeight: 20, - groupsCollapsedByDefault: true, - enableColumnResize: true, - sortInfo: { fields: ['time'], directions: ['desc'] }, - columnDefs: [ - { width: 80, field: 'time', displayName: 't', cellFilter: "momentShortDate", - sortingAlgorithm: (a: moment.Moment, b: moment.Moment) => a.diff(b), - sort: { direction: uiGridConstants.DESC, priority: 1} }, - { width: 50, field: 'price', displayName: 'px' }, - { width: 40, field: 'size', displayName: 'sz' }, - { width: 40, field: 'make_side', displayName: 'ms' }, - { width: 40, field: 'qBz', displayName: 'qBz' }, - { width: 50, field: 'qB', displayName: 'qB' }, - { width: 50, field: 'qA', displayName: 'qA' }, - { width: 40, field: 'qAz', displayName: 'qAz' }, - { width: 40, field: 'mBz', displayName: 'mBz' }, - { width: 50, field: 'mB', displayName: 'mB' }, - { width: 50, field: 'mA', displayName: 'mA' }, - { width: 40, field: 'mAz', displayName: 'mAz' } - ] - }; - - var addNewMarketTrade = (u: Models.MarketTrade) => { - if (u != null) - $scope.marketTrades.push(new MarketTradeViewModel(u)); - }; - - var sub = subscriberFactory.getSubscriber($scope, Messaging.Topics.MarketTrade) - .registerSubscriber(addNewMarketTrade, x => x.forEach(addNewMarketTrade)) - .registerDisconnectedHandler(() => $scope.marketTrades.length = 0); - - $scope.$on('$destroy', () => { - sub.disconnect(); - $log.info("destroy market trade grid"); - }); - - $log.info("started market trade grid"); -}; - -export var marketTradeDirective = "marketTradeDirective"; - -angular - .module(marketTradeDirective, ['ui.bootstrap', 'ui.grid', Shared.sharedDirectives]) - .directive("marketTradeGrid", () => { - var template = '
'; - - return { - restrict: 'E', - replace: true, - transclude: false, - template: template, - controller: MarketTradeGrid - } - }); \ No newline at end of file diff --git a/src/admin/messages.ts b/src/admin/messages.ts deleted file mode 100644 index ce8e7964e..000000000 --- a/src/admin/messages.ts +++ /dev/null @@ -1,75 +0,0 @@ -/// -/// -/// -/// - -import angular = require("angular"); -import Models = require("../common/models"); -import io = require("socket.io-client"); -import moment = require("moment"); -import Messaging = require("../common/messaging"); -import Shared = require("./shared_directives"); - -class MessageViewModel { - text: string; - time: moment.Moment; - - constructor(message: Models.Message) { - this.time = (moment.isMoment(message.time) ? message.time : moment(message.time)); - this.text = message.text; - } -} - -interface MessageLoggerScope extends ng.IScope { - messages: MessageViewModel[]; - messageOptions: Object; -} - -var MessagesController = ($scope: MessageLoggerScope, $log: ng.ILogService, subscriberFactory: Shared.SubscriberFactory) => { - $scope.messages = []; - $scope.messageOptions = { - data: 'messages', - showGroupPanel: false, - rowHeight: 20, - headerRowHeight: 0, - hideHeader: true, - groupsCollapsedByDefault: true, - enableColumnResize: true, - sortInfo: { fields: ['time'], directions: ['desc'] }, - columnDefs: [ - { width: 120, field: 'time', displayName: 't', cellFilter: 'momentFullDate' }, - { width: "*", field: 'text', displayName: 'text' } - ] - }; - - var addNewMessage = (u: Models.Message) => { - $scope.messages.push(new MessageViewModel(u)); - }; - - var sub = subscriberFactory.getSubscriber($scope, Messaging.Topics.Message) - .registerSubscriber(addNewMessage, x => x.forEach(addNewMessage)) - .registerDisconnectedHandler(() => $scope.messages.length = 0); - - $scope.$on('$destroy', () => { - sub.disconnect(); - $log.info("destroy message grid"); - }); - - $log.info("started message grid"); -}; - -export var messagesDirective = "messagesDirective"; - -angular - .module(messagesDirective, ['ui.bootstrap', 'ui.grid', Shared.sharedDirectives]) - .directive("messagesGrid", () => { - var template = '
'; - - return { - restrict: 'E', - replace: true, - transclude: false, - template: template, - controller: MessagesController - } - }); diff --git a/src/admin/orderlist.ts b/src/admin/orderlist.ts deleted file mode 100644 index 4c51b5f03..000000000 --- a/src/admin/orderlist.ts +++ /dev/null @@ -1,171 +0,0 @@ -/// -/// - -import angular = require("angular"); -import Models = require("../common/models"); -import io = require("socket.io-client"); -import moment = require("moment"); -import Messaging = require("../common/messaging"); -import Shared = require("./shared_directives"); - -interface OrderListScope extends ng.IScope { - order_statuses: DisplayOrderStatusReport[]; - gridOptions: any; -} - -class DisplayOrderStatusReport { - orderId: string; - time: moment.Moment; - orderStatus: string; - price: number; - quantity: number; - side: string; - orderType: string; - tif: string; - computationalLatency: number; - lastQuantity: number; - lastPrice: number; - leavesQuantity: number; - cumQuantity: number; - averagePrice: number; - liquidity: string; - rejectMessage: string; - version: number; - trackable: string; - - constructor(public osr: Models.OrderStatusReport, - private _fireCxl: Messaging.IFire) { - this.orderId = osr.orderId; - this.side = Models.Side[osr.side]; - this.updateWith(osr); - } - - public updateWith = (osr: Models.OrderStatusReport) => { - this.time = (moment.isMoment(osr.time) ? osr.time : moment(osr.time)); - this.orderStatus = DisplayOrderStatusReport.getOrderStatus(osr); - this.price = osr.price; - this.quantity = osr.quantity; - this.orderType = Models.OrderType[osr.type]; - this.tif = Models.TimeInForce[osr.timeInForce]; - this.computationalLatency = osr.computationalLatency; - this.lastQuantity = osr.lastQuantity; - this.lastPrice = osr.lastPrice; - this.leavesQuantity = osr.leavesQuantity; - this.cumQuantity = osr.cumQuantity; - this.averagePrice = osr.averagePrice; - this.liquidity = Models.Liquidity[osr.liquidity]; - this.rejectMessage = osr.rejectMessage; - this.version = osr.version; - this.trackable = osr.orderId + ":" + osr.version; - }; - - private static getOrderStatus(o: Models.OrderStatusReport): string { - var endingModifier = (o: Models.OrderStatusReport) => { - if (o.pendingCancel) - return ", PndCxl"; - else if (o.pendingReplace) - return ", PndRpl"; - else if (o.partiallyFilled) - return ", PartFill"; - else if (o.cancelRejected) - return ", CxlRj"; - return ""; - }; - return Models.OrderStatus[o.orderStatus] + endingModifier(o); - } - - public cancel = () => { - this._fireCxl.fire(this.osr); - }; -} - -var OrderListController = ($scope: OrderListScope, - $log: ng.ILogService, - subscriberFactory: Shared.SubscriberFactory, - fireFactory: Shared.FireFactory, - uiGridConstants: any) => { - var fireCxl = fireFactory.getFire(Messaging.Topics.CancelOrder); - - $scope.order_statuses = []; - $scope.gridOptions = { - data: 'order_statuses', - primaryKey: 'orderId', - groupsCollapsedByDefault: true, - treeRowHeaderAlwaysVisible: false, - enableColumnResize: true, - rowHeight: 20, - headerRowHeight: 20, - columnDefs: [ - { width: 120, field: 'time', displayName: 'time', cellFilter: "momentFullDate", - sortingAlgorithm: (a: moment.Moment, b: moment.Moment) => a.diff(b), - sort: { direction: uiGridConstants.DESC, priority: 1} }, - { width: 90, field: 'orderId', displayName: 'id' }, - { width: 35, field: 'version', displayName: 'v' }, - { width: 120, field: 'orderStatus', displayName: 'status' }, - { width: 65, field: 'price', displayName: 'px', cellFilter: 'currency' }, - { width: 60, field: 'quantity', displayName: 'qty' }, - { width: 50, field: 'side', displayName: 'side' }, - { width: 50, field: 'orderType', displayName: 'type' }, - { width: 50, field: 'tif', displayName: 'tif' }, - { width: 35, field: 'computationalLatency', displayName: 'lat' }, - { width: 60, field: 'lastQuantity', displayName: 'lQty' }, - { width: 65, field: 'lastPrice', displayName: 'lPx', cellFilter: 'currency' }, - { width: 60, field: 'leavesQuantity', displayName: 'lvQty' }, - { width: 60, field: 'cumQuantity', displayName: 'cum' }, - { width: 65, field: 'averagePrice', displayName: 'avg', cellFilter: 'currency' }, - { width: 40, field: 'liquidity', displayName: 'liq' }, - { width: "*", field: 'rejectMessage', displayName: 'msg' }, - { width: 40, name: "cancel", displayName: 'cxl', cellTemplate: '' }, - ] - }; - - var idsToIndex = {}; - var addOrderRpt = (o: Models.OrderStatusReport) => { - var idx = idsToIndex[o.orderId]; - if (typeof idx === "undefined") { - idsToIndex[o.orderId] = $scope.order_statuses.length; - $scope.order_statuses.push(new DisplayOrderStatusReport(o, fireCxl)); - } - else { - var existing = $scope.order_statuses[idx]; - if (existing.version < o.version) { - existing.updateWith(o); - } - } - }; - - var clear = () => { - $scope.order_statuses.length = 0; - idsToIndex = {}; - }; - - var sub = subscriberFactory.getSubscriber($scope, Messaging.Topics.OrderStatusReports) - .registerConnectHandler(clear) - .registerDisconnectedHandler(clear) - .registerSubscriber(addOrderRpt, os => os.forEach(addOrderRpt)); - - $scope.$on('$destroy', () => { - sub.disconnect(); - $log.info("destroy order list"); - }); - - $log.info("started order list"); -}; - -var orderList = (): ng.IDirective => { - var template = '
'; - - return { - template: template, - restrict: "E", - replace: true, - transclude: false, - controller: OrderListController - } -}; - -export var orderListDirective = "orderListDirective"; - -angular.module(orderListDirective, ['ui.bootstrap', 'ui.grid', "ui.grid.grouping", Shared.sharedDirectives]) - .controller('OrderListController', OrderListController) - .directive("orderList", orderList); diff --git a/src/admin/pair.ts b/src/admin/pair.ts deleted file mode 100644 index 156fd3bd4..000000000 --- a/src/admin/pair.ts +++ /dev/null @@ -1,135 +0,0 @@ -/// -/// -/// - -import angular = require("angular"); -import Models = require("../common/models"); -import io = require("socket.io-client"); -import moment = require("moment"); -import Messaging = require("../common/messaging"); -import Shared = require("./shared_directives"); - -class FormViewModel { - master: T; - display: T; - pending: boolean = false; - connected: boolean = false; - - constructor(defaultParameter: T, - private _sub: Messaging.ISubscribe, - private _fire: Messaging.IFire, - private _submitConverter: (disp: T) => T = null) { - if (this._submitConverter === null) - this._submitConverter = d => d; - - _sub.registerConnectHandler(() => this.connected = true) - .registerDisconnectedHandler(() => this.connected = false) - .registerSubscriber(this.update, us => us.forEach(this.update)); - - this.connected = _sub.connected; - this.master = angular.copy(defaultParameter); - this.display = angular.copy(defaultParameter); - } - - public reset = () => { - this.display = angular.copy(this.master); - }; - - public update = (p: T) => { - console.log("updating parameters", p); - this.master = angular.copy(p); - this.display = angular.copy(p); - this.pending = false; - }; - - public submit = () => { - this.pending = true; - this._fire.fire(this._submitConverter(this.display)); - }; -} - -class QuotingButtonViewModel extends FormViewModel { - constructor(sub: Messaging.ISubscribe, - fire: Messaging.IFire) { - super(false, sub, fire, d => !d); - } - - public getClass = () => { - if (this.pending) return "btn btn-warning"; - if (this.display) return "btn btn-success"; - return "btn btn-danger"; - } -} - -class DisplayQuotingParameters extends FormViewModel { - availableQuotingModes = []; - availableFvModels = []; - availableAutoPositionModes = []; - - constructor(sub: Messaging.ISubscribe, - fire: Messaging.IFire) { - super(new Models.QuotingParameters(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null), sub, fire); - - this.availableQuotingModes = DisplayQuotingParameters.getMapping(Models.QuotingMode); - this.availableFvModels = DisplayQuotingParameters.getMapping(Models.FairValueModel); - this.availableAutoPositionModes = DisplayQuotingParameters.getMapping(Models.AutoPositionMode); - } - - private static getMapping(enumObject: T) { - var names = []; - for (var mem in enumObject) { - if (!enumObject.hasOwnProperty(mem)) continue; - var val = parseInt(mem, 10); - if (val >= 0) { - names.push({ 'str': enumObject[mem], 'val': val }); - } - } - return names; - } -} - -export class DisplayPair { - name: string; - connected: boolean; - - active: QuotingButtonViewModel; - quotingParameters: DisplayQuotingParameters; - - private _subscribers: Messaging.ISubscribe[] = []; - - constructor(public scope: ng.IScope, - subscriberFactory: Shared.SubscriberFactory, - fireFactory: Shared.FireFactory) { - - var setConnectStatus = (cs: Models.ConnectivityStatus) => { - this.connected = cs == Models.ConnectivityStatus.Connected; - }; - - var connectivitySubscriber = subscriberFactory.getSubscriber(scope, Messaging.Topics.ExchangeConnectivity) - .registerSubscriber(setConnectStatus, cs => cs.forEach(setConnectStatus)); - this._subscribers.push(connectivitySubscriber); - - var activeSub = subscriberFactory.getSubscriber(scope, Messaging.Topics.ActiveChange); - this.active = new QuotingButtonViewModel( - activeSub, - fireFactory.getFire(Messaging.Topics.ActiveChange) - ); - this._subscribers.push(activeSub); - - var qpSub = subscriberFactory.getSubscriber(scope, Messaging.Topics.QuotingParametersChange); - this.quotingParameters = new DisplayQuotingParameters( - qpSub, - fireFactory.getFire(Messaging.Topics.QuotingParametersChange) - ); - this._subscribers.push(qpSub); - } - - public dispose = () => { - console.log("dispose client"); - this._subscribers.forEach(s => s.disconnect()); - }; - - public updateParameters = (p: Models.QuotingParameters) => { - this.quotingParameters.update(p); - }; -} \ No newline at end of file diff --git a/src/admin/position.ts b/src/admin/position.ts deleted file mode 100644 index 3a433c773..000000000 --- a/src/admin/position.ts +++ /dev/null @@ -1,76 +0,0 @@ -/// -/// -/// -/// -/// - -import angular = require("angular"); -import Models = require("../common/models"); -import io = require("socket.io-client"); -import moment = require("moment"); -import Messaging = require("../common/messaging"); -import Pair = require("./pair"); -import Shared = require("./shared_directives"); - -interface PositionScope extends ng.IScope { - baseCurrency : string; - basePosition : number; - quoteCurrency : string; - quotePosition : number; - baseHeldPosition : number; - quoteHeldPosition : number; - value : number; - quoteValue : number; -} - -var PositionController = ($scope : PositionScope, $log : ng.ILogService, subscriberFactory : Shared.SubscriberFactory) => { - var clearPosition = () => { - $scope.baseCurrency = null; - $scope.quoteCurrency = null; - $scope.basePosition = null; - $scope.quotePosition = null; - $scope.baseHeldPosition = null; - $scope.quoteHeldPosition = null; - $scope.value = null; - $scope.quoteValue = null; - }; - - var updatePosition = (position : Models.PositionReport) => { - $scope.baseCurrency = Models.Currency[position.pair.base]; - $scope.quoteCurrency = Models.Currency[position.pair.quote]; - $scope.basePosition = position.baseAmount; - $scope.quotePosition = position.quoteAmount; - $scope.baseHeldPosition = position.baseHeldAmount; - $scope.quoteHeldPosition = position.quoteHeldAmount; - $scope.value = position.value; - $scope.quoteValue = position.quoteValue; - }; - - var positionSubscriber = subscriberFactory.getSubscriber($scope, Messaging.Topics.Position) - .registerDisconnectedHandler(clearPosition) - .registerSubscriber(updatePosition, us => us.forEach(updatePosition)); - - $scope.$on('$destroy', () => { - positionSubscriber.disconnect(); - $log.info("destroy position grid"); - }); - - $log.info("started position grid"); -}; - -export var positionDirective = "positionDirective"; - -angular - .module(positionDirective, ['ui.bootstrap', 'sharedDirectives']) - .directive("positionGrid", () => { - return { - restrict: 'E', - replace: true, - transclude: false, - templateUrl: "positions.html", - controller: PositionController, - scope: { - exch: '=' - } - } - }); diff --git a/src/admin/shared_directives.ts b/src/admin/shared_directives.ts deleted file mode 100644 index 64779770b..000000000 --- a/src/admin/shared_directives.ts +++ /dev/null @@ -1,100 +0,0 @@ -/// -/// -/// -/// - -import angular = require("angular"); -import Messaging = require("../common/messaging"); -import Models = require("../common/models"); -import io = require("socket.io-client"); - -var mypopover = ($compile : ng.ICompileService, $templateCache : ng.ITemplateCacheService) => { - var getTemplate = (contentType, template_url) => { - var template = ''; - switch (contentType) { - case 'user': - template = $templateCache.get(template_url); - break; - } - return template; - }; - return { - restrict: "A", - link: (scope, element, attrs) => { - var popOverContent = $compile("
" + getTemplate("user", attrs.popoverTemplate) + "
")(scope); - var options = { - content: popOverContent, - placement: attrs.dataPlacement, - html: true, - date: scope.date - }; - (jQuery(element)).popover(options).click((e) => { - e.preventDefault(); - }); - } - }; -}; - -var bindOnce = () => { - return { - scope: true, - link: ($scope) => { - setTimeout(() => { - $scope.$destroy(); - }, 0); - } - } -}; - -export class FireFactory { - constructor(private socket : any, private $log : ng.ILogService) {} - - public getFire = (topic : string) : Messaging.IFire => { - return new Messaging.Fire(topic, this.socket, this.$log.info); - } -} - -export class SubscriberFactory { - constructor(private socket : any, private $log : ng.ILogService) {} - - public getSubscriber = (scope : ng.IScope, topic : string) : Messaging.ISubscribe => { - return new EvalAsyncSubscriber(scope, topic, this.socket, this.$log.info); - } -} - -export class EvalAsyncSubscriber implements Messaging.ISubscribe { - private _wrapped : Messaging.ISubscribe; - - constructor(private _scope : ng.IScope, topic : string, io : any, log : (...args: any[]) => void) { - this._wrapped = new Messaging.Subscriber(topic, io, log); - } - - public registerSubscriber = (incrementalHandler : (msg : T) => void, snapshotHandler : (msgs : T[]) => void) => { - return this._wrapped.registerSubscriber( - x => this._scope.$evalAsync(() => incrementalHandler(x)), - xs => this._scope.$evalAsync(() => snapshotHandler(xs))) - }; - - public registerDisconnectedHandler = (handler : () => void) => { - return this._wrapped.registerDisconnectedHandler(() => this._scope.$evalAsync(handler)); - }; - - public registerConnectHandler = (handler : () => void) => { - return this._wrapped.registerConnectHandler(() => this._scope.$evalAsync(handler)); - }; - - public disconnect = () => this._wrapped.disconnect(); - - public get connected() { return this._wrapped.connected; } -} - -export var sharedDirectives = "sharedDirectives"; - -angular.module(sharedDirectives, ['ui.bootstrap']) - .directive('mypopover', mypopover) - .directive('bindOnce', bindOnce) - .factory("socket", () : SocketIOClient.Socket => io()) - .service("subscriberFactory", SubscriberFactory) - .service("fireFactory", FireFactory) - .filter("momentFullDate", () => Models.toUtcFormattedTime) - .filter("momentShortDate", () => Models.toShortTimeString); diff --git a/src/admin/target-base-position.ts b/src/admin/target-base-position.ts deleted file mode 100644 index f2a507a82..000000000 --- a/src/admin/target-base-position.ts +++ /dev/null @@ -1,51 +0,0 @@ -/// -/// -/// -/// - -import angular = require("angular"); -import Models = require("../common/models"); -import io = require("socket.io-client"); -import moment = require("moment"); -import Messaging = require("../common/messaging"); -import Pair = require("./pair"); -import Shared = require("./shared_directives"); - -interface TargetBasePositionScope extends ng.IScope { - targetBasePosition : number; -} - -var TargetBasePositionController = ($scope : TargetBasePositionScope, $log : ng.ILogService, subscriberFactory : Shared.SubscriberFactory) => { - - var update = (value : Models.TargetBasePositionValue) => { - if (value == null) return; - $scope.targetBasePosition = value.data; - }; - - var subscriber = subscriberFactory.getSubscriber($scope, Messaging.Topics.TargetBasePosition) - .registerDisconnectedHandler(() => $scope.targetBasePosition = null) - .registerSubscriber(update, us => us.forEach(update)); - - $scope.$on('$destroy', () => { - subscriber.disconnect(); - $log.info("destroy target base position"); - }); - - $log.info("started target base position"); -}; - -export var targetBasePositionDirective = "targetBasePositionDirective"; - -angular - .module(targetBasePositionDirective, ['sharedDirectives']) - .directive("targetBasePosition", () => { - var template = '{{ targetBasePosition|number:2 }}'; - - return { - restrict: 'E', - replace: true, - transclude: false, - template: template, - controller: TargetBasePositionController - } - }); \ No newline at end of file diff --git a/src/admin/trade-safety.ts b/src/admin/trade-safety.ts deleted file mode 100644 index a105bac3c..000000000 --- a/src/admin/trade-safety.ts +++ /dev/null @@ -1,61 +0,0 @@ -/// -/// -/// -/// - -import angular = require("angular"); -import Models = require("../common/models"); -import io = require("socket.io-client"); -import moment = require("moment"); -import Messaging = require("../common/messaging"); -import Pair = require("./pair"); -import Shared = require("./shared_directives"); - -interface TradeSafetyScope extends ng.IScope { - buySafety: number; - sellSafety: number; - tradeSafetyValue : number; -} - -var TradeSafetyController = ($scope : TradeSafetyScope, $log : ng.ILogService, subscriberFactory : Shared.SubscriberFactory) => { - - var updateValue = (value : Models.TradeSafety) => { - if (value == null) return; - $scope.tradeSafetyValue = value.combined; - $scope.buySafety = value.buy; - $scope.sellSafety = value.sell; - }; - - var clear = () => { - $scope.tradeSafetyValue = null; - $scope.buySafety = null; - $scope.sellSafety = null; - }; - - var subscriber = subscriberFactory.getSubscriber($scope, Messaging.Topics.TradeSafetyValue) - .registerDisconnectedHandler(clear) - .registerSubscriber(updateValue, us => us.forEach(updateValue)); - - $scope.$on('$destroy', () => { - subscriber.disconnect(); - $log.info("destroy trade safety"); - }); - - $log.info("started trade safety"); -}; - -export var tradeSafetyDirective = "tradeSafetyDirective"; - -angular - .module(tradeSafetyDirective, ['sharedDirectives']) - .directive("tradeSafety", () => { - var template = 'BuyTS: {{ buySafety|number:2 }}, SellTS: {{ sellSafety|number:2 }}, TotalTS: {{ tradeSafetyValue|number:2 }}'; - - return { - restrict: 'E', - replace: true, - transclude: false, - template: template, - controller: TradeSafetyController - } - }); \ No newline at end of file diff --git a/src/admin/trades.ts b/src/admin/trades.ts deleted file mode 100644 index 646459f3d..000000000 --- a/src/admin/trades.ts +++ /dev/null @@ -1,111 +0,0 @@ -/// -/// - -import angular = require("angular"); -import Models = require("../common/models"); -import io = require("socket.io-client"); -import moment = require("moment"); -import Messaging = require("../common/messaging"); -import Shared = require("./shared_directives"); - -interface TradesScope extends ng.IScope { - trade_statuses : DisplayTrade[]; - exch : Models.Exchange; - pair : Models.CurrencyPair; - gridOptions : any; -} - -class DisplayTrade { - tradeId : string; - time : moment.Moment; - price : number; - quantity : number; - side : string; - value : number; - liquidity : string; - - constructor(public trade : Models.Trade) { - this.tradeId = trade.tradeId; - this.side = trade.side === Models.Side.Ask ? "S" : "B"; - this.time = (moment.isMoment(trade.time) ? trade.time : moment(trade.time)); - this.price = trade.price; - this.quantity = trade.quantity; - this.value = trade.value; - - if (trade.liquidity === 0 || trade.liquidity === 1) { - this.liquidity = Models.Liquidity[trade.liquidity].charAt(0); - } - else { - this.liquidity = "?"; - } - } -} - -var TradesListController = ($scope : TradesScope, $log : ng.ILogService, subscriberFactory : Shared.SubscriberFactory, uiGridConstants: any) => { - $scope.trade_statuses = []; - $scope.gridOptions = { - data: 'trade_statuses', - treeRowHeaderAlwaysVisible: false, - primaryKey: 'tradeId', - groupsCollapsedByDefault: true, - enableColumnResize: true, - sortInfo: {fields: ['time'], directions: ['desc']}, - rowHeight: 20, - headerRowHeight: 20, - columnDefs: [ - {width: 80, field:'time', displayName:'t', cellFilter: 'momentShortDate', - sortingAlgorithm: (a: moment.Moment, b: moment.Moment) => a.diff(b), - sort: { direction: uiGridConstants.DESC, priority: 1} }, - {width: 55, field:'price', displayName:'px', cellFilter: 'currency'}, - {width: 50, field:'quantity', displayName:'qty'}, - {width: 30, field:'side', displayName:'side', cellClass: (grid, row, col, rowRenderIndex, colRenderIndex) => { - if (grid.getCellValue(row, col) === 'B') { - return 'buy'; - } - else if (grid.getCellValue(row, col) === 'S') { - return "sell"; - } - else { - return "unknown"; - } - }}, - {width: 30, field:'liquidity', displayName:'liq'}, - {width: 60, field:'value', displayName:'val', cellFilter: 'currency:"$":3'} - ] - }; - - var addTrade = t => $scope.trade_statuses.push(new DisplayTrade(t)); - - var sub = subscriberFactory.getSubscriber($scope, Messaging.Topics.Trades) - .registerConnectHandler(() => $scope.trade_statuses.length = 0) - .registerDisconnectedHandler(() => $scope.trade_statuses.length = 0) - .registerSubscriber(addTrade, trades => trades.forEach(addTrade)); - - $scope.$on('$destroy', () => { - sub.disconnect(); - $log.info("destroy trades list"); - }); - - $log.info("started trades list"); -}; - -var tradeList = () : ng.IDirective => { - var template = '
'; - - return { - template: template, - restrict: "E", - replace: true, - transclude: false, - controller: TradesListController, - scope: { - exch: '=', - pair: '=' - } - } -}; - -export var tradeListDirective = "tradeListDirective"; - -angular.module(tradeListDirective, ['ui.bootstrap', 'ui.grid', "ui.grid.grouping", Shared.sharedDirectives]) - .directive("tradeList", tradeList); diff --git a/src/bin/+portfolios/+portfolios.client/Client.ts b/src/bin/+portfolios/+portfolios.client/Client.ts new file mode 100644 index 000000000..4e3a242fd --- /dev/null +++ b/src/bin/+portfolios/+portfolios.client/Client.ts @@ -0,0 +1,70 @@ +import {Component, OnInit, Input} from '@angular/core'; + +import {Socket, Models} from 'lib/K'; + +@Component({ + selector: 'client', + template: `
+
+ +
+
+
+ + + +
+
+ +
+
+
` +}) +export class ClientComponent implements OnInit { + + private wallets: any = null; + + private markets: any = null; + + private orders: Models.Order[] = []; + + private settings: Models.PortfolioParameters = new Models.PortfolioParameters(); + + private showSubmitOrder: boolean = false; + + @Input() addr: string; + + @Input() tradeFreq: number; + + @Input() state: Models.ExchangeState; + + @Input() product: Models.ProductAdvertisement; + + ngOnInit() { + new Socket.Subscriber(Models.Topics.QuotingParametersChange) + .registerSubscriber((o: Models.PortfolioParameters) => { this.settings = o; }); + + new Socket.Subscriber(Models.Topics.MarketData) + .registerSubscriber((o: any) => { this.markets = o; }) + .registerDisconnectedHandler(() => { this.markets = null; }); + + new Socket.Subscriber(Models.Topics.Position) + .registerSubscriber((o: any[]) => { this.wallets = o; }) + .registerDisconnectedHandler(() => { this.wallets = null; }); + + new Socket.Subscriber(Models.Topics.OrderStatusReports) + .registerSubscriber((o: Models.Order[]) => { this.orders = o; }) + .registerDisconnectedHandler(() => { this.orders = []; }); + }; +}; diff --git a/src/bin/+portfolios/+portfolios.client/Markets.ts b/src/bin/+portfolios/+portfolios.client/Markets.ts new file mode 100644 index 000000000..c63b4d390 --- /dev/null +++ b/src/bin/+portfolios/+portfolios.client/Markets.ts @@ -0,0 +1,161 @@ +import {Component, Input, Output, EventEmitter} from '@angular/core'; + +import {GridOptions, GridApi} from 'ag-grid-community'; + +import {Shared, Models} from 'lib/K'; + +@Component({ + selector: 'markets', + template: `` +}) +export class MarketsComponent { + + private _markets: any = null; + private _market: any = null; + + @Output() rendered = new EventEmitter(); + + @Input() settings: Models.PortfolioParameters; + + @Input() set markets(o: any) { + this._markets = o; + this.addRowData(); + }; + + @Input() set market(o: any) { + this._market = o; + this.addRowData(); + }; + + private api: GridApi; + + private grid: GridOptions = { + overlayLoadingTemplate: `missing data`, + overlayNoRowsTemplate: `missing data`, + defaultColDef: { sortable: true, resizable: true, flex: 0 }, + rowHeight:35, + headerHeight:35, + domLayout: 'autoHeight', + animateRows:true, + enableCellTextSelection: true, + getRowId: (params: any) => params.data.currency, + columnDefs: [{ + width: 120, + field: 'currency', + headerName: 'markets', + sort: 'asc', + cellRenderer: (params) => ` ` + params.value + `` + }, { + width: 220, + field: 'price', + headerName: 'price', + type: 'rightAligned', + cellRenderer: (params) => `1 = ` + params.value + ` `, + cellClassRules: { + 'text-muted': '!parseFloat(x)', + 'up-data': 'data.dir_price == "up-data"', + 'down-data': 'data.dir_price == "down-data"' + }, + comparator: Shared.comparator + }, { + width: 170, + field: 'spread', + headerName: 'spread', + type: 'rightAligned', + cellRenderer: (params) => `` + params.value + `` + ` `, + cellClassRules: { + 'text-muted': '!parseFloat(x)', + 'up-data': 'data.dir_spread == "up-data"', + 'down-data': 'data.dir_spread == "down-data"' + }, + comparator: Shared.comparator + }, { + width: 80, + field: 'open', + headerName: '24h price', + type: 'rightAligned', + cellRenderer: (params) => `` + ['','+'][+(Shared.num(params.value) > 0)] + params.value + ``, + cellClassRules: { + 'text-muted': '!parseFloat(x)', + 'up-data': 'data.dir_open == "up-data"', + 'down-data': 'data.dir_open == "down-data"' + }, + comparator: Shared.comparator + }, { + width: 140, + field: 'volume', + headerName: '24h volume', + type: 'rightAligned', + cellRenderer: (params) => `` + params.value + `` + ` `, + cellClassRules: { + 'text-muted': '!parseFloat(x)', + 'up-data': 'data.dir_volume == "up-data"', + 'down-data': 'data.dir_volume == "down-data"' + }, + comparator: Shared.comparator + }] + }; + + private onGridReady($event: any) { + if ($event.api) this.api = $event.api; + }; + + private onGridTheme() { + return document.body.classList.contains('theme-dark') + ? '-dark' : ''; + }; + + private addRowData = () => { + if (!this.api) return; + if (!this._market || !this._markets) + return this.api.setGridOption('rowData', []); + var o = {}; + for (let x in this._markets) + if (x == this._market) + for (let z in this._markets[x]) + o[z] = this._markets[x][z]; + else if (this._markets[x].hasOwnProperty(this._market)) + o[x] = this._markets[x][this._market]; + if (!Object.keys(o).length) + return this.api.setGridOption('rowData', []); + for (let x in o) { + const price = Shared.str(o[x].price, 8); + const spread = Shared.str(o[x].spread, 8); + const open = Shared.str(o[x].open, 2); + const volume = Shared.str(o[x].volume, 0); + var node: any = this.api.getRowNode(x); + if (!node) + this.api.applyTransaction({add: [{ + currency: x, + web: o[x].web, + base: o[x].base, + quote: o[x].quote, + price, + spread, + open, + volume + }]}); + else + this.api.flashCells({ + rowNodes: [node], + columns: [].concat(Shared.resetRowData('price', price, node)) + .concat(Shared.resetRowData('spread', spread, node)) + .concat(Shared.resetRowData('open', open, node)) + .concat(Shared.resetRowData('volume', volume, node)) + }); + } + + this.api.onSortChanged(); + + this.rendered.emit(true); + }; +}; diff --git a/src/bin/+portfolios/+portfolios.client/Orders.ts b/src/bin/+portfolios/+portfolios.client/Orders.ts new file mode 100644 index 000000000..1f790db0c --- /dev/null +++ b/src/bin/+portfolios/+portfolios.client/Orders.ts @@ -0,0 +1,287 @@ +import {Component, Input} from '@angular/core'; + +import {GridOptions, GridApi} from 'ag-grid-community'; + +import {Shared, Socket, Models} from 'lib/K'; + +@Component({ + selector: 'orders', + template: `` +}) +export class OrdersComponent { + + private fireCxl: Socket.IFire = new Socket.Fire(Models.Topics.CancelOrder); + + private editCxl: Socket.IFire = new Socket.Fire(Models.Topics.EditOrder); + + @Input() product: Models.ProductAdvertisement; + + private best_ask: string; + private best_bid: string; + private quote: string = ""; + private base: string = ""; + private orders_market: string; + + private _markets: any = null; + + @Input() set markets(o: any) { + this._markets = o; + this.addAskBid(); + }; + + @Input() set orders(o: Models.Order[]) { + this.addRowData(o); + }; + + private symbols: any[] = []; + private filter: string; + + private api: GridApi; + + private grid: GridOptions = { + suppressNoRowsOverlay: true, + defaultColDef: { sortable: true, resizable: true, flex: 1 }, + rowHeight:35, + headerHeight:35, + domLayout: 'autoHeight', + getRowId: (params: any) => params.data.exchangeId, + isExternalFilterPresent: () => !!this.filter, + doesExternalFilterPass: (node) => ( + !this.filter || node.data.symbol == this.filter + ), + columnDefs: [{ + width: 20, + field: "cancel", + flex: 0, + headerName: 'cxl', + suppressSizeToFit: true, + cellRenderer: (params) => `` + }, { + width: 74, + field: 'price', + headerName: 'price', + sort: 'desc', + editable: true, + cellEditorSelector: (params) => { + return { component: "agNumberCellEditor", params: { + precision: params.data.pricePrecision, + min: parseFloat(Math.pow(10, -params.data.pricePrecision).toFixed(params.data.pricePrecision)), + step: parseFloat(Math.pow(10, -params.data.pricePrecision).toFixed(params.data.pricePrecision)), + showStepperButtons: true + } }; + }, + cellRenderer: (params) => Shared.str(params.value, params.data.pricePrecision) + ` ` + ' ', + cellClassRules: { + 'sell': 'data.side == "Ask"', + 'buy': 'data.side == "Bid"' + } + }, { + width: 95, + field: 'quantity', + headerName: 'qty', + suppressSizeToFit: true, + cellRenderer: (params) => Shared.str(params.value, params.data.quantityPrecision) + ` `, + cellClassRules: { + 'sell': 'data.side == "Ask"', + 'buy': 'data.side == "Bid"' + } + }, { + width: 80, + field: 'side', + headerName: 'side', + flex: 0, + suppressSizeToFit: true, + cellClassRules: { + 'sell': 'data.side == "Ask"', + 'buy': 'data.side == "Bid"' + }, + cellRenderer: (params) => ( + params.value == "Ask" + ? '' + : '' + ) + params.value + }, { + width: 74, + field: 'value', + headerName: 'value', + cellRenderer: (params) => Shared.str(params.value, params.data.pricePrecision) + ` `, + cellClassRules: { + 'sell': 'data.side == "Ask"', + 'buy': 'data.side == "Bid"' + } + }, { + width: 100, + field: 'time', + flex: 0, + headerName: 'time', + suppressSizeToFit: true, + cellRenderer: (params) => { + var d = new Date(params.value||0); + return (d.getHours()+'') + .padStart(2, "0")+':'+(d.getMinutes()+'') + .padStart(2, "0")+':'+(d.getSeconds()+'') + .padStart(2, "0")+','+(d.getMilliseconds()+'') + .padStart(3, "0"); + } + }, { + width: 75, + field: 'type', + headerName: 'type', + flex: 0, + suppressSizeToFit: true + }, { + width: 50, + field: 'tif', + flex: 0, + headerName: 'tif' + }, { + width: 75, + field: 'lat', + flex: 0, + headerName: 'lat' + }, { + width: 130, + field: 'exchangeId', + flex: 0, + headerName: 'openOrderId', + suppressSizeToFit: true, + cellRenderer: (params) => params.value + ? params.value.toString().split('-')[0] + : '' + }] + }; + + private onGridReady($event: any) { + if ($event.api) this.api = $event.api; + }; + + private onCellClicked = ($event) => { + if ($event.event.target.getAttribute('data-action-type') != 'remove') return; + this.fireCxl.fire(new Models.OrderCancelRequestFromUI($event.data.orderId, $event.data.exchange)); + }; + + private onCellValueChanged = ($event) => { + this.editCxl.fire(new Models.OrderEditRequestFromUI( + $event.data.orderId, + parseFloat($event.data.price), + $event.data.quantity, + +($event.data.side == "Ask"), + $event.data.symbol + )); + }; + + private addAskBid = () => { + if (!this.filter || !this._markets) return; + loops: for (let x in this._markets) + for (let z in this._markets[x]) + if (this.filter == this._markets[x][z].symbol) { + var precision = this.symbols.filter(s => s.symbol == this.filter)[0].pricePrecision; + this.best_ask = Shared.str(this._markets[x][z].ask, precision); + this.best_bid = Shared.str(this._markets[x][z].bid, precision); + this.orders_market = this._markets[x][z].web; + this.base = this._markets[x][z].base; + this.quote = this._markets[x][z].quote; + break loops; + } + }; + + private applyFilter = (filter) => { + this.filter = filter; + this.api.onFilterChanged(); + this.addAskBid(); + }; + + private addRowData = (o: Models.Order[]) => { + if (!this.api) return; + + this.symbols.forEach(s => s.bids = s.asks = 0); + + var add: any[] = [], + update: any[] = [], + remove: any[] = []; + + this.api.forEachNode((rowNode, index) => { + remove.push({exchangeId: rowNode.data.exchangeId}); + }); + + o.forEach(o => { + (remove.filter(x => x.exchangeId == o.exchangeId).length + ? update : add + ).push({ + symbol: o.symbol, + orderId: o.orderId, + exchangeId: o.exchangeId, + side: Models.Side[o.side], + price: o.price, + value: o.quantity * o.price, + type: Models.OrderType[o.type], + tif: Models.TimeInForce[o.timeInForce], + lat: o.latency < 0 ? 'loaded' : o.latency + 'ms', + quantity: o.quantity, + pong: o.isPong, + time: o.time, + pricePrecision: -Math.log10(o.pricePrecision), + quantityPrecision: -Math.log10(o.quantityPrecision) + }); + + remove = remove.filter(x => x.exchangeId != o.exchangeId); + + this.addSymbol(o.symbol, o.side, -Math.log10(o.pricePrecision)); + }); + + this.api.applyTransaction({add, update, remove}); + + if (!this.filter && this.symbols.length) { + this.applyFilter(this.symbols[0].symbol) + } + }; + + private addSymbol(sym: string, side: Models.Side, pricePrecision: number) { + if (!this.symbols.filter(s => s.symbol == sym).length) { + this.symbols.push({ + symbol: sym, + pricePrecision: pricePrecision, + bids: 0, + asks: 0 + }); + this.symbols.sort((a,b) => a.symbol.localeCompare(b.symbol)); + } + this.symbols.forEach(s => { + if (s.symbol == sym) { + if (side == Models.Side.Bid) + s.bids++; + else s.asks++; + } + }); + }; +}; diff --git a/src/bin/+portfolios/+portfolios.client/Settings.ts b/src/bin/+portfolios/+portfolios.client/Settings.ts new file mode 100644 index 000000000..e03dc6bb9 --- /dev/null +++ b/src/bin/+portfolios/+portfolios.client/Settings.ts @@ -0,0 +1,47 @@ +import {Component, Input} from '@angular/core'; + +import {Socket, Models} from 'lib/K'; + +@Component({ + selector: 'settings', + template: `
+
+ + +
+

+ 0.00000000 +

+
` +}) +export class SettingsComponent { + + private params: Models.PortfolioParameters = JSON.parse(JSON.stringify(new Models.PortfolioParameters())); + private pending: boolean = false; + + private fireCxl: Socket.IFire = new Socket.Fire(Models.Topics.QuotingParametersChange); + + @Input() product: Models.ProductAdvertisement; + + @Input() set settings(o: Models.PortfolioParameters) { + this.params = JSON.parse(JSON.stringify(o)); + this.pending = false; + setTimeout(() => {window.dispatchEvent(new Event('resize'))}, 0); + }; + + private submitSettings = () => { + this.pending = true; + this.fireCxl.fire(this.params); + }; +}; diff --git a/src/bin/+portfolios/+portfolios.client/Submit.ts b/src/bin/+portfolios/+portfolios.client/Submit.ts new file mode 100644 index 000000000..a820ded49 --- /dev/null +++ b/src/bin/+portfolios/+portfolios.client/Submit.ts @@ -0,0 +1,117 @@ +import {Component, Input} from '@angular/core'; + +import {Socket, Models} from 'lib/K'; + +@Component({ + selector: 'submit-order', + template: ` + + + + + + + + + + + + + + + + + + + + + +
Symbol:Side:Price:Size:TIF:Type:
+ + + + + + + + + + + + + +
` +}) +export class SubmitComponent { + + private symbol: string; + private side: Models.Side = Models.Side.Bid; + private price: number; + private quantity: number; + private timeInForce: Models.TimeInForce = Models.TimeInForce.GTC; + private type: Models.OrderType = Models.OrderType.Limit; + + private availableSides: Models.Map[] = Models.getMap(Models.Side); + private availableTifs: Models.Map[] = Models.getMap(Models.TimeInForce); + private availableOrderTypes: Models.Map[] = Models.getMap(Models.OrderType); + private availableSymbols: string[] = []; + + private fireCxl: Socket.IFire = new Socket.Fire(Models.Topics.SubmitNewOrder); + + + @Input() set markets(o: any) { + if (!this.availableSymbols.length) + for (let x in o) + for (let z in o[x]) + if (!this.availableSymbols.filter(s => s == o[x][z].symbol).length) { + this.availableSymbols.push(o[x][z].symbol); + this.availableSymbols.sort(); + } + }; + + private submitManualOrder = () => { + if (this.price && this.quantity) + this.fireCxl.fire(new Models.OrderRequestFromUI( + this.symbol, + this.side, + this.price, + this.quantity, + this.timeInForce, + this.type + )); + }; +}; diff --git a/src/bin/+portfolios/+portfolios.client/Wallets.ts b/src/bin/+portfolios/+portfolios.client/Wallets.ts new file mode 100644 index 000000000..e67595163 --- /dev/null +++ b/src/bin/+portfolios/+portfolios.client/Wallets.ts @@ -0,0 +1,243 @@ +import {Component, Input} from '@angular/core'; + +import {GridOptions, GridApi, RowNode} from 'ag-grid-community'; + +import {Shared, Models} from 'lib/K'; + +@Component({ + selector: 'wallets', + template: `
+ + +
` +}) +export class WalletsComponent { + + private deferredRender: any = null; + + private market: any = null; + + private selection: string = ""; + + @Input() product: Models.ProductAdvertisement; + + @Input() markets: any; + + @Input() set wallets(o: any) { + this.addRowData(o); + }; + + @Input() settings: Models.PortfolioParameters; + + private onRendered = ($event) => { + if (this.deferredRender) setTimeout(this.deferredRender, 0); + }; + + private onRowClicked = ($event) => { + if (!$event.data.currency) return; + if (this.selection == $event.data.currency) { + this.api.deselectAll(); + this.selection = ""; + } + else this.selection = $event.data.currency; + }; + + private api: GridApi; + + private grid: GridOptions = { + overlayLoadingTemplate: `missing data`, + overlayNoRowsTemplate: `missing data`, + defaultColDef: { sortable: true, resizable: true, flex: 1 }, + rowHeight:35, + headerHeight:35, + domLayout: 'autoHeight', + animateRows:true, + rowSelection: { + mode: "singleRow", + checkboxes: false, + enableClickSelection: true + }, + enableCellTextSelection: true, + onSelectionChanged: () => { + this.market = null; + this.api.forEachNode((node: RowNode) => { + node.setRowHeight(this.grid.rowHeight); + }); + var node: any = this.api.getSelectedNodes().reverse().pop(); + if (!node) return this.api.onRowHeightChanged(); + var detail = document.getElementById('markets'); + if (detail) { + var row = document.querySelector("#portfolios ag-grid-angular div[row-id='" + node.data.currency + "'] div[aria-colindex='3']"); + if (row) row.appendChild(detail); + } + this.deferredRender = () => { + if (detail) { + var style = (detail).style; + node.setRowHeight( + this.grid.rowHeight + + parseInt(style.marginTop) + + parseInt(style.marginBottom) + + (document.querySelector('#markets div.ag-root-wrapper')).offsetHeight + ); + this.api.onRowHeightChanged(); + } + }; + setTimeout(() => { + this.market = node.data.currency; + }, 0); + }, + isExternalFilterPresent: () => !this.settings.zeroed, + doesExternalFilterPass: (node) => ( + this.settings.zeroed || parseFloat(node.data.total) > 0.0000001 + ), + getRowId: (params: any) => params.data.currency, + columnDefs: [{ + width: 220, + field: 'held', + headerName: 'held', + type: 'rightAligned', + cellRenderer: (params) => `` + params.value + ` `, + cellClassRules: { + 'text-muted': '!parseFloat(x)', + 'up-data': 'data.dir_held == "up-data"', + 'down-data': 'data.dir_held == "down-data"' + }, + comparator: Shared.comparator + }, { + width: 220, + field: 'amount', + headerName: 'available', + type: 'rightAligned', + cellRenderer: (params) => `` + params.value + ` `, + cellClassRules: { + 'text-muted': '!parseFloat(x)', + 'up-data': 'data.dir_amount == "up-data"', + 'down-data': 'data.dir_amount == "down-data"' + }, + comparator: Shared.comparator + }, { + width: 220, + field: 'total', + headerName: 'total', + type: 'rightAligned', + cellRenderer: (params) => `` + params.value + ` `, + cellClassRules: { + 'text-muted': '!parseFloat(x)', + 'up-data': 'data.dir_total == "up-data"', + 'down-data': 'data.dir_total == "down-data"' + }, + comparator: Shared.comparator + }, { + width: 130, + field: 'currency', + headerName: 'currency', + filter: true, + cellClassRules: { + 'text-muted': '!parseFloat(data.total)' + } + }, { + width: 220, + field: 'price', + headerName: 'price', + type: 'rightAligned', + cellRenderer: (params) => `` + params.value + ` `, + cellClassRules: { + 'text-muted': '!parseFloat(x)', + 'up-data': 'data.dir_price == "up-data"', + 'down-data': 'data.dir_price == "down-data"' + }, + comparator: Shared.comparator + }, { + width: 220, + field: 'balance', + headerName: 'balance', + sort: 'desc', + type: 'rightAligned', + cellRenderer: (params) => `` + params.value + ` ` + + `` + (params.data.balance_percent||'0.00') + ``, + cellClassRules: { + 'text-muted': '!parseFloat(x)', + 'up-data': 'data.dir_balance == "up-data"', + 'down-data': 'data.dir_balance == "down-data"' + }, + comparator: Shared.comparator + }] + }; + + private onGridReady($event: any) { + if ($event.api) this.api = $event.api; + }; + + private onFilterChanged = ($event: any) => { + if (!this.selection) return; + var node: any = this.api.getRowNode(this.selection); + if (node && !this.grid.doesExternalFilterPass(node)) + this.onRowClicked({data:{currency:this.selection}}); + }; + + private addRowData = (o: any) => { + if (!this.api) return; + var sum = 0; + if (o === null) { + this.api.setGridOption('rowData', []); + this.selection = ""; + } + else o.forEach(o => { + var settingsPrecision = this.settings.currency == this.product.quote ? this.product.tickPrice : this.product.tickSize; + var has_settingsPrecision = this.settings.currency == o.wallet.currency; + const amount = Shared.str(o.wallet.amount, has_settingsPrecision ? settingsPrecision : -Math.log10(o.amountPrecision)); + const held = Shared.str(o.wallet.held, has_settingsPrecision ? settingsPrecision : -Math.log10(o.amountPrecision)); + const total = Shared.str(o.wallet.amount + o.wallet.held, has_settingsPrecision ? settingsPrecision : -Math.log10(o.amountPrecision)); + const balance = Shared.str(o.wallet.value, settingsPrecision); + const price = Shared.str(o.price, has_settingsPrecision ? settingsPrecision : -Math.log10(o.pricePrecision)); + sum += o.wallet.value; + var node: any = this.api.getRowNode(o.wallet.currency); + if (!node) + this.api.applyTransaction({add: [{ + currency: o.wallet.currency, + amount: amount, + held: held, + total: total, + balance: balance, + price: price + }]}); + else + this.api.flashCells({ + rowNodes: [node], + columns: [].concat(Shared.resetRowData('balance', balance, node)) + .concat(Shared.resetRowData('price', price, node)) + .concat(Shared.resetRowData('amount', amount, node)) + .concat(Shared.resetRowData('held', held, node)) + .concat(Shared.resetRowData('total', total, node)) + }); + }); + + this.api.onFilterChanged(); + + if (!this.api.getSelectedNodes().length) + this.api.onSortChanged(); + + this.api.forEachNode((node: RowNode) => { + node.data.balance_percent = Shared.str(Shared.num(node.data.balance) / sum * 100, 2); + }); + + var el = document.getElementById('full_balance'); + if (el) { + var val = Shared.str(sum, this.settings.currency == this.product.quote ? this.product.tickPrice : this.product.tickSize); + if (el.innerHTML != val) el.innerHTML = val; + } + }; +}; diff --git a/src/bin/+portfolios/+portfolios.data.h b/src/bin/+portfolios/+portfolios.data.h new file mode 100644 index 000000000..b42f2bc71 --- /dev/null +++ b/src/bin/+portfolios/+portfolios.data.h @@ -0,0 +1,524 @@ +//! \file +//! \brief Welcome user! (just a manager of portfolios). + +namespace analpaper { + struct Settings: public Sqlite::StructBackup, + public Client::Broadcast, + public Client::Clickable { + string currency = ""; + bool zeroed = true; + private_ref: + const KryptoNinja &K; + public: + Settings(const KryptoNinja &bot) + : StructBackup(bot) + , Broadcast(bot) + , Clickable(bot) + , K(bot) + {}; + void from_json(const json &j) { + currency = j.value("currency", K.gateway->quote); + zeroed = j.value("zeroed", zeroed); + if (currency.empty()) currency = K.gateway->quote; + K.clicked(this); + }; + void click(const json &j) override { + from_json(j); + backup(); + broadcast(); + }; + mMatter about() const override { + return mMatter::QuotingParameters; + }; + private: + string explain() const override { + return "Settings"; + }; + string explainKO() const override { + return "using default values for %"; + }; + }; + static void to_json(json &j, const Settings &k) { + j = { + {"currency", k.currency}, + { "zeroed", k.zeroed } + }; + }; + static void from_json(const json &j, Settings &k) { + k.from_json(j); + }; + + struct Portfolio { + Wallet wallet; + unordered_map prices; + unordered_map> precisions; + Price price; + pair precision; + }; + static void to_json(json &j, const Portfolio &k) { + j = { + { "wallet", k.wallet }, + { "price", k.price }, + { "pricePrecision", k.precision.first }, + {"amountPrecision", k.precision.second} + }; + }; + + struct Portfolios: public Client::Broadcast { + Settings settings; + unordered_map portfolio; + private_ref: + const KryptoNinja &K; + public: + Portfolios(const KryptoNinja &bot) + : Broadcast(bot) + , settings(bot) + , K(bot) + {}; + Price calc(const string &from) const { + if (from == settings.currency) + return 1; + if (portfolio.at(from).prices.contains(settings.currency)) + return portfolio.at(from).prices.at(settings.currency); + else for (const auto &it : portfolio.at(from).prices) + if (portfolio.contains(it.first)) { + if (portfolio.at(it.first).prices.contains(settings.currency)) + return it.second * portfolio.at(it.first).prices.at(settings.currency); + else for (const auto &_it : portfolio.at(it.first).prices) + if (portfolio.contains(_it.first) + and portfolio.at(_it.first).prices.contains(settings.currency) + ) return it.second * _it.second * portfolio.at(_it.first).prices.at(settings.currency); + } + return 0; + }; + void calc() { + for (auto &it : portfolio) { + portfolio.at(it.first).wallet.value = ( + portfolio.at(it.first).price = calc(it.first) + ) * portfolio.at(it.first).wallet.total; + portfolio.at(it.first).precision = portfolio.at(it.first).precisions.contains(settings.currency) + ? portfolio.at(it.first).precisions.at(settings.currency) : (pair){1e-8, 1e-8}; + } + if (ratelimit()) return; + broadcast(); + K.repaint(true); + }; + bool ready() const { + const bool err = portfolio.empty(); + if (err and Tspent > 21e+3) + K.warn("QE", "Unable to display portfolios, missing wallet data", 3e+3); + return !err; + }; + mMatter about() const override { + return mMatter::Position; + }; + private: + bool ratelimit() { + return !read_soon(); + }; + }; + static void to_json(json &j, const Portfolios &k) { + j = json::array(); + for (const auto &it : k.portfolio) + if (it.second.price + and (k.settings.zeroed or it.second.wallet.total) + ) j.push_back(it.second); + }; + + struct Market { + string web, + base, + quote, + symbol; + Price price, + spread, + raw_spread, + ask, + bid; + double open; + Amount volume, + raw_volume; + }; + static void to_json(json &j, const Market &k) { + j = { + { "web", k.web }, + { "base", k.base }, + { "quote", k.quote }, + {"symbol", k.symbol}, + { "price", k.price }, + {"spread", k.spread}, + { "ask", k.ask }, + { "bid", k.bid }, + { "open", k.open }, + {"volume", k.volume} + }; + }; + + struct Markets: public Client::Broadcast { + unordered_map> market; + private_ref: + const KryptoNinja &K; + public: + Markets(const KryptoNinja &bot) + : Broadcast(bot) + , K(bot) + {}; + void add(const Ticker &raw) { + market[raw.base][raw.quote] = { + K.gateway->web(raw.base, raw.quote), + raw.base, + raw.quote, + raw.symbol, + raw.price, + 0, + raw.spread, + raw.bestask, + raw.bestbid, + raw.open, + 0, + raw.volume + }; + }; + void calc(const string &base, const string "e, const Price &volume, const Price &spread) { + market[base][quote].volume = volume; + market[base][quote].spread = spread; + if (ratelimit()) return; + broadcast(); + }; + mMatter about() const override { + return mMatter::MarketData; + }; + private: + bool ratelimit() { + return !read_soon(1e+3); + }; + }; + static void to_json(json &j, const Markets &k) { + j = k.market; + }; + + struct Tickers: public Client::Clicked { + Markets markets; + private_ref: + const KryptoNinja &K; + Portfolios &portfolios; + public: + Tickers(const KryptoNinja &bot, Portfolios &p) + : Clicked(bot, { + {&p.settings, [&]() { calc(); }} + }) + , markets(bot) + , K(bot) + , portfolios(p) + {}; + void read_from_gw(const Ticker &raw) { + Price pricePrecision = 1e-8; + Amount amountPrecision = 1e-8; + if (K.gateway->precisions.contains(raw.symbol)) { + pricePrecision = K.gateway->precisions.at(raw.symbol).first; + amountPrecision = K.gateway->precisions.at(raw.symbol).second; + } + add(raw.base, raw.quote, raw.price, pricePrecision, amountPrecision); + add(raw.quote, raw.base, 1 / raw.price, amountPrecision, pricePrecision); + portfolios.calc(); + markets.add(raw); + markets.calc( + raw.base, + raw.quote, + raw.volume * portfolios.portfolio.at(raw.base).price, + raw.spread * portfolios.portfolio.at(raw.quote).price + ); + }; + private: + void calc() { + portfolios.calc(); + for (auto &it : markets.market) + for (auto &_it : it.second) { + markets.calc( + it.first, + _it.first, + _it.second.raw_volume * portfolios.portfolio.at(it.first).price, + _it.second.raw_spread * portfolios.portfolio.at(_it.first).price + ); + } + }; + void add(const string &base, const string "e, const Price &price, const Price &pricePrecision, const Amount &amountPrecision) { + portfolios.portfolio[base].prices[quote] = price; + portfolios.portfolio[base].precisions[quote] = {pricePrecision, amountPrecision}; + if (portfolios.portfolio.at(base).wallet.currency.empty()) + portfolios.portfolio.at(base).wallet.currency = base; + }; + }; + + struct Wallets { + private_ref: + Portfolios &portfolios; + public: + Wallets(Portfolios &p) + : portfolios(p) + {}; + void read_from_gw(const Wallet &raw) { + portfolios.portfolio[raw.currency].wallet = raw; + portfolios.calc(); + }; + }; + + struct Orders: public System::Orderbook, + public Client::Broadcast { + private_ref: + const KryptoNinja &K; + public: + Orders(const KryptoNinja &bot) + : Orderbook(bot) + , Broadcast(bot) + , K(bot) + { + withExternal = true; + }; + void read_from_gw(const Order &) { + broadcast(); + }; + mMatter about() const override { + return mMatter::OrderStatusReports; + }; + bool realtime() const override { + return false; + }; + json blob() const override { + return working(false); + }; + }; + static void to_json(json &j, const Orders &k) { + j = k.blob(); + }; + + struct Semaphore: public Client::Broadcast, + public Hotkey::Keymap { + Connectivity greenGateway = Connectivity::Disconnected; + private_ref: + const KryptoNinja &K; + public: + Semaphore(const KryptoNinja &bot) + : Broadcast(bot) + , Keymap(bot, { + {'Q', [&]() { exit(); }}, + {'q', [&]() { exit(); }}, + {'\e', [&]() { exit(); }} + }) + , K(bot) + {}; + bool online() const { + return (bool)greenGateway; + }; + void read_from_gw(const Connectivity &raw) { + greenGateway = raw; + broadcast(); + K.repaint(); + }; + mMatter about() const override { + return mMatter::Connectivity; + }; + }; + static void to_json(json &j, const Semaphore &k) { + j = { + {"online", k.greenGateway} + }; + }; + + struct Product: public Client::Broadcast { + private_ref: + const KryptoNinja &K; + public: + Product(const KryptoNinja &bot) + : Broadcast(bot) + , K(bot) + {}; + json to_json() const { + return { + { "exchange", K.gateway->exchange }, + { "base", K.gateway->base }, + { "quote", K.gateway->quote }, + { "symbol", K.gateway->symbol }, + { "webMarket", K.gateway->web() }, + { "webOrders", K.gateway->web(true) }, + { "tickPrice", K.gateway->decimal.price.stream.precision() }, + { "tickSize", K.gateway->decimal.amount.stream.precision()}, + { "stepPrice", K.gateway->decimal.price.step }, + { "stepSize", K.gateway->decimal.amount.step }, + { "minSize", K.gateway->minSize }, + { "inet", K.arg("interface") }, + {"environment", K.arg("title") }, + { "matryoshka", K.arg("matryoshka") }, + { "source", K_SOURCE " " K_BUILD } + }; + }; + mMatter about() const override { + return mMatter::ProductAdvertisement; + }; + }; + static void to_json(json &j, const Product &k) { + j = k.to_json(); + }; + + struct Memory: public Client::Broadcast { + public: + unsigned int orders_60s = 0; + private: + Product product; + private_ref: + const KryptoNinja &K; + public: + Memory(const KryptoNinja &bot) + : Broadcast(bot) + , product(bot) + , K(bot) + {}; + void timer_60s() { + broadcast(); + orders_60s = 0; + }; + json to_json() const { + return { + { "addr", K.gateway->unlock }, + { "freq", orders_60s }, + { "theme", K.arg("ignore-moon") + + K.arg("ignore-sun")}, + {"memory", K.memSize() }, + {"dbsize", K.dbSize() } + }; + }; + mMatter about() const override { + return mMatter::ApplicationState; + }; + }; + static void to_json(json &j, const Memory &k) { + j = k.to_json(); + }; + + struct ButtonSubmitNewOrder: public Client::Clickable { + private_ref: + const KryptoNinja &K; + public: + ButtonSubmitNewOrder(const KryptoNinja &bot) + : Clickable(bot) + , K(bot) + {}; + void click(const json &j) override { + if (j.is_object() + and !j.value("symbol", "").empty() + and j.value("price", 0.0) + and j.value("quantity", 0.0) + ) { + json order = j; + order["manual"] = true; + order["orderId"] = K.gateway->randId(); + K.clicked(this, order); + } + }; + mMatter about() const override { + return mMatter::SubmitNewOrder; + }; + }; + struct ButtonCancelOrder: public Client::Clickable { + private_ref: + const KryptoNinja &K; + public: + ButtonCancelOrder(const KryptoNinja &bot) + : Clickable(bot) + , K(bot) + {}; + void click(const json &j) override { + if (j.is_object() and !j.value("orderId", "").empty()) + K.clicked(this, j.at("orderId").get()); + }; + mMatter about() const override { + return mMatter::CancelOrder; + }; + }; + struct InputEditOrder: public Client::Clickable { + private_ref: + const KryptoNinja &K; + ButtonSubmitNewOrder &submit; + ButtonCancelOrder &cancel; + public: + InputEditOrder(const KryptoNinja &bot, ButtonSubmitNewOrder &s, ButtonCancelOrder &c) + : Clickable(bot) + , K(bot) + , submit(s) + , cancel(c) + {}; + void click(const json &j) override { + cancel.click(j); + submit.click(j); + }; + mMatter about() const override { + return mMatter::EditOrder; + }; + }; + + struct Buttons { + ButtonSubmitNewOrder submit; + ButtonCancelOrder cancel; + InputEditOrder edit; + Buttons(const KryptoNinja &bot) + : submit(bot) + , cancel(bot) + , edit(bot, submit, cancel) + {}; + }; + + struct Broker: public Client::Clicked { + Memory memory; + Semaphore semaphore; + private_ref: + const KryptoNinja &K; + public: + Broker(const KryptoNinja &bot, const Buttons &b) + : Clicked(bot, { + {&b.submit, [&](const json &j) { K.place(j); }}, + {&b.cancel, [&](const json &j) { K.cancel(j); }} + }) + , memory(bot) + , semaphore(bot) + , K(bot) + {}; + bool ready() const { + return semaphore.online(); + }; + }; + + class Engine { + public: + Portfolios portfolios; + Tickers ticker; + Wallets wallet; + Buttons button; + Orders orders; + Broker broker; + public: + Engine(const KryptoNinja &bot) + : portfolios(bot) + , ticker(bot, portfolios) + , wallet(portfolios) + , button(bot) + , orders(bot) + , broker(bot, button) + {}; + void read(const Connectivity &rawdata) { + broker.semaphore.read_from_gw(rawdata); + }; + void read(const Ticker &rawdata) { + ticker.read_from_gw(rawdata); + }; + void read(const Wallet &rawdata) { + wallet.read_from_gw(rawdata); + }; + void read(const Order &rawdata) { + orders.read_from_gw(rawdata); + }; + void timer_1s(const unsigned int &tick) { + if (!(tick % 60)) + broker.memory.timer_60s(); + }; + }; +} diff --git a/src/bin/+portfolios/+portfolios.disk.S b/src/bin/+portfolios/+portfolios.disk.S new file mode 100644 index 000000000..6e64a0b91 --- /dev/null +++ b/src/bin/+portfolios/+portfolios.disk.S @@ -0,0 +1,18 @@ +//! \file +//! \brief Lazy disk file builder using global external linkage. +//! \note Line 11 can be removed; just exemplifies each column. +//! \note See src/lib/Krypto.ninja-disk.S for info about DISK() +//! \note Webserver 404 page is loaded using an empty url path. +//! \note Filesystem paths at www/* are included from lib/, at: +//! - /var/lib/K/client/www/* +//! - ./src/lib/Krypto.ninja-client/www/* + +#define DISK(file) \ +/*file( id , www/filesystem/path/to/files , /webserver/url/paths )*/\ + file( 01 , www/index.html , / ) \ + file( 02 , www/favicon.ico , /favicon.ico ) \ + file( 03 , www/js/client.min.js , /js/client.min.js ) \ + file( 04 , www/css/bootstrap.min.css , /css/bootstrap.min.css ) \ + file( 05 , www/css/bootstrap-dark.min.css , /css/bootstrap-dark.min.css ) \ + file( 06 , www/font/beacons.woff2 , /font/beacons.woff2 ) \ + file( 00 , www/.bomb.gzip , ) diff --git a/src/bin/+portfolios/+portfolios.main.h b/src/bin/+portfolios/+portfolios.main.h new file mode 100644 index 000000000..128a16287 --- /dev/null +++ b/src/bin/+portfolios/+portfolios.main.h @@ -0,0 +1,80 @@ +class Portfolios: public KryptoNinja { + private: + analpaper::Engine engine; + public: + Portfolios() + : engine(*this) + { + display = { terminal }; + events = { + [&](const Connectivity &rawdata) { engine.read(rawdata); }, + [&](const Ticker &rawdata) { engine.read(rawdata); }, + [&](const Wallet &rawdata) { engine.read(rawdata); }, + [&](const Order &rawdata) { engine.read(rawdata); }, + [&](const unsigned int &tick ) { engine.timer_1s(tick); } + }; + }; + private: + static string terminal(); +} K; + +string Portfolios::terminal() { + const string quit = "┤ [" + ANSI_HIGH_WHITE + + "ESC" + ANSI_PUKE_WHITE + "]: Quit!" + + ", [" + ANSI_HIGH_WHITE + + "q" + ANSI_PUKE_WHITE + "]: Quit!"; + const string title = K.arg("exchange") + + ANSI_PUKE_GREEN + + ' ' + (K.arg("headless") + ? "headless" + : "UI at " + K.location() + ) + ' '; + const string top = "┌───────┐ K │ " + + ANSI_HIGH_GREEN + title + + ANSI_PUKE_WHITE + "├"; + string top_line; + for ( + unsigned int i = fmax(0, + K.display.width + - 1 + - top.length() + - quit.length() + + ANSI_SYMBOL_SIZE(12) + + ANSI_COLORS_SIZE(7) + ); + i --> 0; + top_line += "─" + ); + const string online = string(K.engine.broker.ready() ? "on" : "off") + + "line (" + ANSI_HIGH_YELLOW + + K.engine.portfolios.settings.currency + + ANSI_PUKE_WHITE + ") ├"; + string online_line; + for ( + unsigned int i = fmax(0, + title.length() + - online.length() + + ANSI_SYMBOL_SIZE(1) + + ANSI_COLORS_SIZE(1) + ); + i --> 0; + online_line += "─" + ); + unsigned int rows = 0; + string data; + for (const auto &it : K.engine.portfolios.portfolio) { + if (!it.second.wallet.total) continue; + data += ANSI_PUKE_WHITE + "├──" + + (it.second.wallet.total ? ANSI_PUKE_GREEN : ANSI_PUKE_YELLOW) + + ((json)it.second.wallet).dump() + + ANSI_END_LINE; + if (++rows == 7) break; + } + return ANSI_PUKE_WHITE + + top + top_line + quit + + ANSI_END_LINE + + "│ " + K.spin() + " └───┤ " + online + online_line + "┘" + + ANSI_END_LINE + + K.logs(rows + 3, "│ ") + + data; +}; diff --git a/src/bin/+portfolios/README.md b/src/bin/+portfolios/README.md new file mode 100644 index 000000000..80e7b8f7d --- /dev/null +++ b/src/bin/+portfolios/README.md @@ -0,0 +1 @@ +

diff --git a/src/bin/hello-world/README.md b/src/bin/hello-world/README.md new file mode 100644 index 000000000..0e6dae4ab --- /dev/null +++ b/src/bin/hello-world/README.md @@ -0,0 +1,25 @@ +```sh +# command-line examples: + + $ K-hello-world # print result and logs + $ K-hello-world 2> /dev/null # print result + $ K-hello-world > /dev/null # print logs + $ K-hello-world 2> my_logs # print result and write logs to file + $ K-hello-world > my_result # print logs and write result to file + $ K-hello-world > my_result 2>&1 # write logs and result to same file + $ K-hello-world > my_result 2> my_logs # write logs and result to different files + $ K-hello-world | cowsay # print verbose speaking cow notification + $ K-hello-world 2> /dev/null | cowsay # print speaking cow notification: + ______________________________ +/ Hello, WORLD! \ +| | +\ pssst.. 1 BTC = 9999.99 EUR. / + ------------------------------ + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || + +# enjoy! +``` diff --git a/src/bin/hello-world/hello-world.data.h b/src/bin/hello-world/hello-world.data.h new file mode 100644 index 000000000..74b7f6e3c --- /dev/null +++ b/src/bin/hello-world/hello-world.data.h @@ -0,0 +1,38 @@ +//! \file +//! \brief Welcome user! (just a working example to begin with). + +namespace example { + class Engine { + private: + Levels levels; + private_ref: + const KryptoNinja &K; + public: + Engine(const KryptoNinja &bot) + : K(bot) + {}; + void read(const Levels &rawdata) { + levels = rawdata; + if (levels.bids.empty() or levels.asks.empty()) return; + exit(greeting()); + }; + private: + string greeting() { + const Price fair = (levels.bids.cbegin()->price + + levels.asks.cbegin()->price) / 2; + cout << "Hello, " << K.arg("subject") + << ANSI_NEW_LINE + << " pssst.. " + << K.gateway->decimal.amount.str(1) << ' ' << K.gateway->base + << " = " + << K.gateway->decimal.price.str(fair) << ' ' << K.gateway->quote + << '.' + << ANSI_NEW_LINE; + return "Executed " + string( + K.arg("debug") + ? __PRETTY_FUNCTION__ + : K_SOURCE + ) + " OK"; + }; + }; +} diff --git a/src/bin/hello-world/hello-world.main.h b/src/bin/hello-world/hello-world.main.h new file mode 100644 index 000000000..170a79efb --- /dev/null +++ b/src/bin/hello-world/hello-world.main.h @@ -0,0 +1,23 @@ +class HelloWorld: public KryptoNinja { + private: + example::Engine engine; + public: + HelloWorld() + : engine(*this) + { + events = { + [&](const Levels &rawdata) { engine.read(rawdata); } + }; + arguments = { + { + {"subject", "NAME", "World", "say hello to NAME (default: 'World')"} + }, + [&](MutableUserArguments &args) { + if (arg("subject").empty()) + error("CF", "Invalid empty --subject value"); + else args["subject"] = Text::strU(arg("subject")) + "!"; + log("CF", "arguments validated", "OK"); + } + }; + }; +} K; diff --git a/src/bin/scaling-bot/README.md b/src/bin/scaling-bot/README.md new file mode 100644 index 000000000..62fe29697 --- /dev/null +++ b/src/bin/scaling-bot/README.md @@ -0,0 +1 @@ +

diff --git a/src/bin/scaling-bot/scaling-bot.ansi.h b/src/bin/scaling-bot/scaling-bot.ansi.h new file mode 100644 index 000000000..cb8baa14c --- /dev/null +++ b/src/bin/scaling-bot/scaling-bot.ansi.h @@ -0,0 +1,23 @@ +//! \file +//! \brief Color palette override for terminals. +//! \note ANSI color codes to override ANSI_* colors: +//! "0" BLACK "4" BLUE +//! "1" RED "5" MAGENTA +//! "2" GREEN "6" CYAN +//! "3" YELLOW "7" WHITE + +//! \def +//! \brief Define color red as magenta. +#define ANSI_RED "5" + +//! \def +//! \brief Define color magenta as red. +#define ANSI_MAGENTA "1" + +//! \def +//! \brief Define color green as yellow. +#define ANSI_GREEN "3" + +//! \def +//! \brief Define color yellow as green. +#define ANSI_YELLOW "2" diff --git a/src/bin/scaling-bot/scaling-bot.data.h b/src/bin/scaling-bot/scaling-bot.data.h new file mode 100644 index 000000000..01f5ef37a --- /dev/null +++ b/src/bin/scaling-bot/scaling-bot.data.h @@ -0,0 +1,633 @@ +//! \file +//! \brief Welcome user! (just a scaling bot to open/close positions while price decrements/increments). + +namespace analpaper { + struct Pongs { + Price maxBid = 0, + minAsk = 0; + private: + unordered_map bids, + asks; + private_ref: + const KryptoNinja &K; + public: + Pongs(const KryptoNinja &bot) + : K(bot) + {}; + void maxmin(const Order &order) { + if (!limit() or order.exchangeId.empty() or ( + !order.orderId.empty() and !order.isPong + )) return; + if (order.status == Status::Working) + (order.side == Side::Bid ? bids : asks)[order.exchangeId] = order.price; + else if (bids.contains(order.exchangeId)) bids.erase(order.exchangeId); + else if (asks.contains(order.exchangeId)) asks.erase(order.exchangeId); + maxBid = bids.empty() ? 0 : max_element(bids.begin(), bids.end(), compare)->second; + minAsk = asks.empty() ? 0 : min_element(asks.begin(), asks.end(), compare)->second; + }; + double limit() const { + return K.arg("wait-width"); + }; + bool limit(const System::Quote "e) const { + return find(quote.price, quote.side == Side::Bid ? &asks : &bids); + }; + private: + static bool compare(const pair &a, const pair &b) { + return a.second < b.second; + }; + bool find(const Price &price, const unordered_map *const book) const { + return any_of(book->begin(), book->end(), [&](auto &it) { + return abs(it.second - price) < limit(); + }); + }; + }; + + struct Orders: public System::Orderbook { + Pongs pongs; + private_ref: + const KryptoNinja &K; + public: + Orders(const KryptoNinja &bot) + : Orderbook(bot) + , pongs(bot) + , K(bot) + {}; + bool purgeable(const Order &order) const override { + return order.status == Status::Terminated + or order.isPong; + }; + void read_from_gw(const Order &order) { + pongs.maxmin(order); + if (order.orderId.empty()) return; + last->qtyFilled = !order.isPong + and order.status == Status::Terminated + and order.quantity == order.qtyFilled + ? order.quantity : 0; + if (last->qtyFilled + or (order.isPong and order.status == Status::Working) + ) K.log("GW " + K.gateway->exchange, + string(order.side == Side::Bid + ? ANSI_HIGH_CYAN + (order.isPong?"PONG":"PING") + " TRADE BUY " + : ANSI_PUKE_MAGENTA + (order.isPong?"PONG":"PING") + " TRADE SELL " + ) + + K.gateway->decimal.amount.str(order.quantity) + + " " + K.gateway->base + " at " + + K.gateway->decimal.price.str(order.price) + + " " + K.gateway->quote + + " " + (order.isPong ? "(left opened)" : "(just filled)")); + else K.repaint(true); + }; + Price calcPongPrice(const Price &fairValue) const { + const Price price = last->side == Side::Bid + ? fmax(last->price + K.arg("pong-width"), fairValue + K.gateway->tickPrice) + : fmin(last->price - K.arg("pong-width"), fairValue - K.gateway->tickPrice); + if (K.arg("pong-scale")) { + if (last->side == Side::Bid) { + if (pongs.minAsk) + return fmax(price, pongs.minAsk - K.arg("pong-width")); + } else if (pongs.maxBid) + return fmin(price, pongs.maxBid + K.arg("pong-width")); + } + return price; + }; + }; + + struct Deviation { + bool bid = false, + ask = false; + private: + vector fairValues; + private_ref: + const KryptoNinja &K; + public: + Deviation(const KryptoNinja &bot) + : K(bot) + {}; + void timer_1s(const Price &fv) { + fairValues.push_back(fv); + if (fairValues.size() > limit()) + fairValues.erase(fairValues.begin(), fairValues.end() - limit()); + calc(fv); + }; + vector::size_type limit() const { + return K.arg("time-price"); + }; + private: + void calc(const Price ¤t) { + if (K.arg("bids-size")) { + const Price high = *max_element(fairValues.begin(), fairValues.end()); + reset("DOWN", high, current, high - current > K.arg("wait-price"), &bid); + } + if (K.arg("asks-size")) { + const Price low = *min_element(fairValues.begin(), fairValues.end()); + reset(" UP ", low, current, current - low > K.arg("wait-price"), &ask); + } + }; + void reset(const string &side, const Price &from, const Price &to, const bool &next, bool *const state) { + if (*state != next) { + K.log("QE", "Fair value deviation " + side + (next ? " by" : " is"), + next + ? K.gateway->decimal.price.str(K.arg("wait-price")) + + " " + K.gateway->base + ANSI_PUKE_WHITE + " (from " + + K.gateway->decimal.price.str(from) + " to " + + K.gateway->decimal.price.str(to) + ")" + : "OFF"); + *state = next; + } + }; + }; + struct MarketLevels: public Levels { + Price fairValue = 0; + Deviation deviated; + private: + Levels unfiltered; + unordered_map filterBidOrders, + filterAskOrders; + private_ref: + const KryptoNinja &K; + const Orders &orders; + public: + MarketLevels(const KryptoNinja &bot, const Orders &o) + : deviated(bot) + , K(bot) + , orders(o) + {}; + void read_from_gw(const Levels &raw) { + unfiltered.bids = raw.bids; + unfiltered.asks = raw.asks; + filter(); + K.repaint(); + }; + bool ready() { + filter(); + if (!fairValue and Tspent > 21e+3) + K.warn("QE", "Unable to calculate quote, missing market data", 10e+3); + return fairValue; + }; + void timer_1s() { + if (deviated.limit() and ready()) + deviated.timer_1s(fairValue); + }; + private: + void filter() { + orders.resetFilters(&filterBidOrders, &filterAskOrders); + bids = filter(unfiltered.bids, &filterBidOrders); + asks = filter(unfiltered.asks, &filterAskOrders); + if (bids.empty() or asks.empty()) + fairValue = 0; + else + fairValue = (bids.cbegin()->price + + asks.cbegin()->price) / 2; + }; + vector filter(vector levels, unordered_map *const filterOrders) { + if (!filterOrders->empty()) + for (auto it = levels.begin(); it != levels.end();) { + for (auto it_ = filterOrders->begin(); it_ != filterOrders->end();) + if (abs(it->price - it_->first) < K.gateway->tickPrice) { + it->size -= it_->second; + filterOrders->erase(it_); + break; + } else ++it_; + if (it->size < K.gateway->minSize) it = levels.erase(it); + else ++it; + if (filterOrders->empty()) break; + } + return levels; + }; + }; + + struct Wallets { + Wallet base, + quote; + private_ref: + const KryptoNinja &K; + const Price &fairValue; + public: + Wallets(const KryptoNinja &bot, const MarketLevels &l) + : K(bot) + , fairValue(l.fairValue) + {}; + void read_from_gw(const Wallet &raw) { + if (raw.currency == K.gateway->base) base = raw; + else if (raw.currency == K.gateway->quote) quote = raw; + else return; + calcValues(); + }; + bool ready() const { + const bool err = base.currency.empty() or quote.currency.empty(); + if (err and Tspent > 21e+3) + K.warn("QE", "Unable to calculate quote, missing wallet data", 3e+3); + return !err; + }; + void calcValues() { + if (base.currency.empty() or quote.currency.empty() or !fairValue) return; + base.value = base.total + (quote.total / fairValue); + quote.value = quote.total + (base.total * fairValue); + }; + }; + + struct AntonioCalculon: public System::Quotes { + private_ref: + const KryptoNinja &K; + const Pongs &pongs; + const MarketLevels &levels; + const Wallets &wallet; + public: + AntonioCalculon(const KryptoNinja &bot, const Pongs &p, const MarketLevels &l, const Wallets &w) + : Quotes(bot) + , K(bot) + , pongs(p) + , levels(l) + , wallet(w) + {}; + private: + string explainState(const System::Quote "e) const override { + string reason = ""; + if (quote.state == QuoteState::Live) + reason = " LIVE " + ANSI_PUKE_WHITE + + "because of reasons (ping: " + + K.gateway->decimal.price.str(quote.price) + " " + K.gateway->quote + + ", fair value: " + + K.gateway->decimal.price.str(levels.fairValue) + " " + K.gateway->quote + +")"; + else if (quote.state == QuoteState::DepletedFunds) + reason = " PAUSED " + ANSI_PUKE_WHITE + + "because not enough available funds (" + + (quote.side == Side::Bid + ? K.gateway->decimal.price.str(wallet.quote.amount) + " " + K.gateway->quote + : K.gateway->decimal.amount.str(wallet.base.amount) + " " + K.gateway->base + ) + ")"; + else if (quote.state == QuoteState::ScaleSided) + reason = "DISABLED " + ANSI_PUKE_WHITE + + "because " + (quote.side == Side::Bid ? "--bids-size" : "--asks-size") + + " was not set"; + else if (quote.state == QuoteState::ScalationLimit) + reason = " PAUSED " + ANSI_PUKE_WHITE + + "because the nearest pong (" + + K.gateway->decimal.price.str(quote.side == Side::Bid + ? pongs.minAsk + : pongs.maxBid + ) + " " + K.gateway->quote + ") is closer than --wait-width"; + else if (quote.state == QuoteState::DeviationLimit) + reason = " PAUSED " + ANSI_PUKE_WHITE + + "because the price deviation is lower than --wait-price"; + else if (quote.state == QuoteState::WaitingFunds) + reason = " PAUSED " + ANSI_PUKE_WHITE + + "because a pong is pending to be placed"; + else if (quote.state == QuoteState::DisabledQuotes) + reason = "DISABLED " + ANSI_PUKE_WHITE + + "because an admin considered it"; + return reason; + }; + void calcRawQuotes() override { + bid.size = K.arg("bids-size"); + ask.size = K.arg("asks-size"); + bid.price = fmin( + levels.fairValue - K.gateway->tickPrice, + levels.fairValue - K.arg("ping-width") + ); + ask.price = fmax( + levels.fairValue + K.gateway->tickPrice, + levels.fairValue + K.arg("ping-width") + ); + }; + void applyQuotingParameters() override { + debug("?"); applyScaleSide(); + debug("A"); applyPongsScalation(); + debug("B"); applyFairValueDeviation(); + debug("C"); applyBestWidth(); + debug("D"); applyRoundPrice(); + debug("E"); applyRoundSize(); + debug("F"); applyDepleted(); + debug("!"); + }; + void applyScaleSide() { + if (!K.arg("bids-size")) + bid.skip(QuoteState::ScaleSided); + if (!K.arg("asks-size")) + ask.skip(QuoteState::ScaleSided); + }; + void applyPongsScalation() { + if (pongs.limit()) { + if (!bid.empty() and pongs.limit(bid)) + bid.skip(QuoteState::ScalationLimit); + if (!ask.empty() and pongs.limit(ask)) + ask.skip(QuoteState::ScalationLimit); + } + }; + void applyFairValueDeviation() { + if (levels.deviated.limit()) { + if (!bid.empty() and !levels.deviated.bid) + bid.skip(QuoteState::DeviationLimit); + if (!ask.empty() and !levels.deviated.ask) + ask.skip(QuoteState::DeviationLimit); + } + }; + void applyBestWidth() { + if (!ask.empty()) + for (const Level &it : levels.asks) + if (it.price > ask.price) { + const Price bestAsk = it.price - K.gateway->tickPrice; + if (bestAsk > ask.price) + ask.price = bestAsk; + break; + } + if (!bid.empty()) + for (const Level &it : levels.bids) + if (it.price < bid.price) { + const Price bestBid = it.price + K.gateway->tickPrice; + if (bestBid < bid.price) + bid.price = bestBid; + break; + } + }; + void applyRoundPrice() { + if (!bid.empty()) + bid.price = fmax( + 0, + K.gateway->decimal.price.round(bid.price) + ); + if (!ask.empty()) + ask.price = fmax( + bid.price + K.gateway->tickPrice, + K.gateway->decimal.price.round(ask.price) + ); + }; + void applyRoundSize() { + if (!bid.empty()) { + const Amount minBid = K.gateway->minValue + ? fmax(K.gateway->minSize, K.gateway->minValue / bid.price) + : K.gateway->minSize; + const Amount maxBid = wallet.quote.total / bid.price; + bid.size = K.gateway->decimal.amount.round( + fmax(minBid * (1.0 + K.gateway->takeFee * 1e+2), fmin( + bid.size, + K.gateway->decimal.amount.floor(maxBid) + )) + ); + } + if (!ask.empty()) { + const Amount minAsk = K.gateway->minValue + ? fmax(K.gateway->minSize, K.gateway->minValue / ask.price) + : K.gateway->minSize; + const Amount maxAsk = wallet.base.total; + ask.size = K.gateway->decimal.amount.round( + fmax(minAsk * (1.0 + K.gateway->takeFee * 1e+2), fmin( + ask.size, + K.gateway->decimal.amount.floor(maxAsk) + )) + ); + } + }; + void applyDepleted() { + if (!bid.empty() and wallet.quote.amount / bid.price < bid.size) + bid.skip(QuoteState::DepletedFunds); + if (!ask.empty() and wallet.base.amount < ask.size) + ask.skip(QuoteState::DepletedFunds); + }; + }; + + struct Semaphore: public Hotkey::Keymap { + Connectivity greenButton = Connectivity::Disconnected, + greenGateway = Connectivity::Disconnected; + private_ref: + const KryptoNinja &K; + public: + Semaphore(const KryptoNinja &bot) + : Keymap(bot, { + {'Q', [&]() { exit(); }}, + {'q', [&]() { exit(); }}, + {'\e', [&]() { toggle(); }} + }) + , K(bot) + {}; + bool paused() const { + return !(bool)greenButton; + }; + bool offline() const { + return !(bool)greenGateway; + }; + void read_from_gw(const Connectivity &raw) { + greenGateway = raw; + switchButton(); + }; + private: + void toggle() { + K.gateway->adminAgreement = (Connectivity)!(bool)K.gateway->adminAgreement; + switchButton(); + }; + void switchButton() { + greenButton = (Connectivity)( + (bool)greenGateway and (bool)K.gateway->adminAgreement + ); + K.repaint(); + }; + }; + + struct Broker { + Semaphore semaphore; + AntonioCalculon quotes; + private: + vector pending; + int limit = 0; + private_ref: + const KryptoNinja &K; + Orders &orders; + const MarketLevels &levels; + const Wallets &wallet; + public: + Broker(const KryptoNinja &bot, Orders &o, const MarketLevels &l, const Wallets &w) + : semaphore(bot) + , quotes(bot, o.pongs, l, w) + , K(bot) + , orders(o) + , levels(l) + , wallet(w) + {}; + bool ready() { + if (semaphore.offline()) { + quotes.offline(); + return false; + } + return true; + }; + void calcQuotes() { + if (pending.empty() and !semaphore.paused()) { + quotes.calcQuotes(); + quote2orders(quotes.ask); + quote2orders(quotes.bid); + } else { + if (semaphore.paused()) + quotes.paused(); + else quotes.pending(); + K.cancel(); + } + }; + void timer_60s() { + if (K.arg("heartbeat") and levels.fairValue) { + string bids, asks; + for (auto &it : orders.open()) + (it->side == Side::Bid + ? bids + : asks + ) += K.gateway->decimal.price.str(it->price) + ','; + if (!bids.empty()) bids.pop_back(); else bids = '0'; + if (!asks.empty()) asks.pop_back(); else asks = '0'; + K.log("HB", ((json){ + {"bid|fv|ask", K.gateway->decimal.price.str( + levels.bids.empty() + ? 0 : levels.bids.cbegin()->price + ) + "|" + + K.gateway->decimal.price.str( + levels.fairValue + ) + "|" + + K.gateway->decimal.price.str( + levels.asks.empty() + ? 0 : levels.asks.cbegin()->price + ) }, + {"pongs", K.gateway->decimal.price.str( + orders.pongs.maxBid + ) + "|" + + K.gateway->decimal.price.str( + orders.pongs.minAsk + ) }, + {"pings", bids + "|" + asks } + }).dump()); + } + }; + void timer_1s() { + if (!pending.empty() + and pending.at(0).quantity < (pending.at(0).side == Side::Bid + ? wallet.quote.amount / pending.at(0).price + : wallet.base.amount + )) { + K.place(pending.at(0)); + pending.erase(pending.begin()); + } + }; + void scale() { + if (orders.last + and orders.last->qtyFilled + and K.arg("pong-width") + ) pending.push_back({ + K.gateway->symbol, + orders.last->side == Side::Bid + ? Side::Ask + : Side::Bid, + orders.calcPongPrice(levels.fairValue), + orders.last->qtyFilled, + Tstamp, + true, + K.gateway->randId() + }); + }; + void quit_after() { + if (orders.last + and orders.last->isPong + and K.arg("quit-after") + and K.arg("quit-after") == ++limit + ) exit("CF " + ANSI_PUKE_WHITE + + "--quit-after=" + + ANSI_HIGH_YELLOW + to_string(K.arg("quit-after")) + + ANSI_PUKE_WHITE + " limit reached" + ); + }; + void quit() { + unsigned int n = 0; + for (Order *const it : orders.open()) { + K.gateway->cancel(it); + n++; + } + if (n) + K.log("QE", "Canceled " + to_string(n) + " open order" + string(n != 1, 's') + " before quit"); + }; + private: + bool abandon(const Order &order, const Price ¤tPrice, System::Quote "e) { + if (orders.zombies.stillAlive(order)) { + if (order.status == Status::Waiting + or abs(order.price - currentPrice) < K.gateway->tickPrice + or (K.arg("lifetime") and order.time + K.arg("lifetime") > Tstamp) + ) quote.skip(); + else return true; + } + return false; + }; + vector abandon(System::Quote "e) { + vector abandoned; + const Price currentPrice = quote.price; + for (Order *const it : orders.at(quote.side)) + if (!currentPrice or abandon(*it, currentPrice, quote)) + abandoned.push_back(it); + return abandoned; + }; + void quote2orders(System::Quote "e) { + const vector abandoned = abandon(quote); + const unsigned int replace = K.gateway->askForReplace and !( + quote.empty() or abandoned.empty() + ); + for ( + auto it = abandoned.end() - replace; + it --> abandoned.begin(); + K.cancel(*it) + ); + if (quote.empty()) return; + if (replace) K.replace(quote.price, quote.isPong, abandoned.back()); + else K.place({ + K.gateway->symbol, + quote.side, + quote.price, + quote.size, + Tstamp, + quote.isPong, + K.gateway->randId() + }); + }; + }; + + class Engine { + public: + Orders orders; + MarketLevels levels; + Wallets wallet; + Broker broker; + public: + Engine(const KryptoNinja &bot) + : orders(bot) + , levels(bot, orders) + , wallet(bot, levels) + , broker(bot, orders, levels, wallet) + {}; + void read(const Connectivity &rawdata) { + broker.semaphore.read_from_gw(rawdata); + }; + void read(const Wallet &rawdata) { + wallet.read_from_gw(rawdata); + }; + void read(const Levels &rawdata) { + levels.read_from_gw(rawdata); + wallet.calcValues(); + calcQuotes(); + }; + void read(const Order &rawdata) { + orders.read_from_gw(rawdata); + broker.scale(); + broker.quit_after(); + }; + void timer_1s(const unsigned int &tick) { + levels.timer_1s(); + broker.timer_1s(); + if (!(tick % 60)) + broker.timer_60s(); + calcQuotes(); + }; + void quit() { + broker.quit(); + }; + private: + void calcQuotes() { + if (broker.ready() and levels.ready() and wallet.ready()) + broker.calcQuotes(); + orders.zombies.purge(); + }; + }; +} diff --git a/src/bin/scaling-bot/scaling-bot.main.h b/src/bin/scaling-bot/scaling-bot.main.h new file mode 100644 index 000000000..29952520a --- /dev/null +++ b/src/bin/scaling-bot/scaling-bot.main.h @@ -0,0 +1,218 @@ +class ScalingBot: public KryptoNinja { + public: + analpaper::Engine engine; + public: + ScalingBot() + : engine(*this) + { + display = { terminal }; + events = { + [&](const Connectivity &rawdata) { engine.read(rawdata); }, + [&](const Wallet &rawdata) { engine.read(rawdata); }, + [&](const Levels &rawdata) { engine.read(rawdata); }, + [&](const Order &rawdata) { engine.read(rawdata); }, + [&](const unsigned int &tick ) { engine.timer_1s(tick); }, + [&]( ) { engine.quit(); } + }; + arguments = { + { + {"bids-size", "AMOUNT", "0", "set AMOUNT of size in base currency to place ping bid orders," + ANSI_NEW_LINE "or leave it unset to not place ping bid orders"}, + {"asks-size", "AMOUNT", "0", "set AMOUNT of size in base currency to place ping ask orders," + ANSI_NEW_LINE "or leave it unset to not place ping ask orders"}, + {"ping-width", "AMOUNT", "0", "set AMOUNT of price width to place pings away from fair value"}, + {"pong-width", "AMOUNT", "0", "set AMOUNT of price width to place pongs away from pings," + ANSI_NEW_LINE "or leave it unset to not place pong orders"}, + {"pong-scale", "1", nullptr, "place new pongs away from last pongs instead of from pings"}, + {"quit-after", "NUMBER", "0", "set NUMBER of filled pings before quit"}, + {"wait-width", "AMOUNT", "0", "set AMOUNT of price width from last pong before start"}, + {"wait-price", "AMOUNT", "0", "set AMOUNT of fair value price deviation before start"}, + {"time-price", "NUMBER", "0", "set NUMBER of seconds to measure price deviation difference"} + }, + [&](MutableUserArguments &args) { + args["bids-size"] = fmax(0, arg("bids-size")); + args["asks-size"] = fmax(0, arg("asks-size")); + args["ping-width"] = fmax(0, arg("ping-width")); + args["pong-width"] = fmax(0, arg("pong-width")); + args["pong-scale"] = max(0, arg("pong-scale")); + args["quit-after"] = max(0, arg("quit-after")); + args["wait-width"] = fmax(0, arg("wait-width")); + args["wait-price"] = fmax(0, arg("wait-price")); + args["time-price"] = max(0, arg("time-price")); + if (arg("pong-scale") and !arg("pong-width")) + error("CF", "Invalid use of --pong-scale without --pong-width"); + const string enabled = ANSI_HIGH_YELLOW + + (arg("quit-after") + ? to_string(arg("quit-after")) + : "Unlimited" + ) + ANSI_PUKE_WHITE + " pings enabled on"; + if (!arg("bids-size") and !arg("asks-size")) + error("CF", "Invalid empty --bids-size or --asks-size value"); + else if (arg("bids-size") + and arg("asks-size")) log("CF", enabled, "both sides"); + else if ( !arg("bids-size")) log("CF", enabled, "ASK side only"); + else if ( !arg("asks-size")) log("CF", enabled, "BID side only"); + Decimal opt; + opt.precision(1e-8); + if (arg("wait-price") and arg("wait-width")) + error("CF", "Invalid use of --wait-price and --wait-width together"); + else if (!arg("wait-width")) + log("CF", "Pongs scalation limit", "disabled"); + else + log("CF", "Pongs scalation limit enabled (" + ANSI_HIGH_YELLOW + + opt.str(arg("wait-width")) + " " + arg("quote") + ")" + ); + if (!arg("wait-price") and arg("time-price")) + error("CF", "Invalid use of --time-price without --wait-price"); + else if (arg("wait-price") and !arg("time-price")) + error("CF", "Invalid use of --wait-price without --time-price"); + else if (!arg("wait-price")) + log("CF", "Price deviation limit", "disabled"); + else + log("CF", "Price deviation limit enabled (" + ANSI_HIGH_YELLOW + + opt.str(arg("wait-price")) + " " + arg("quote") + + ANSI_PUKE_WHITE + " in " + ANSI_HIGH_YELLOW + + (arg("time-price") + ? to_string(arg("time-price")) + : "unlimited" + ) + " seconds" + ANSI_PUKE_WHITE + ")" + ); + if (arg("bids-size")) + log("CF", "--bids-size=", opt.str(arg("bids-size"))); + if (arg("asks-size")) + log("CF", "--asks-size=", opt.str(arg("asks-size"))); + if (!arg("ping-width")) + error("CF", "Invalid empty --ping-width value"); + else log("CF", "--ping-width=", opt.str(arg("ping-width"))); + log("CF", "--pong-width=", opt.str(arg("pong-width")) + + (arg("pong-width") ? "" : ANSI_HIGH_RED + " (pong orders disabled)") + ); + if (arg("pong-width")) { + if (!arg("wait-width")) + warn("CF", "Pong orders may overlap because --wait-width is not set"); + else if (arg("wait-width") < arg("pong-width") * 2) + warn("CF", "Pong orders may overlap because " + "--wait-width is smaller than the double of --pong-width"); + } + } + }; + }; + private: + static string terminal(); +} K; + +string ScalingBot::terminal() { + const string baseValue = K.gateway->decimal.funds.str(K.engine.wallet.base.value), + quoteValue = K.gateway->decimal.price.str(K.engine.wallet.quote.value); + const string coins = ANSI_HIGH_MAGENTA + baseValue + + ANSI_PUKE_MAGENTA + ' ' + K.gateway->base + + ANSI_PUKE_GREEN + " or " + + ANSI_HIGH_CYAN + quoteValue + + ANSI_PUKE_CYAN + ' ' + K.gateway->quote + + ANSI_PUKE_WHITE + " ├"; + const string quit = "┤ [" + ANSI_HIGH_WHITE + + "ESC" + ANSI_PUKE_WHITE + "]: " + + (K.engine.broker.semaphore.paused() + ? "Start" + : "Stop?" + ) + "!" + + ", [" + ANSI_HIGH_WHITE + + "q" + ANSI_PUKE_WHITE + "]: Quit!"; + const string title = K.arg("exchange") + + ANSI_PUKE_GREEN + + ' ' + (K.arg("headless") + ? "headless" + : "UI at " + K.location() + ) + ' '; + const string space = string(fmax(0, + coins.length() + - title.length() + - ANSI_SYMBOL_SIZE(1) + - ANSI_COLORS_SIZE(5) + ), ' '); + const string top = "┌───────┐ K │ " + + ANSI_HIGH_GREEN + title + space + + ANSI_PUKE_WHITE + "├"; + string top_line; + for ( + unsigned int i = fmax(0, + K.display.width + - 1 + - top.length() + - quit.length() + + ANSI_SYMBOL_SIZE(12) + + ANSI_COLORS_SIZE(7) + ); + i --> 0; + top_line += "─" + ); + string coins_line; + for ( + unsigned int i = fmax(0, + title.length() + + space.length() + - coins.length() + + ANSI_SYMBOL_SIZE(1) + + ANSI_COLORS_SIZE(5) + ); + i --> 0; + coins_line += "─" + ); + const vector openOrders = K.engine.orders.working(true); + unsigned int orders = openOrders.size(); + unsigned int rows = 0; + string data = ANSI_PUKE_WHITE + + (openOrders.empty() and K.engine.broker.semaphore.paused() + ? "└" + : "├" + ) + "───┤< ("; + if (K.engine.broker.semaphore.offline()) { + data += ANSI_HIGH_RED + "DISCONNECTED" + + ANSI_PUKE_WHITE + ")" + + ANSI_END_LINE; + } else { + if (K.engine.broker.semaphore.paused()) + data += ANSI_WAVE_YELLOW + "press START to trade" + + ANSI_PUKE_WHITE + ")"; + else + data += ANSI_PUKE_YELLOW + to_string(orders) + + ANSI_PUKE_WHITE + ") Open Orders"; + data += " while " + + ANSI_PUKE_GREEN + + "1 " + K.gateway->base + + " = " + + ANSI_HIGH_GREEN + + K.gateway->decimal.price.str(K.engine.levels.fairValue) + + ANSI_PUKE_GREEN + + " " + K.gateway->quote + + (K.engine.broker.semaphore.paused() ? ' ' : ':') + + ANSI_END_LINE; + for (const auto &it : openOrders) { + data += ANSI_PUKE_WHITE + "├" + + (it.side == Side::Bid ? ANSI_HIGH_CYAN + "BID" : ANSI_PUKE_MAGENTA + "ASK") + + " > " + + K.gateway->decimal.amount.str(it.quantity) + + ' ' + K.gateway->base + " at price " + + K.gateway->decimal.price.str(it.price) + + ' ' + K.gateway->quote + " (value " + + K.gateway->decimal.price.str(abs(it.price * it.quantity)) + + ' ' + K.gateway->quote + ")" + + ANSI_END_LINE; + ++rows; + } + if (!K.engine.broker.semaphore.paused()) + while (orders < 2) { + data += ANSI_PUKE_WHITE + "├" + + ANSI_END_LINE; + ++orders; + ++rows; + } + } + return ANSI_PUKE_WHITE + + top + top_line + quit + + ANSI_END_LINE + + "│ " + K.spin() + " └───┤ " + coins + coins_line + "┘" + + ANSI_END_LINE + + K.logs(rows + 4, "│ ") + + data; +}; diff --git a/src/bin/stable--bot/README.md b/src/bin/stable--bot/README.md new file mode 100644 index 000000000..62fe29697 --- /dev/null +++ b/src/bin/stable--bot/README.md @@ -0,0 +1 @@ +

diff --git a/src/bin/stable--bot/stable--bot.data.h b/src/bin/stable--bot/stable--bot.data.h new file mode 100644 index 000000000..2ea763c1e --- /dev/null +++ b/src/bin/stable--bot/stable--bot.data.h @@ -0,0 +1,368 @@ +//! \file +//! \brief Welcome user! (just a stable bot for flat markets with constant buy/sell prices). + +namespace analpaper { + struct Wallets { + Wallet base, + quote; + private_ref: + const KryptoNinja &K; + public: + Wallets(const KryptoNinja &bot) + : K(bot) + {}; + void read_from_gw(const Wallet &raw) { + if (raw.currency == K.gateway->base) base = raw; + else if (raw.currency == K.gateway->quote) quote = raw; + }; + bool ready() const { + const bool err = base.currency.empty() or quote.currency.empty(); + if (err and Tspent > 21e+3) + K.warn("QE", "Unable to calculate quote, missing wallet data", 3e+3); + return !err; + }; + }; + + struct Orders: public System::Orderbook { + private_ref: + const KryptoNinja &K; + public: + Orders(const KryptoNinja &bot) + : Orderbook(bot) + , K(bot) + {}; + void read_from_gw(const Order &order) { + if (!order.orderId.empty() and order.qtyFilled == order.quantity and order.status == Status::Terminated) + K.log("GW " + K.gateway->exchange, + string(order.side == Side::Bid + ? ANSI_HIGH_CYAN + "TRADE BUY " + : ANSI_PUKE_MAGENTA + "TRADE SELL " + ) + + K.gateway->decimal.amount.str(order.quantity) + + " " + K.gateway->base + " at " + + K.gateway->decimal.price.str(order.price) + + " " + K.gateway->quote); + }; + }; + + struct MarketLevels: public Levels { + Price fairValue = 0; + private: + Levels unfiltered; + unordered_map filterBidOrders, + filterAskOrders; + private_ref: + const KryptoNinja &K; + const Orders &orders; + public: + MarketLevels(const KryptoNinja &bot, const Orders &o) + : K(bot) + , orders(o) + {}; + void read_from_gw(const Levels &raw) { + unfiltered.bids = raw.bids; + unfiltered.asks = raw.asks; + filter(); + }; + bool ready() { + filter(); + if (!fairValue and Tspent > 21e+3) + K.warn("QE", "Unable to calculate quote, missing market data", 10e+3); + return fairValue; + }; + private: + void filter() { + orders.resetFilters(&filterBidOrders, &filterAskOrders); + bids = filter(unfiltered.bids, &filterBidOrders); + asks = filter(unfiltered.asks, &filterAskOrders); + if (bids.empty() or asks.empty()) + fairValue = 0; + else + fairValue = (bids.cbegin()->price + + asks.cbegin()->price) / 2; + }; + vector filter(vector levels, unordered_map *const filterOrders) { + if (!filterOrders->empty()) + for (auto it = levels.begin(); it != levels.end();) { + for (auto it_ = filterOrders->begin(); it_ != filterOrders->end();) + if (abs(it->price - it_->first) < K.gateway->tickPrice) { + it->size -= it_->second; + filterOrders->erase(it_); + break; + } else ++it_; + if (it->size < K.gateway->minSize) it = levels.erase(it); + else ++it; + if (filterOrders->empty()) break; + } + return levels; + }; + }; + + struct AntonioCalculon: public System::Quotes { + private_ref: + const KryptoNinja &K; + const MarketLevels &levels; + const Wallets &wallet; + public: + AntonioCalculon(const KryptoNinja &bot, const MarketLevels &l, const Wallets &w) + : Quotes(bot) + , K(bot) + , levels(l) + , wallet(w) + {}; + private: + string explainState(const System::Quote "e) const override { + string reason = ""; + if (quote.state == QuoteState::Live) + reason = " LIVE " + ANSI_PUKE_WHITE + + "because of reasons (ping: " + + K.gateway->decimal.price.str(quote.price) + " " + K.gateway->quote + + ", fair value: " + + K.gateway->decimal.price.str(levels.fairValue) + " " + K.gateway->quote + +")"; + else if (quote.state == QuoteState::DepletedFunds) + reason = " PAUSED " + ANSI_PUKE_WHITE + + "because not enough available funds (" + + (quote.side == Side::Bid + ? K.gateway->decimal.price.str(wallet.quote.amount) + " " + K.gateway->quote + : K.gateway->decimal.amount.str(wallet.base.amount) + " " + K.gateway->base + ) + ")"; + else if (quote.state == QuoteState::DisabledQuotes) + reason = "DISABLED " + ANSI_PUKE_WHITE + + "because " + (quote.side == Side::Bid ? "--bid-price" : "--ask-price") + + " was not set"; + return reason; + }; + void calcRawQuotes() override { + bid.size = + ask.size = K.arg("order-size"); + bid.price = fmin( + levels.fairValue - K.gateway->tickPrice, + K.arg("bid-price") + ); + ask.price = fmax( + levels.fairValue + K.gateway->tickPrice, + K.arg("ask-price") + ); + }; + void applyQuotingParameters() override { + debug("?"); applyStableSide(); + debug("A"); applyBestWidth(); + debug("B"); applyRoundPrice(); + debug("C"); applyRoundSize(); + debug("D"); applyDepleted(); + debug("!"); + }; + void applyStableSide() { + if (!K.arg("bid-price")) + bid.skip(QuoteState::DisabledQuotes); + if (!K.arg("ask-price")) + ask.skip(QuoteState::DisabledQuotes); + }; + void applyBestWidth() { + if (!ask.empty()) + for (const Level &it : levels.asks) + if (it.price > ask.price) { + const Price bestAsk = it.price - K.gateway->tickPrice; + if (bestAsk > ask.price) + ask.price = bestAsk; + break; + } + if (!bid.empty()) + for (const Level &it : levels.bids) + if (it.price < bid.price) { + const Price bestBid = it.price + K.gateway->tickPrice; + if (bestBid < bid.price) + bid.price = bestBid; + break; + } + }; + void applyRoundPrice() { + if (!bid.empty()) + bid.price = fmax( + 0, + K.gateway->decimal.price.round(bid.price) + ); + if (!ask.empty()) + ask.price = fmax( + bid.price + K.gateway->tickPrice, + K.gateway->decimal.price.round(ask.price) + ); + }; + void applyRoundSize() { + if (!bid.empty()) { + const Amount minBid = K.gateway->minValue + ? fmax(K.gateway->minSize, K.gateway->minValue / bid.price) + : K.gateway->minSize; + const Amount maxBid = wallet.quote.total / bid.price; + bid.size = K.gateway->decimal.amount.round( + fmax(minBid * (1.0 + K.gateway->takeFee * 1e+2), fmin( + bid.size, + K.gateway->decimal.amount.floor(maxBid) + )) + ); + } + if (!ask.empty()) { + const Amount minAsk = K.gateway->minValue + ? fmax(K.gateway->minSize, K.gateway->minValue / ask.price) + : K.gateway->minSize; + const Amount maxAsk = wallet.base.total; + ask.size = K.gateway->decimal.amount.round( + fmax(minAsk * (1.0 + K.gateway->takeFee * 1e+2), fmin( + ask.size, + K.gateway->decimal.amount.floor(maxAsk) + )) + ); + } + }; + void applyDepleted() { + if (!bid.empty()) { + const Amount minBid = K.gateway->minValue + ? fmax(K.gateway->minSize, K.gateway->minValue / bid.price) + : K.gateway->minSize; + if (wallet.quote.total / bid.price < minBid) + bid.skip(QuoteState::DepletedFunds); + } + if (!ask.empty()) { + const Amount minAsk = K.gateway->minValue + ? fmax(K.gateway->minSize, K.gateway->minValue / ask.price) + : K.gateway->minSize; + if (wallet.base.total < minAsk) + ask.skip(QuoteState::DepletedFunds); + } + }; + }; + + struct Broker { + Connectivity greenGateway = Connectivity::Disconnected; + AntonioCalculon quotes; + private_ref: + const KryptoNinja &K; + Orders &orders; + const MarketLevels &levels; + public: + Broker(const KryptoNinja &bot, Orders &o, const MarketLevels &l, const Wallets &w) + : quotes(bot, l, w) + , K(bot) + , orders(o) + , levels(l) + {}; + void read_from_gw(const Connectivity &raw) { + greenGateway = raw; + if (!ready()) + quotes.offline(); + }; + bool ready() const { + return (bool)greenGateway; + }; + void calcQuotes() { + quotes.calcQuotes(); + quote2orders(quotes.ask); + quote2orders(quotes.bid); + }; + void timer_60s() { + if (K.arg("heartbeat") and levels.fairValue) + K.log("HB", ((json){ + {"bid|fv|ask", K.gateway->decimal.price.str(levels.fairValue) + + "|" + + K.gateway->decimal.price.str( + levels.bids.empty() ? 0 : levels.bids.begin()->price) + + "|" + + K.gateway->decimal.price.str( + levels.asks.empty() ? 0 : levels.asks.begin()->price)} + }).dump()); + }; + void quit() { + unsigned int n = 0; + for (Order *const it : orders.open()) { + K.gateway->cancel(it); + n++; + } + if (n) + K.log("QE", "Canceled " + to_string(n) + " open order" + string(n != 1, 's') + " before quit"); + }; + private: + bool abandon(const Order &order, const Price ¤tPrice, System::Quote "e) { + if (orders.zombies.stillAlive(order)) { + if (order.status == Status::Waiting + or abs(order.price - currentPrice) < K.gateway->tickPrice + or (K.arg("lifetime") and order.time + K.arg("lifetime") > Tstamp) + ) quote.skip(); + else return true; + } + return false; + }; + vector abandon(System::Quote "e) { + vector abandoned; + const Price currentPrice = quote.price; + for (Order *const it : orders.at(quote.side)) + if (!currentPrice or abandon(*it, currentPrice, quote)) + abandoned.push_back(it); + return abandoned; + }; + void quote2orders(System::Quote "e) { + const vector abandoned = abandon(quote); + const unsigned int replace = K.gateway->askForReplace and !( + quote.empty() or abandoned.empty() + ); + for ( + auto it = abandoned.end() - replace; + it --> abandoned.begin(); + K.cancel(*it) + ); + if (quote.empty()) return; + if (replace) K.replace(quote.price, quote.isPong, abandoned.back()); + else K.place({ + K.gateway->symbol, + quote.side, + quote.price, + quote.size, + Tstamp, + quote.isPong, + K.gateway->randId() + }); + }; + }; + + class Engine { + private: + Orders orders; + MarketLevels levels; + Wallets wallet; + Broker broker; + public: + Engine(const KryptoNinja &bot) + : orders(bot) + , levels(bot, orders) + , wallet(bot) + , broker(bot, orders, levels, wallet) + {}; + void read(const Connectivity &rawdata) { + broker.read_from_gw(rawdata); + }; + void read(const Wallet &rawdata) { + wallet.read_from_gw(rawdata); + }; + void read(const Levels &rawdata) { + levels.read_from_gw(rawdata); + calcQuotes(); + }; + void read(const Order &rawdata) { + orders.read_from_gw(rawdata); + }; + void timer_1s(const unsigned int &tick) { + if (!(tick % 60)) + broker.timer_60s(); + calcQuotes(); + }; + void quit() { + broker.quit(); + }; + private: + void calcQuotes() { + if (broker.ready() and levels.ready() and wallet.ready()) + broker.calcQuotes(); + orders.zombies.purge(); + }; + }; +} diff --git a/src/bin/stable--bot/stable--bot.main.h b/src/bin/stable--bot/stable--bot.main.h new file mode 100644 index 000000000..5f3928559 --- /dev/null +++ b/src/bin/stable--bot/stable--bot.main.h @@ -0,0 +1,44 @@ +class StableBot: public KryptoNinja { + private: + analpaper::Engine engine; + public: + StableBot() + : engine(*this) + { + events = { + [&](const Connectivity &rawdata) { engine.read(rawdata); }, + [&](const Wallet &rawdata) { engine.read(rawdata); }, + [&](const Levels &rawdata) { engine.read(rawdata); }, + [&](const Order &rawdata) { engine.read(rawdata); }, + [&](const unsigned int &tick ) { engine.timer_1s(tick); }, + [&]( ) { engine.quit(); } + }; + arguments = { + { + {"order-size", "AMOUNT", "0", "set AMOUNT of size in base currency to place orders"}, + {"ask-price", "AMOUNT", "0", "set AMOUNT of price to place ask orders"}, + {"bid-price", "AMOUNT", "0", "set AMOUNT of price to place bid orders"} + }, + [&](MutableUserArguments &args) { + args["order-size"] = fmax(0, arg("order-size")); + args["ask-price"] = fmax(0, arg("ask-price")); + args["bid-price"] = fmax(0, arg("bid-price")); + if (!arg("ask-price") and !arg("bid-price")) + error("CF", "Invalid empty --ask-price or --bid-price value"); + else if (arg("ask-price") + and arg("bid-price")) log("CF", "Orders enabled on", "both sides"); + else if (arg("ask-price")) log("CF", "Orders enabled on", "ASK side only"); + else if (arg("bid-price")) log("CF", "Orders enabled on", "BID side only"); + Decimal opt; + opt.precision(1e-8); + if (!arg("order-size")) + error("CF", "Invalid empty --order-size value"); + else log("CF", "--order-size=", opt.str(arg("order-size"))); + if (arg("ask-price")) + log("CF", "--ask-price=", opt.str(arg("ask-price"))); + if (arg("bid-price")) + log("CF", "--bid-price=", opt.str(arg("bid-price"))); + } + }; + }; +} K; diff --git a/src/bin/trading-bot/README.md b/src/bin/trading-bot/README.md new file mode 100644 index 000000000..7ac405c3f --- /dev/null +++ b/src/bin/trading-bot/README.md @@ -0,0 +1 @@ +

diff --git a/src/bin/trading-bot/trading-bot.client/Client.ts b/src/bin/trading-bot/trading-bot.client/Client.ts new file mode 100644 index 000000000..49cde0cbb --- /dev/null +++ b/src/bin/trading-bot/trading-bot.client/Client.ts @@ -0,0 +1,398 @@ +import {Component, OnInit, Input, Output, EventEmitter} from '@angular/core'; + +import {Socket, Shared, Models} from 'lib/K'; + +@Component({ + selector: 'client', + template: `
+
+
+ +
+
+
+
+
+
+ + +
+ Market + , + Orders +

+
+ +
+
+ +
+
+ +
+
+ +
+
+ Watch +
+
+ Takers, Stats +
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
` +}) +export class ClientComponent implements OnInit { + + private tradesLength: number = 0; + private tradesMatchedLength: number = 0; + private bidsLength: number = 0; + private asksLength: number = 0; + private notepad: string; + private showSettings: boolean = true; + private showTakers: boolean = false; + private showStats: number = 0; + private showSubmitOrder: boolean = false; + private quotingParameters: Models.QuotingParameters = {}; + private marketWidth: number = 0; + private orders: Models.Order[] = []; + private market: Models.Market = null; + private trade: Models.Trade = null; + private taker: Models.MarketTrade[] = []; + private tradesChart: Models.TradeChart = new Models.TradeChart(); + private marketChart: Models.MarketChart = new Models.MarketChart(); + private status: Models.TwoSidedQuoteStatus = new Models.TwoSidedQuoteStatus(); + private fairValue: Models.FairValue = new Models.FairValue(); + private position: Models.PositionReport = new Models.PositionReport(); + private tradeSafety: Models.TradeSafety = new Models.TradeSafety(); + private targetBasePosition: Models.TargetBasePositionValue = new Models.TargetBasePositionValue(); + + private cancelAllOrders = () => new Socket.Fire(Models.Topics.CancelAllOrders).fire(); + + private cleanAllClosedOrders = () => new Socket.Fire(Models.Topics.CleanAllClosedTrades).fire(); + + private cleanAllOrders = () => new Socket.Fire(Models.Topics.CleanAllTrades).fire(); + + private changeNotepad = (content:string) => new Socket.Fire(Models.Topics.Notepad).fire([content]); + + @Input() addr: string; + + @Input() tradeFreq: number; + + @Input() state: Models.ExchangeState; + + @Input() product: Models.ProductAdvertisement; + + @Output() onFooter = new EventEmitter(); + + ngOnInit() { + new Socket.Subscriber(Models.Topics.QuotingParametersChange) + .registerSubscriber((o: Models.QuotingParameters) => { this.quotingParameters = o; }); + + new Socket.Subscriber(Models.Topics.OrderStatusReports) + .registerSubscriber((o: Models.Order[]) => { this.orders = o; }) + .registerDisconnectedHandler(() => { this.orders = []; }); + + new Socket.Subscriber(Models.Topics.Position) + .registerSubscriber((o: Models.PositionReport) => { this.position = o; }); + + new Socket.Subscriber(Models.Topics.FairValue) + .registerSubscriber((o: Models.FairValue) => { this.fairValue = o; }); + + new Socket.Subscriber(Models.Topics.TradeSafetyValue) + .registerSubscriber((o: Models.TradeSafety) => { this.tradeSafety = o; }); + + new Socket.Subscriber(Models.Topics.MarketData) + .registerSubscriber((o: Models.Market) => { this.market = o; }) + .registerDisconnectedHandler(() => { this.market = null; }); + + new Socket.Subscriber(Models.Topics.Trades) + .registerSubscriber((o: Models.Trade) => { this.trade = o; }) + .registerDisconnectedHandler(() => { this.trade = null; }); + + new Socket.Subscriber(Models.Topics.MarketTrade) + .registerSubscriber((o: Models.MarketTrade[]) => { this.taker = o; }) + .registerDisconnectedHandler(() => { this.taker = []; }); + + new Socket.Subscriber(Models.Topics.QuoteStatus) + .registerSubscriber((o: Models.TwoSidedQuoteStatus) => { this.status = o; }) + .registerDisconnectedHandler(() => { this.status = new Models.TwoSidedQuoteStatus(); }); + + new Socket.Subscriber(Models.Topics.TargetBasePosition) + .registerSubscriber((o: Models.TargetBasePositionValue) => { this.targetBasePosition = o; }); + + new Socket.Subscriber(Models.Topics.MarketChart) + .registerSubscriber((o: Models.MarketChart) => { this.marketChart = o; }); + + new Socket.Subscriber(Models.Topics.Notepad) + .registerSubscriber((notepad : string) => { this.notepad = notepad; }); + + window.addEventListener("message", e => { + if (!e.data.indexOf) return; + + if (e.data.indexOf('cryptoWatch=') === 0) { + if (window.parent !== window) window.parent.postMessage(e.data, '*'); + else { + var data: string[] = e.data.replace('cryptoWatch=', '').split(','); + this._toggleWatch(data[0], data[1]); + } + } + }, false); + }; + + private toggleSettings = () => { + this.showSettings = !this.showSettings; + setTimeout(() => {window.dispatchEvent(new Event('resize'))}, 0); + }; + + private toggleTakers = () => { + this.showTakers = !this.showTakers; + setTimeout(() => {window.dispatchEvent(new Event('resize'))}, 0); + }; + + private toggleStats = () => { + if (++this.showStats>=3) this.showStats = 0; + }; + + private toggleWatch = (watchExchange: string, watchPair: string) => { + if (window.parent !== window) { + window.parent.postMessage('cryptoWatch='+watchExchange+','+watchPair, '*'); + return; + } + var self = this; + var toggleWatch = function() { + self._toggleWatch(watchExchange, watchPair); + }; + if (!window.hasOwnProperty("cryptowatch")) + (function(d, script) { + script = d.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + script.onload = toggleWatch; + script.src = 'https://static.cryptowat.ch/assets/scripts/embed.bundle.js'; + d.getElementsByTagName('head')[0].appendChild(script); + }(document)); + else toggleWatch(); + }; + + private _toggleWatch = (watchExchange: string, watchPair: string) => { + if (!document.getElementById('cryptoWatch'+watchExchange+watchPair)) { + if(watchExchange=='coinbase') watchExchange = 'coinbase-pro'; + this.setDialog('cryptoWatch'+watchExchange+watchPair, 'open', {title: watchExchange.toUpperCase()+' '+watchPair.toUpperCase().replace('-','/'),width: 800,height: 400,content: `
`}); + (new (window).cryptowatch.Embed(watchExchange, watchPair.replace('-',''), {timePeriod: '1d',customColorScheme: {bg:"000000",text:"b2b2b2",textStrong:"e5e5e5",textWeak:"7f7f7f",short:"FD4600",shortFill:"FF672C",long:"6290FF",longFill:"002782",cta:"363D52",ctaHighlight:"414A67",alert:"FFD506"}})).mount('#container'+watchExchange+watchPair); + } else this.setDialog('cryptoWatch'+watchExchange+watchPair, 'close', {content:''}); + }; + + private onTradesChartData(o: Models.TradeChart) { + this.tradesChart = o; + }; + + private onTradesLength(o: number) { + this.tradesLength = o; + this.setFooter(); + }; + + private onTradesMatchedLength(o: number) { + this.tradesMatchedLength = o; + this.setFooter(); + }; + + private onBidsLength(o: number) { + this.bidsLength = o; + this.setFooter(); + }; + + private onAsksLength(o: number) { + this.asksLength = o; + this.setFooter(); + }; + + private setFooter() { + this.onFooter.emit(` + ` + this.tradesLength + `` + ( + this.tradesMatchedLength < 0 + ? `` + : `/` + this.tradesMatchedLength + `` + ) + ` - + ` + this.bidsLength + `|` + this.asksLength + ` - `); + }; + + private onMarketWidth(o: number) { + this.marketWidth = o; + }; + + private setDialog = (uniqueId: string, set: string, config: object) => { + if (set === "open") { + var div = document.createElement('div'); + div.className = 'dialog-box'; + div.id = uniqueId; + div.innerHTML = '
 

'; + document.body.appendChild(div); + } + + var dialog = document.getElementById(uniqueId), selected = null, defaults = { + title: '', + content: '', + width: 300, + height: 150, + top: false, + left: false + }; + + for (var i in config) { defaults[i] = (typeof config[i] !== 'undefined') ? config[i] : defaults[i]; } + + function _drag_init(e, el) { + for (var i=0;idocument.getElementsByClassName('dialog-box')[i]).style.zIndex = "9999"; + el.style.zIndex = "10000"; + var posX = e.clientX, + posY = e.clientY, + divTop = parseFloat(el.style.top.indexOf('%')>-1?el.offsetTop + el.offsetHeight/2:el.style.top), + divLeft = parseFloat(el.style.left.indexOf('%')>-1?el.offsetLeft + el.offsetWidth/2:el.style.left) + var diffX = posX - divLeft, + diffY = posY - divTop; + document.onmousemove = function(e){ + var posX = e.clientX, + posY = e.clientY, + aX = posX - diffX, + aY = posY - diffY; + el.style.left = aX + 'px'; + el.style.top = aY + 'px'; + } + } + + dialog.className = 'dialog-box fixed-dialog-box'; + dialog.style.visibility = (set === "open") ? "visible" : "hidden"; + dialog.style.opacity = (set === "open") ? "1" : "0"; + dialog.style.width = defaults.width + 'px'; + dialog.style.height = defaults.height + 'px'; + dialog.style.top = (!defaults.top) ? "50%" : '0px'; + dialog.style.left = (!defaults.left) ? "50%" : '0px'; + dialog.style.marginTop = (!defaults.top) ? '-' + defaults.height/2 + 'px' : defaults.top + 'px'; + dialog.style.marginLeft = (!defaults.left) ? '-' + defaults.width/2 + 'px' : defaults.left + 'px'; + dialog.children[1].innerHTML = defaults.title + dialog.children[1].innerHTML; + dialog.children[0].innerHTML = defaults.content; + (dialog.children[1]).onmousedown = function(e) { + if ((e.target || e.srcElement)===this) _drag_init(e, (this).parentNode); + }; + (dialog.children[1].children[0]).onclick = function() { + dialog.remove(); + }; + document.onmouseup = function() { document.onmousemove = function() {}; }; + if (set === "close") dialog.remove(); + }; +}; diff --git a/src/bin/trading-bot/trading-bot.client/Market.ts b/src/bin/trading-bot/trading-bot.client/Market.ts new file mode 100644 index 000000000..df20d2a6d --- /dev/null +++ b/src/bin/trading-bot/trading-bot.client/Market.ts @@ -0,0 +1,296 @@ +import {Component, Input, Output, EventEmitter} from '@angular/core'; + +import {Models} from 'lib/K'; + +@Component({ + selector: 'market', + template: `
+ +
+
+
+
Market Width:{{ marketWidth.toFixed(product.tickPrice) }}
+
Quote Width:{{ ordersWidth.toFixed(product.tickPrice) }}
+
Quotes:{{ status.quotesInMemoryWaiting }}/{{ status.quotesInMemoryWorking }}/{{ status.quotesInMemoryZombies }}
+
openOrders/60sec:{{ tradeFreq }}
+
Wallet TBP:{{ targetBasePosition.tbp.toFixed(8) }}
+
pDiv:{{ targetBasePosition.pDiv.toFixed(8) }}
+
APR:{{ getAPR() }}
+
+
+
+ + + + + + + + + + + + + + + +
BID SizeBID PriceASK PriceASK Size
{{ qBidSz.toFixed(product.tickSize) }} {{ qBidPx.toFixed(product.tickPrice) }}{{ getStatus(status.bidStatus) }}{{ qAskPx.toFixed(product.tickPrice) }}{{ qAskSz.toFixed(product.tickSize) }} {{ getStatus(status.askStatus) }}
+
+ + + + + +
+
+
+ {{ getSizeLevel(lvl.size.toFixed(product.tickSize), true) }}{{ getSizeLevel(lvl.size.toFixed(product.tickSize), false) }} +
+
+
+ {{ lvl.price.toFixed(product.tickPrice) }} +
+
+ + + + + +
+
+ {{ lvl.price.toFixed(product.tickPrice) }} +
+
+
+
+ {{ getSizeLevel(lvl.size.toFixed(product.tickSize), true) }}{{ getSizeLevel(lvl.size.toFixed(product.tickSize), false) }} +
+
+
+


To unlock realtime market data,
and to collaborate with the development..

make an acceptable Pull Request on github,

or send 0.00121000 BTC or more to:
{{ addr }}

Wait 0 confirmations and restart this bot.
+
` +}) +export class MarketComponent { + + private levels: Models.Market = null; + private allBidsSize: number = 0; + private allAsksSize: number = 0; + private dirtyBids: number = 0; + private dirtyAsks: number = 0; + private qBidSz: number = 0; + private qBidPx: number = 0; + private qAskPx: number = 0; + private qAskSz: number = 0; + private orderBids: Models.OrderSide[]; + private orderAsks: Models.OrderSide[]; + private orderPriceBids: string[] = []; + private orderPriceAsks: string[] = []; + private marketWidth: number = 0; + private ordersWidth: number = 0; + private noBidReason: string; + private noAskReason: string; + + @Input() product: Models.ProductAdvertisement; + + @Input() fairValue: Models.FairValue; + + @Input() tradeSafety: Models.TradeSafety; + + @Input() targetBasePosition: Models.TargetBasePositionValue; + + @Input() tradeFreq: number; + + @Input() status: Models.TwoSidedQuoteStatus; + + @Input() addr: string; + + @Input() set orders(o: Models.Order[]) { + this.addOrders(o); + }; + + @Input() set market(o: Models.Market) { + this.addMarket(o); + }; + + @Output() onBidsLength = new EventEmitter(); + + @Output() onAsksLength = new EventEmitter(); + + @Output() onMarketWidth = new EventEmitter(); + + private clearQuote = () => { + this.orderBids = []; + this.orderAsks = []; + this.orderPriceBids = []; + this.orderPriceAsks = []; + }; + + private getAPR = () => { + return Models.SideAPR[this.status.sideAPR]; + }; + + private getStatus = (o: Models.QuoteStatus) => { + return Models.QuoteStatus[o].replace(/([A-Z])/g, ' $1').trim(); + }; + + private getSizeLevel = (size: string, ret: boolean) => { + var decimals = (''+size).indexOf(".")+1; + if (!decimals) return ret?size:""; + var tokens: string[] = size.split(""); + var token: string = tokens.pop(); + var zeros: number = 0; + while(token == "0" || token == ".") { + zeros++; + if (token == ".") break; + token = tokens.pop(); + } + if (!zeros) return ret?size:""; + return ret + ? size.substr(0, size.length-zeros) + : size.substr(size.length-zeros); + }; + + private getBgSize = (lvl: Models.MarketSide, side: string) => { + var allSize: string = side=='bids'?'allBidsSize':'allAsksSize'; + var red: string = side=='bids'?'141':'255'; + var green: string = side=='bids'?'226':'142'; + var blue: string = side=='bids'?'255':'140'; + var dir: string = side=='bids'?'left':'right'; + return 'linear-gradient(to '+dir+', rgba('+red+', '+green+', '+blue+', 0.69) ' + + Math.ceil(lvl.size/this[allSize]*100) + + '%, rgba('+red+', '+green+', '+blue+', 0) 0%)'; + }; + + private incrementMarketData = (diff: Models.MarketSide[], side: string) => { + var allSize: string = side=='bids'?'allBidsSize':'allAsksSize'; + var dirtySize: string = side=='bids'?'dirtyBids':'dirtyAsks'; + for (var i: number = 0; i < diff.length; i++) { + if (typeof diff[i].size != 'number') diff[i].size = 0; + var found = false; + for (var j: number = 0; j < this.levels[side].length; j++) + if (diff[i].price === this.levels[side][j].price) { + found = true; + this[allSize] -= this.levels[side][j].size; + if (diff[i].size) { + this.levels[side][j].size = diff[i].size; + this.levels[side][j].cssMod = 1; + this[allSize] += this.levels[side][j].size; + } else { + this.levels[side][j].cssMod = 2; + this[dirtySize]++; + } + break; + } + if (!found && diff[i].size) { + for (var j: number = 0; j < this.levels[side].length; j++) + if (this.levels[side][j].cssMod != 2 && (side == 'bids' + ? diff[i].price > this.levels[side][j].price + : diff[i].price < this.levels[side][j].price) + ) { + found = true; + this[allSize] += diff[i].size; + this.levels[side].splice(j, 0, diff[i]); + this.levels[side][j].cssMod = 1; + break; + } + if (!found) { + this[allSize] += diff[i].size; + this.levels[side].push(diff[i]); + this.levels[side][this.levels[side].length - 1].cssMod = 1; + } + } + } + }; + + private addOrders = (o: Models.Order[]) => { + this.clearQuote(); + o.forEach(o => { + const orderSide = o.side === Models.Side.Bid ? 'orderBids' : 'orderAsks'; + const orderPrice = o.side === Models.Side.Bid ? 'orderPriceBids' : 'orderPriceAsks'; + if (o.status == Models.OrderStatus.Terminated) + this[orderSide] = this[orderSide].filter(x => x.orderId !== o.orderId); + else if (!this[orderSide].filter(x => x.orderId === o.orderId).length) + this[orderSide].push({ + orderId: o.orderId, + side: o.side, + price: o.price, + quantity: o.quantity, + }); + this[orderPrice] = this[orderSide].map((a)=>a.price.toFixed(this.product.tickPrice)); + + if (this.orderBids.length) { + var bid = this.orderBids.reduce((a,b)=>a.price>b.price?a:b); + this.qBidPx = bid.price; + this.qBidSz = bid.quantity; + } else { + this.qBidPx = 0; + this.qBidSz = 0; + } + if (this.orderAsks.length) { + var ask = this.orderAsks.reduce((a,b)=>a.price { + if (o == null || typeof o.diff != 'boolean') { + this.allBidsSize = 0; + this.allAsksSize = 0; + if (o != null) { + for (var i: number = 0; i < o.bids.length; i++) + this.allBidsSize += o.bids[i].size; + for (var i: number = 0; i < o.asks.length; i++) + this.allAsksSize += o.asks[i].size; + } + this.levels = o; + } else { + if (this.levels == null) return; + for (var i = this.levels.bids.length - 1; i >= 0; i--) + if (this.levels.bids[i].cssMod) + if (this.levels.bids[i].cssMod==2) + this.levels.bids.splice(i, 1); + else this.levels.bids[i].cssMod = 0; + for (var i = this.levels.asks.length - 1; i >= 0; i--) + if (this.levels.asks[i].cssMod) + if (this.levels.asks[i].cssMod==2) + this.levels.asks.splice(i, 1); + else this.levels.asks[i].cssMod = 0; + this.dirtyBids = 0; + this.dirtyAsks = 0; + this.incrementMarketData(o.bids, 'bids'); + this.incrementMarketData(o.asks, 'asks'); + if (this.levels == null) { + this.onBidsLength.emit(0); + this.onAsksLength.emit(0); + this.marketWidth = 0; + } else { + this.onBidsLength.emit(this.levels.bids.length - this.dirtyBids); + this.onAsksLength.emit(this.levels.asks.length - this.dirtyAsks); + var topBid: number = 0; + var topAsk: number = 0; + for (var i: number = 0; i < this.levels.bids.length; i++) + if (this.levels.bids[i].cssMod != 2) { + topBid = this.levels.bids[i].price; + break; + } + for (var i: number = 0; i < this.levels.asks.length; i++) + if (this.levels.asks[i].cssMod != 2) { + topAsk = this.levels.asks[i].price; + break; + } + this.marketWidth = (topBid && topAsk) ? topAsk - topBid : 0; + } + this.onMarketWidth.emit(this.marketWidth / 2); + } + }; +}; diff --git a/src/bin/trading-bot/trading-bot.client/Orders.ts b/src/bin/trading-bot/trading-bot.client/Orders.ts new file mode 100644 index 000000000..874134178 --- /dev/null +++ b/src/bin/trading-bot/trading-bot.client/Orders.ts @@ -0,0 +1,156 @@ +import {Component, Input} from '@angular/core'; + +import {GridOptions, GridApi} from 'ag-grid-community'; + +import {Socket, Shared, Models} from 'lib/K'; + +@Component({ + selector: 'orders', + template: `` +}) +export class OrdersComponent { + + private fireCxl: Socket.IFire = new Socket.Fire(Models.Topics.CancelOrder); + + @Input() product: Models.ProductAdvertisement; + + @Input() set orders(o: Models.Order[]) { + this.addRowData(o); + }; + + private api: GridApi; + + private grid: GridOptions = { + suppressNoRowsOverlay: true, + defaultColDef: { sortable: true, resizable: true, flex: 1 }, + rowHeight:28, + headerHeight:25, + columnDefs: [{ + width: 30, + field: "cancel", + headerName: 'cxl', + suppressSizeToFit: true, + cellRenderer: (params) => `` + }, { + width: 82, + field: 'time', + headerName: 'time', + suppressSizeToFit: true, + cellRenderer: (params) => { + var d = new Date(params.value||0); + return (d.getHours()+'') + .padStart(2, "0")+':'+(d.getMinutes()+'') + .padStart(2, "0")+':'+(d.getSeconds()+'') + .padStart(2, "0")+','+(d.getMilliseconds()+'') + .padStart(3, "0"); + } + }, { + width: 40, + field: 'side', + headerName: 'side', + suppressSizeToFit: true, + cellClassRules: { + 'sell': 'data.side == "Ask"', + 'buy': 'data.side == "Bid"' + }, + cellRenderer: (params) => ( + params.data.pong + ? '⥄' + : '➜' + ) + params.value + }, { + width: 74, + field: 'price', + headerName: 'price', + sort: 'desc', + cellRenderer: (params) => `` + params.value + `` + ` `, + cellClassRules: { + 'sell': 'data.side == "Ask"', + 'buy': 'data.side == "Bid"' + } + }, { + width: 95, + field: 'quantity', + headerName: 'qty', + suppressSizeToFit: true, + cellRenderer: (params) => `` + params.value + `` + ` `, + cellClassRules: { + 'sell': 'data.side == "Ask"', + 'buy': 'data.side == "Bid"' + } + }, { + width: 74, + field: 'value', + headerName: 'value', + cellRenderer: (params) => `` + params.value + `` + ` `, + cellClassRules: { + 'sell': 'data.side == "Ask"', + 'buy': 'data.side == "Bid"' + } + }, { + width: 55, + field: 'type', + headerName: 'type', + suppressSizeToFit: true + }, { + width: 40, + field: 'tif', + headerName: 'tif' + }, { + width: 45, + field: 'lat', + headerName: 'lat' + }, { + width: 110, + field: 'exchangeId', + headerName: 'openOrderId', + suppressSizeToFit: true, + cellRenderer: (params) => params.value + ? params.value.toString().split('-')[0] + : '' + }] + }; + + private onGridReady($event: any) { + if ($event.api) this.api = $event.api; + }; + + private onCellClicked = ($event) => { + if ($event.event.target.getAttribute('data-action-type') != 'remove') return; + this.fireCxl.fire(new Models.OrderCancelRequestFromUI($event.data.orderId, $event.data.exchange)); + }; + + private addRowData = (o: Models.Order[]) => { + if (!this.api) return; + + var add: any[] = []; + + o.forEach(o => { + add.push({ + orderId: o.orderId, + exchangeId: o.exchangeId, + side: Models.Side[o.side], + price: Shared.str(o.price, this.product.tickPrice), + value: Shared.str(Math.round(o.quantity * o.price * 100) / 100, this.product.tickPrice), + type: Models.OrderType[o.type], + tif: Models.TimeInForce[o.timeInForce], + lat: o.latency + 'ms', + quantity: Shared.str(o.quantity, this.product.tickSize), + pong: o.isPong, + time: o.time + }); + }); + + this.api.setGridOption('rowData', []); + + if (add.length) this.api.applyTransaction({add: add}); + }; +}; diff --git a/src/bin/trading-bot/trading-bot.client/Safety.ts b/src/bin/trading-bot/trading-bot.client/Safety.ts new file mode 100644 index 000000000..b35393cfc --- /dev/null +++ b/src/bin/trading-bot/trading-bot.client/Safety.ts @@ -0,0 +1,60 @@ +import {Component, Input} from '@angular/core'; + +import {Models} from 'lib/K'; + +@Component({ + selector: 'safety', + template: `
+
+
+ Fair Value: + {{ fairValue.price.toFixed(product.tickPrice) }} +
+
+ BuyPing: + {{ tradeSafety.buyPing.toFixed(product.tickPrice) }} +
+
+ SellPing: + {{ tradeSafety.sellPing.toFixed(product.tickPrice) }} +
+
+ BuyTS:{{ tradeSafety.buy.toFixed(2) }} +
+
+ SellTS:{{ tradeSafety.sell.toFixed(2) }} +
+
+ TotalTS:{{ tradeSafety.combined.toFixed(2) }} +
+
+
` +}) +export class SafetyComponent { + + @Input() product: Models.ProductAdvertisement; + + @Input() fairValue: Models.FairValue; + + @Input() tradeSafety: Models.TradeSafety; + + private getClass = (o: number) => { + if (o) return "param-value text-danger"; + else return "param-value text-muted"; + }; +}; diff --git a/src/bin/trading-bot/trading-bot.client/Settings.ts b/src/bin/trading-bot/trading-bot.client/Settings.ts new file mode 100644 index 000000000..3e0b0fec4 --- /dev/null +++ b/src/bin/trading-bot/trading-bot.client/Settings.ts @@ -0,0 +1,514 @@ +import {Component, Input} from '@angular/core'; + +import {Socket, Models} from 'lib/K'; + +@Component({ + selector: 'settings', + template: ` +
+ MARKET MAKING + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
%modesafetybulletsrangerange%pingAtpongAtsopsopWidthsopTradessopSizeordPctTotexpminBbidSize%maxBidSize?minAaskSize%maxAskSize?
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ TECHNICAL ANALYSIS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
apModeverylonglongmediumshortsensibilitytbp%tbpmin%tbpmax%pDivModepDiv%pDivMin%apraprFactorbw?bwSize%w?widthdepth%pingWidth%pongWidth%
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ PROTECTION + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
fvtrades/secewmaPrice?periodsewmaewmaWidth?ewmaTrend?thresholdmicroultrastdevperiodsstddevfactorBB?cxl?lifetimeprofitKmemorydelayUIaudio? + + Applied + + + Pending + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
` +}) +export class SettingsComponent { + + private master: Models.QuotingParameters = JSON.parse(JSON.stringify({})); + private params: Models.QuotingParameters = JSON.parse(JSON.stringify({})); + private pending: boolean = false; + + private availableQuotingModes: Models.Map[] = Models.getMap(Models.QuotingMode); + private availableOrderPctTotals: Models.Map[] = Models.getMap(Models.OrderPctTotal); + private availableQuotingSafeties: Models.Map[] = Models.getMap(Models.QuotingSafety); + private availableFvModels: Models.Map[] = Models.getMap(Models.FairValueModel); + private availableAutoPositionModes: Models.Map[] = Models.getMap(Models.AutoPositionMode); + private availablePdiv: Models.Map[] = Models.getMap(Models.DynamicPDivMode); + private availableAPR: Models.Map[] = Models.getMap(Models.APR); + private availableSuperTrades: Models.Map[] = Models.getMap(Models.SOP); + private availablePingAt: Models.Map[] = Models.getMap(Models.PingAt); + private availablePongAt: Models.Map[] = Models.getMap(Models.PongAt); + private availableSTDEV: Models.Map[] = Models.getMap(Models.STDEV); + + private fireCxl: Socket.IFire = new Socket.Fire(Models.Topics.QuotingParametersChange); + + @Input() product: Models.ProductAdvertisement; + + @Input() set quotingParameters(o: Models.QuotingParameters) { + this.master = JSON.parse(JSON.stringify(o)); + this.params = JSON.parse(JSON.stringify(o)); + this.pending = false; + }; + + private backup = () => { + try { + this.params = JSON.parse(window.prompt( + 'Backup quoting parameters\n\nTo export the current setup,' + + ' copy all from the input below and paste it somewhere else.\n\nTo import' + + ' a setup replacement, replace the quoting parameters below by some new,' + + ' then click [Save] to apply.\n\nCurrent/New setup of quoting parameters:', + JSON.stringify(this.params) + )) || this.params; + } catch(e) {} + }; + + private resetSettings = () => { + this.params = JSON.parse(JSON.stringify(this.master)); + }; + + private submitSettings = () => { + this.pending = true; + this.fireCxl.fire(this.params); + }; +}; diff --git a/src/bin/trading-bot/trading-bot.client/State.ts b/src/bin/trading-bot/trading-bot.client/State.ts new file mode 100644 index 000000000..d767466f2 --- /dev/null +++ b/src/bin/trading-bot/trading-bot.client/State.ts @@ -0,0 +1,36 @@ +import {Component, Input} from '@angular/core'; + +import {Socket, Models} from 'lib/K'; + +@Component({ + selector: 'state-button', + template: `` +}) +export class StateComponent { + + private fireCxl: Socket.IFire = new Socket.Fire(Models.Topics.Connectivity); + + @Input() product: Models.ProductAdvertisement; + + @Input() state: Models.ExchangeState; + + private getClass = () => { + if (this.state.agree === null) return "btn btn-warning"; + else if (this.state.agree) return "btn btn-success"; + else return "btn btn-danger"; + }; + + private toggle = () => { + this.fireCxl.fire(new Models.AgreeRequestFromUI(Math.abs(this.state.agree - 1))); + this.state.agree = null; + }; +}; diff --git a/src/bin/trading-bot/trading-bot.client/Stats.ts b/src/bin/trading-bot/trading-bot.client/Stats.ts new file mode 100644 index 000000000..026bb634e --- /dev/null +++ b/src/bin/trading-bot/trading-bot.client/Stats.ts @@ -0,0 +1,590 @@ +import {Component, Input, OnInit} from '@angular/core'; + +import {Shared, Models} from 'lib/K'; + +@Component({ + selector: 'stats', + template: `
+
+ +
+
+
+ +
+
+ +
+
+
` +}) +export class StatsComponent implements OnInit { + + private fvChart: number = 0; + private baseChart: number = 1; + private quoteChart: number = 2; + + private showStats: boolean; + + private Highcharts: typeof Shared.Highcharts = Shared.Highcharts; + + @Input() product: Models.ProductAdvertisement; + + @Input() fairValue: Models.FairValue; + + @Input() quotingParameters: Models.QuotingParameters; + + @Input() position: Models.PositionReport; + + @Input() marketWidth: number; + + @Input() targetBasePosition: Models.TargetBasePositionValue; + + @Input() marketChart: Models.MarketChart; + + @Input() set _showStats(showStats: boolean) { + if (!this.showStats && showStats) + this.Highcharts.charts.forEach(chart => chart.redraw(false)); + this.showStats = showStats; + }; + + @Input() set tradesChart(o: Models.TradeChart) { + if (!o.price) return; + let time = new Date().getTime(); + this.Highcharts.charts[this.fvChart].series[Models.Side[o.side] == 'Bid' ? 4 : 2].addPoint([time, o.price], false); + this.Highcharts.charts[this.fvChart].series[Models.Side[o.side] == 'Bid' ? 5 : 3].addPoint({ + x: time, + y: o.price, + z: o.quantity, + title: (o.pong ? '⥄' : '➜')+(Models.Side[o.side] == 'Bid' ? 'B' : 'S'), + side: (Models.Side[o.side] == 'Bid' ? 'Buy':'Sell'), + pong: o.pong?'o':'i', + price: o.price.toFixed(this.product.tickPrice), + qty: o.quantity.toFixed(8), + val: o.value.toFixed(this.product.tickPrice) + }, true); + this.updateCharts(time); + }; + + private syncExtremes = function (e) { + var thisChart = this.chart; + if (e.trigger !== 'syncExtremes') { + this.Highcharts.each(this.Highcharts.charts, function (chart) { + if (chart !== thisChart && chart.xAxis[0].setExtremes) + chart.xAxis[0].setExtremes(e.min, e.max, undefined, true, { trigger: 'syncExtremes' }); + }); + } + }; + + private pointFormatterBase = function () { + return this.series.type=='arearange' + ? ''+this.series.name+(this.series.name=="Width" && this.series.chart.options.self.quotingParameters.protectionEwmaWidthPing ?" EWMA":"")+' High: '+ this.high.toFixed(this.series.chart.options.self.product.tickPrice) +' ' + this.series.chart.options.self.product.quote + '' + + ''+this.series.name+(this.series.name=="Width" && this.series.chart.options.self.quotingParameters.protectionEwmaWidthPing?" EWMA":"")+' Low: '+ this.low.toFixed(this.series.chart.options.self.product.tickPrice) +' ' + this.series.chart.options.self.product.quote + '' + : ( this.series.type=='bubble' + ? ''+(this.side == 'Buy' ? '▼':'▲')+' '+this.side+' (P'+this.pong+'ng)' + + '' + 'Price: '+this.price+' '+this.series.chart.options.self.product.quote+'' + + '' + 'Qty: '+this.qty+' '+this.series.chart.options.self.product.base+'' + + '' + 'Value: '+this.val+' '+this.series.chart.options.self.product.quote+'' + :' '+this.series.name+': '+ this.y.toFixed(this.series.chart.options.self.product.tickPrice) +' ' + this.series.chart.options.self.product.quote + '' + ); + }; + + private pointFormatterQuote = function () { + return ' '+this.series.name+': '+this.y.toFixed(8)+' ' + this.series.chart.options.self.product.base + ''; + }; + + private pointFormatterPercentage = function () { + return ' '+this.series.name+': '+this.y.toFixed(4)+' % '; + }; + + private fvChartOptions = { + self: this, + title: 'fair value', + chart: { + width: null, + height: 615, + type: 'bubble', + zoomType: false, + backgroundColor:'rgba(255, 255, 255, 0)' + }, + plotOptions: {series: {marker: {enabled: false}}}, + navigator: {enabled: false}, + rangeSelector:{enabled: false,height:0}, + scrollbar: {enabled: false}, + accessibility: {enabled: false}, + credits: {enabled: false}, + xAxis: { + type: 'datetime', + crosshair: true, + // events: {setExtremes: this.syncExtremes}, + labels: {enabled: true}, + gridLineWidth: 0, + dateTimeLabelFormats: {millisecond: '%H:%M:%S',second: '%H:%M:%S',minute: '%H:%M',hour: '%H:%M',day: '%m-%d',week: '%m-%d',month: '%m',year: '%Y'} + }, + yAxis: [{ + title: {text: 'Fair Value and Trades'}, + labels: {enabled: true}, + crosshair: true, + gridLineWidth: 0 + },{ + title: {text: 'STDEV 20'}, + labels: {enabled: false}, + opposite: true, + gridLineWidth: 0 + },{ + title: {text: 'Market Size'}, + labels: {enabled: false}, + opposite: true, + pointPadding: 0, + groupPadding: 0, + borderWidth: 0, + shadow: false, + gridLineWidth: 0 + },{ + title: {text: 'Percentage'}, + min: -100, + max: 100, + labels: {enabled: false}, + opposite: true, + gridLineWidth: 0 + }], + legend: { + enabled:true, + itemStyle: { + color: 'yellowgreen' + }, + itemHoverStyle: { + color: 'gold' + }, + itemHiddenStyle: { + color: 'darkgrey' + } + }, + tooltip: { + shared: true, + useHTML: true, + headerFormat: 'Fair value:
{point.x:%A} {point.x:%H:%M:%S}', + footerFormat: '
' + }, + series: [{ + name: 'Fair Value', + type: 'spline', + lineWidth:4, + colorIndex: 2, + tooltip: {pointFormatter: this.pointFormatterBase}, + data: [], + id: 'fvseries' + },{ + name: 'Width', + type: 'arearange', + tooltip: {pointFormatter: this.pointFormatterBase}, + lineWidth: 0, + colorIndex: 2, + fillOpacity: 0.2, + zIndex: -1, + data: [] + },{ + name: 'Sell', + type: 'spline', + zIndex:1, + colorIndex: 5, + tooltip: {pointFormatter: this.pointFormatterBase}, + data: [], + id: 'sellseries' + },{ + name: 'Sell', + type: 'bubble', + zIndex:2, + color: 'white', + marker: {enabled: true,fillColor: 'transparent',lineColor: this.Highcharts.getOptions().colors[5]}, + tooltip: {pointFormatter: this.pointFormatterBase}, + dataLabels: {enabled: true,format: '{point.title}'}, + data: [], + linkedTo: ':previous' + },{ + name: 'Buy', + type: 'spline', + zIndex:1, + colorIndex: 0, + tooltip: {pointFormatter: this.pointFormatterBase}, + data: [], + id: 'buyseries' + },{ + name: 'Buy', + type: 'bubble', + zIndex:2, + color: 'white', + marker: {enabled: true,fillColor: 'transparent',lineColor: this.Highcharts.getOptions().colors[0]}, + tooltip: {pointFormatter: this.pointFormatterBase}, + dataLabels: {enabled: true,format: '{point.title}'}, + data: [], + linkedTo: ':previous' + },{ + name: 'EWMA Quote', + type: 'spline', + color: '#ffff00', + tooltip: {pointFormatter: this.pointFormatterBase}, + data: [] + },{ + name: 'EWMA Very Long', + type: 'spline', + colorIndex: 6, + tooltip: {pointFormatter: this.pointFormatterBase}, + data: [] + },{ + name: 'EWMA Long', + type: 'spline', + colorIndex: 5, + tooltip: {pointFormatter: this.pointFormatterBase}, + data: [] + },{ + name: 'EWMA Medium', + type: 'spline', + colorIndex: 4, + tooltip: {pointFormatter: this.pointFormatterBase}, + data: [] + },{ + name: 'EWMA Short', + type: 'spline', + colorIndex: 3, + tooltip: {pointFormatter:this.pointFormatterBase}, + data: [] + },{ + name: 'STDEV Fair', + type: 'spline', + lineWidth:1, + color:'#af451e', + tooltip: {pointFormatter: this.pointFormatterBase}, + yAxis: 1, + visible: false, + data: [] + },{ + name: 'STDEV Tops', + type: 'spline', + lineWidth:1, + color:'#af451e', + tooltip: {pointFormatter: this.pointFormatterBase}, + yAxis: 1, + visible: false, + data: [] + },{ + name: 'STDEV TopAsk', + type: 'spline', + lineWidth:1, + color:'#af451e', + tooltip: {pointFormatter: this.pointFormatterBase}, + yAxis: 1, + visible: false, + data: [] + },{ + name: 'STDEV TopBid', + type: 'spline', + lineWidth:1, + color:'#af451e', + tooltip: {pointFormatter: this.pointFormatterBase}, + yAxis: 1, + visible: false, + data: [] + },{ + name: 'STDEV BBFair', + type: 'arearange', + tooltip: {pointFormatter: this.pointFormatterBase}, + lineWidth: 0, + color:'#af451e', + fillOpacity: 0.2, + zIndex: -1, + visible: false, + data: [] + },{ + name: 'STDEV BBTops', + type: 'arearange', + tooltip: {pointFormatter: this.pointFormatterBase}, + lineWidth: 0, + color:'#af451e', + fillOpacity: 0.2, + zIndex: -1, + visible: false, + data: [] + },{ + name: 'STDEV BBTop', + type: 'arearange', + tooltip: {pointFormatter: this.pointFormatterBase}, + lineWidth: 0, + color:'#af451e', + fillOpacity: 0.2, + zIndex: -1, + visible: false, + data: [] + },{ + type: 'column', + name: 'Market Buy Size', + tooltip: {pointFormatter: this.pointFormatterQuote}, + yAxis: 2, + colorIndex:0, + data: [], + zIndex: -2 + },{ + type: 'column', + name: 'Market Sell Size', + tooltip: {pointFormatter: this.pointFormatterQuote}, + yAxis: 2, + colorIndex:5, + data: [], + zIndex: -2 + },{ + name: 'EWMA Trend Diff', + type: 'spline', + color: '#fd00ff', + tooltip: {pointFormatter: this.pointFormatterPercentage}, + yAxis: 3, + visible: false, + data: [] + }] + }; + + private quoteChartOptions = { + self: this, + title: 'quote wallet', + chart: { + width: null, + height: 306, + zoomType: false, + resetZoomButton: {theme: {display: 'none'}}, + backgroundColor:'rgba(255, 255, 255, 0)' + }, + accessibility: {enabled: false}, + credits: {enabled: false}, + tooltip: { + shared: true, + useHTML: true, + headerFormat: 'Quote wallet:
{point.x:%A} {point.x:%H:%M:%S}', + footerFormat: '
', + pointFormatter: this.pointFormatterBase + }, + plotOptions: { + area: {stacking: 'normal',connectNulls: true,marker: {enabled: false}}, + spline: {marker: {enabled: false}} + }, + xAxis: { + type: 'datetime', + crosshair: true, + // events: {setExtremes: this.syncExtremes}, + labels: {enabled: true}, + dateTimeLabelFormats: {millisecond: '%H:%M:%S',second: '%H:%M:%S',minute: '%H:%M',hour: '%H:%M',day: '%m-%d',week: '%m-%d',month: '%m',year: '%Y'} + }, + yAxis: [{ + title: {text: 'Total Position'}, + opposite: true, + labels: {enabled: false}, + gridLineWidth: 0 + },{ + title: {text: 'Available and Held'}, + min: 0, + labels: {enabled: false}, + gridLineWidth: 0 + }], + legend: {enabled: false}, + series: [{ + name: 'Total Value', + type: 'spline', + zIndex: 1, + colorIndex:2, + lineWidth:3, + data: [] + },{ + name: 'Target', + type: 'spline', + yAxis: 1, + zIndex: 2, + colorIndex:6, + data: [] + },{ + name: 'pDiv', + type: 'arearange', + lineWidth: 0, + yAxis: 1, + colorIndex: 6, + fillOpacity: 0.3, + zIndex: 0, + marker: { enabled: false }, + data: [] + },{ + name: 'Available', + type: 'area', + colorIndex:0, + fillOpacity: 0.2, + yAxis: 1, + data: [] + },{ + name: 'Held', + type: 'area', + yAxis: 1, + marker:{symbol:'triangle-down'}, + data: [] + }] + }; + + private baseChartOptions = { + self: this, + title: 'base wallet', + chart: { + width: null, + height: 306, + zoomType: false, + resetZoomButton: {theme: {display: 'none'}}, + backgroundColor:'rgba(255, 255, 255, 0)' + }, + accessibility: {enabled: false}, + credits: {enabled: false}, + tooltip: { + shared: true, + headerFormat: 'Base wallet:
{point.x:%A} {point.x:%H:%M:%S}', + footerFormat: '
', + useHTML: true, + pointFormatter: this.pointFormatterQuote + }, + plotOptions: { + area: {stacking: 'normal',connectNulls: true,marker: {enabled: false}}, + spline: {marker: {enabled: false}} + }, + xAxis: { + type: 'datetime', + crosshair: true, + // events: {setExtremes: this.syncExtremes}, + labels: {enabled: false}, + dateTimeLabelFormats: {millisecond: '%H:%M:%S',second: '%H:%M:%S',minute: '%H:%M',hour: '%H:%M',day: '%m-%d',week: '%m-%d',month: '%m',year: '%Y'} + }, + yAxis: [{ + title: {text: 'Total Position'}, + opposite: true, + labels: {enabled: false}, + gridLineWidth: 0 + },{ + title: {text: 'Available and Held'}, + min: 0, + labels: {enabled: false}, + gridLineWidth: 0 + }], + legend: {enabled: false}, + series: [{ + name: 'Total Value', + type: 'spline', + zIndex: 1, + colorIndex:2, + lineWidth:3, + data: [] + },{ + name: 'Target (TBP)', + type: 'spline', + yAxis: 1, + zIndex: 2, + colorIndex:6, + data: [] + },{ + name: 'pDiv', + type: 'arearange', + lineWidth: 0, + yAxis: 1, + colorIndex: 6, + fillOpacity: 0.3, + zIndex: 0, + marker: { enabled: false }, + data: [] + },{ + name: 'Available', + type: 'area', + yAxis: 1, + colorIndex:5, + fillOpacity: 0.2, + data: [] + },{ + name: 'Held', + type: 'area', + yAxis: 1, + colorIndex:5, + data: [] + }] + }; + + ngOnInit() { + setInterval(() => this.updateCharts(new Date().getTime()), 10e+3); + }; + + private updateCharts = (time: number) => { + this.removeOldPoints(time); + if (this.fairValue.price) { + if (this.marketChart.stdevWidth) { + if (this.marketChart.stdevWidth.fv) + this.Highcharts.charts[this.fvChart].series[11].addPoint([time, this.marketChart.stdevWidth.fv], false); + if (this.marketChart.stdevWidth.tops) + this.Highcharts.charts[this.fvChart].series[12].addPoint([time, this.marketChart.stdevWidth.tops], false); + if (this.marketChart.stdevWidth.ask) + this.Highcharts.charts[this.fvChart].series[13].addPoint([time, this.marketChart.stdevWidth.ask], false); + if (this.marketChart.stdevWidth.bid) + this.Highcharts.charts[this.fvChart].series[14].addPoint([time, this.marketChart.stdevWidth.bid], false); + if (this.marketChart.stdevWidth.fv && this.marketChart.stdevWidth.fvMean) + this.Highcharts.charts[this.fvChart].series[15].addPoint([time, this.marketChart.stdevWidth.fvMean-this.marketChart.stdevWidth.fv, this.marketChart.stdevWidth.fvMean+this.marketChart.stdevWidth.fv], this.showStats, false, false); + if (this.marketChart.stdevWidth.tops && this.marketChart.stdevWidth.topsMean) + this.Highcharts.charts[this.fvChart].series[16].addPoint([time, this.marketChart.stdevWidth.topsMean-this.marketChart.stdevWidth.tops, this.marketChart.stdevWidth.topsMean+this.marketChart.stdevWidth.tops], this.showStats, false, false); + if (this.marketChart.stdevWidth.ask && this.marketChart.stdevWidth.bid && this.marketChart.stdevWidth.askMean && this.marketChart.stdevWidth.bidMean) + this.Highcharts.charts[this.fvChart].series[17].addPoint([time, this.marketChart.stdevWidth.bidMean-this.marketChart.stdevWidth.bid, this.marketChart.stdevWidth.askMean+this.marketChart.stdevWidth.ask], this.showStats, false, false); + } + this.Highcharts.charts[this.fvChart].yAxis[2].setExtremes(0, Math.max(this.marketChart.tradesBuySize*4,this.marketChart.tradesSellSize*4,this.Highcharts.charts[this.fvChart].yAxis[2].getExtremes().dataMax*4), false, true, { trigger: 'syncExtremes' }); + if (this.marketChart.tradesBuySize) + this.Highcharts.charts[this.fvChart].series[18].addPoint([time, this.marketChart.tradesBuySize], false); + if (this.marketChart.tradesSellSize) + this.Highcharts.charts[this.fvChart].series[19].addPoint([time, this.marketChart.tradesSellSize], false); + this.marketChart.tradesBuySize = 0; + this.marketChart.tradesSellSize = 0; + if (this.marketChart.ewma) { + if (this.marketChart.ewma.ewmaQuote) + this.Highcharts.charts[this.fvChart].series[6].addPoint([time, this.marketChart.ewma.ewmaQuote], false); + if (this.marketChart.ewma.ewmaVeryLong) + this.Highcharts.charts[this.fvChart].series[7].addPoint([time, this.marketChart.ewma.ewmaVeryLong], false); + if (this.marketChart.ewma.ewmaLong) + this.Highcharts.charts[this.fvChart].series[8].addPoint([time, this.marketChart.ewma.ewmaLong], false); + if (this.marketChart.ewma.ewmaMedium) + this.Highcharts.charts[this.fvChart].series[9].addPoint([time, this.marketChart.ewma.ewmaMedium], false); + if (this.marketChart.ewma.ewmaShort) + this.Highcharts.charts[this.fvChart].series[10].addPoint([time, this.marketChart.ewma.ewmaShort], false); + if (this.marketChart.ewma.ewmaTrendDiff) + this.Highcharts.charts[this.fvChart].series[20].addPoint([time, this.marketChart.ewma.ewmaTrendDiff], false); + } + this.Highcharts.charts[this.fvChart].series[0].addPoint([time, this.fairValue.price], this.showStats); + if (this.quotingParameters.protectionEwmaWidthPing && (this.marketChart.ewma && this.marketChart.ewma.ewmaWidth)) + this.Highcharts.charts[this.fvChart].series[1].addPoint([time, this.fairValue.price-this.marketChart.ewma.ewmaWidth, this.fairValue.price+this.marketChart.ewma.ewmaWidth], this.showStats, false, false); + else if (this.marketWidth) + this.Highcharts.charts[this.fvChart].series[1].addPoint([time, this.fairValue.price-this.marketWidth, this.fairValue.price+this.marketWidth], this.showStats, false, false); + } + if (this.position.base.value || this.position.quote.value) { + this.Highcharts.charts[this.quoteChart].yAxis[1].setExtremes(0, Math.max(this.position.quote.value,this.Highcharts.charts[this.quoteChart].yAxis[1].getExtremes().dataMax), false, true, { trigger: 'syncExtremes' }); + this.Highcharts.charts[this.baseChart].yAxis[1].setExtremes(0, Math.max(this.position.base.value,this.Highcharts.charts[this.baseChart].yAxis[1].getExtremes().dataMax), false, true, { trigger: 'syncExtremes' }); + this.Highcharts.charts[this.quoteChart].series[1].addPoint([time, (this.position.base.value-this.targetBasePosition.tbp)*this.position.quote.value/this.position.base.value], false); + this.Highcharts.charts[this.baseChart].series[1].addPoint([time, this.targetBasePosition.tbp], false); + this.Highcharts.charts[this.quoteChart].series[2].addPoint([time, Math.max(0, this.position.base.value-this.targetBasePosition.tbp-this.targetBasePosition.pDiv)*this.position.quote.value/this.position.base.value, Math.min(this.position.base.value, this.position.base.value-this.targetBasePosition.tbp+this.targetBasePosition.pDiv)*this.position.quote.value/this.position.base.value], this.showStats, false, false); + this.Highcharts.charts[this.baseChart].series[2].addPoint([time, Math.max(0,this.targetBasePosition.tbp-this.targetBasePosition.pDiv), Math.min(this.position.base.value, this.targetBasePosition.tbp+this.targetBasePosition.pDiv)], this.showStats, false, false); + this.Highcharts.charts[this.quoteChart].series[0].addPoint([time, this.position.quote.value], false); + this.Highcharts.charts[this.quoteChart].series[3].addPoint([time, this.position.quote.amount], false); + this.Highcharts.charts[this.quoteChart].series[4].addPoint([time, this.position.quote.held], this.showStats); + this.Highcharts.charts[this.baseChart].series[0].addPoint([time, this.position.base.value], false); + this.Highcharts.charts[this.baseChart].series[3].addPoint([time, this.position.base.amount], false); + this.Highcharts.charts[this.baseChart].series[4].addPoint([time, this.position.base.held], this.showStats); + } + }; + + private removeOldPoints = (time: number) => { + this.Highcharts.charts.forEach(chart => { chart.series.forEach(serie => { + while(serie.data.length && Math.abs(time - serie.data[0].x) > this.quotingParameters.profitHourInterval * 36e+5) + serie.data[0].remove(false); + })}); + }; +}; diff --git a/src/bin/trading-bot/trading-bot.client/Submit.ts b/src/bin/trading-bot/trading-bot.client/Submit.ts new file mode 100644 index 000000000..93f1e73be --- /dev/null +++ b/src/bin/trading-bot/trading-bot.client/Submit.ts @@ -0,0 +1,99 @@ +import {Component, Input} from '@angular/core'; + +import {Socket, Models} from 'lib/K'; + +@Component({ + selector: 'submit-order', + template: ` + + + + + + + + + + + + + + + + + + + +
Side:Price:Size:TIF:Type:
+ + + + + + + + + + + +
` +}) +export class SubmitComponent { + + private side: Models.Side = Models.Side.Bid; + private price: number; + private quantity: number; + private timeInForce: Models.TimeInForce = Models.TimeInForce.GTC; + private type: Models.OrderType = Models.OrderType.Limit; + + private availableSides: Models.Map[] = Models.getMap(Models.Side); + private availableTifs: Models.Map[] = Models.getMap(Models.TimeInForce); + private availableOrderTypes: Models.Map[] = Models.getMap(Models.OrderType); + + private fireCxl: Socket.IFire = new Socket.Fire(Models.Topics.SubmitNewOrder); + + @Input() product: Models.ProductAdvertisement; + + private submitManualOrder = () => { + if (this.price && this.quantity) + this.fireCxl.fire(new Models.OrderRequestFromUI( + this.product.symbol, + this.side, + this.price, + this.quantity, + this.timeInForce, + this.type + )); + }; +}; diff --git a/src/bin/trading-bot/trading-bot.client/Takers.ts b/src/bin/trading-bot/trading-bot.client/Takers.ts new file mode 100644 index 000000000..46768e087 --- /dev/null +++ b/src/bin/trading-bot/trading-bot.client/Takers.ts @@ -0,0 +1,121 @@ +import {Component, Input} from '@angular/core'; + +import {GridOptions, GridApi, RowNode} from 'ag-grid-community'; + +import {Shared, Models} from 'lib/K'; + +@Component({ + selector: 'takers', + template: `` +}) +export class TakersComponent { + + @Input() product: Models.ProductAdvertisement; + + @Input() set taker(o: Models.MarketTrade[]) { + this.addRowData(o); + }; + + private api: GridApi; + + private grid: GridOptions = { + overlayLoadingTemplate: `empty history`, + overlayNoRowsTemplate: `empty history`, + defaultColDef: { sortable: true, resizable: true, flex: 1 }, + rowHeight:25, + headerHeight:25, + animateRows:false, + getRowId: (params: any) => params.data.id, + columnDefs: [{ + field: 'time', + width: 82, + headerName: 'time', + sort: 'desc', + suppressSizeToFit: true, + cellClassRules: { + 'text-muted': '!data.recent' + }, + cellRenderer: (params) => { + var d = new Date(params.value||0); + return (d.getHours()+'') + .padStart(2, "0")+':'+(d.getMinutes()+'') + .padStart(2, "0")+':'+(d.getSeconds()+'') + .padStart(2, "0")+','+(d.getMilliseconds()+'') + .padStart(3, "0"); + } + }, { + field: 'price', + width: 85, + headerName: 'price', + cellRenderer: (params) => `` + params.value + `` + ` `, + cellClassRules: { + 'sell': 'data.side == "Ask"', + 'buy': 'data.side == "Bid"' + } + }, { + field: 'quantity', + width: 50, + headerName: 'qty', + cellRenderer: (params) => `` + params.value + `` + ` `, + cellClassRules: { + 'sell': 'data.side == "Ask"', + 'buy': 'data.side == "Bid"' + } + }, { + field: 'side', + width: 40, + headerName: 'side', + cellClassRules: { + 'sell': 'x == "Ask"', + 'buy': 'x == "Bid"' + }, + }] + }; + + private onGridReady($event: any) { + if ($event.api) this.api = $event.api; + }; + + private onGridTheme() { + return document.body.classList.contains('theme-dark') + ? '-dark' : ''; + }; + + private addRowData = (o: Models.MarketTrade[]) => { + if (!this.api) return; + + if (!o.length) this.api.setGridOption('rowData', []); + else { + var add: any[] = []; + o.forEach(o => { + add.push({ + id: Math.random().toString(), + price: Shared.str(o.price, this.product.tickPrice), + quantity: Shared.str(o.quantity, this.product.tickSize), + time: o.time, + recent: true, + side: Models.Side[o.side] + }); + }); + + this.api.applyTransactionAsync({add}, () => { + var txn: any = { + update: [], + remove: [] + }; + this.api.forEachNodeAfterFilterAndSort((node: RowNode, index: number) => { + if (index > 30) + txn.remove.push({id: node.data.id}); + else if (node.data.recent && Math.abs(o[o.length-1].time - node.data.time) > 7000) + txn.update.push(Object.assign(node.data, {recent: false})); + }); + this.api.applyTransaction(txn); + }); + } + }; +}; diff --git a/src/bin/trading-bot/trading-bot.client/Trades.ts b/src/bin/trading-bot/trading-bot.client/Trades.ts new file mode 100644 index 000000000..45b3bd614 --- /dev/null +++ b/src/bin/trading-bot/trading-bot.client/Trades.ts @@ -0,0 +1,262 @@ +import {Component, Input, Output, EventEmitter} from '@angular/core'; + +import {GridOptions, GridApi, IRowNode, RowNode, ColDef} from 'ag-grid-community'; + +import {Socket, Shared, Models} from 'lib/K'; + +@Component({ + selector: 'trades', + template: `` +}) +export class TradesComponent { + + private audio: boolean; + private hasPongs: boolean; + private headerNameMod: string = ""; + + private fireCxl: Socket.IFire = new Socket.Fire(Models.Topics.CleanTrade); + + @Input() product: Models.ProductAdvertisement; + + @Input() set quotingParameters(o: Models.QuotingParameters) { + this.addRowConfig(o); + }; + + @Input() set trade(o: Models.Trade) { + this.addRowData(o); + }; + + @Output() onTradesLength = new EventEmitter(); + + @Output() onTradesMatchedLength = new EventEmitter(); + + @Output() onTradesChartData = new EventEmitter(); + + private api: GridApi; + + private grid: GridOptions = { + overlayLoadingTemplate: `0 closed orders`, + overlayNoRowsTemplate: `0 closed orders`, + defaultColDef: { sortable: true, resizable: true, flex: 1 }, + rowHeight:28, + headerHeight:25, + animateRows:true, + getRowId: (params: any) => params.data.tradeId, + columnDefs: [{ + width: 30, + field: 'cancel', + headerName: 'cxl', + suppressSizeToFit: true, + cellRenderer: (params) => { + return ``; + } + }, { + width: 95, + field:'time', + sort: 'desc', + headerValueGetter:(params) => this.headerNameMod + 'time', + suppressSizeToFit: true, + comparator: (valueA: number, valueB: number, nodeA: RowNode, nodeB: RowNode, isInverted: boolean) => { + return (nodeA.data.Ktime||nodeA.data.time) - (nodeB.data.Ktime||nodeB.data.time); + }, + cellRenderer: (params) => { + var d = new Date(params.value||0); + return (d.getDate()+'') + .padStart(2, "0")+'/'+((d.getMonth()+1)+'') + .padStart(2, "0")+' '+(d.getHours()+'') + .padStart(2, "0")+':'+(d.getMinutes()+'') + .padStart(2, "0")+':'+(d.getSeconds()+'') + .padStart(2, "0"); + } + }, { + width: 95, + field:'Ktime', + headerName:'⇋time', + hide:true, + suppressSizeToFit: true, + cellRenderer: (params) => { + if (params.value==0) return ''; + var d = new Date(params.value); + return (d.getDate()+'') + .padStart(2, "0")+'/'+((d.getMonth()+1)+'') + .padStart(2, "0")+' '+(d.getHours()+'') + .padStart(2, "0")+':'+(d.getMinutes()+'') + .padStart(2, "0")+':'+(d.getSeconds()+'') + .padStart(2, "0"); + } + }, { + width: 50, + field:'side', + headerName:'side', + suppressSizeToFit: true, + cellClassRules: { + 'sell': 'x == "Ask"', + 'buy': 'x == "Bid"', + 'kira': 'x == "⥄"' + }, + cellRenderer: (params) => params.value === '⥄' + ? '' + params.value + '' + : params.value + }, { + width: 80, + field:'price', + headerValueGetter:(params) => this.headerNameMod + 'price', + cellRenderer: (params) => `` + params.value + `` + ` `, + cellClassRules: { + 'sell': 'data._side == "Ask"', + 'buy': 'data._side == "Bid"' + } + }, { + width: 95, + field:'quantity', + headerValueGetter:(params) => this.headerNameMod + 'qty', + cellRenderer: (params) => `` + params.value + `` + ` `, + suppressSizeToFit: true, + cellClassRules: { + 'sell': 'data._side == "Ask"', + 'buy': 'data._side == "Bid"' + } + }, { + width: 69, + field:'value', + headerValueGetter:(params) => this.headerNameMod + 'value', + cellRenderer: (params) => `` + params.value + `` + ` `, + cellClassRules: { + 'sell': 'data._side == "Ask"', + 'buy': 'data._side == "Bid"' + } + }, { + width: 75, + field:'Kvalue', + headerName:'⥄value', + cellRenderer: (params) => `` + params.value + `` + ` `, + cellClassRules: { + 'buy': 'data._side == "Ask"', + 'sell': 'data._side == "Bid"' + } + }, { + width: 85, + field:'Kqty', + headerName:'⥄qty', + suppressSizeToFit: true, + cellRenderer: (params) => `` + params.value + `` + ` `, + cellClassRules: { + 'buy': 'data._side == "Ask"', + 'sell': 'data._side == "Bid"' + } + }, { + width: 80, + field:'Kprice', + headerName:'⥄price', + cellRenderer: (params) => `` + params.value + `` + ` `, + cellClassRules: { + 'buy': 'data._side == "Ask"', + 'sell': 'data._side == "Bid"' + } + }, { + width: 65, + field:'delta', + headerName:'delta', + cellClassRules: { + 'kira': 'data.side == "⥄"' + }, + cellRenderer: (params) => params.value + ? `` + params.value + `` + ` ` + : '' + }] + }; + + private onGridReady($event: any) { + if ($event.api) this.api = $event.api; + }; + + private onCellClicked = ($event) => { + if ($event.event.target.getAttribute('data-action-type') != 'remove') return; + this.fireCxl.fire(new Models.CleanTradeRequestFromUI($event.data.tradeId)); + } + + private addRowConfig = (o: Models.QuotingParameters) => { + this.audio = o.audio; + + this.hasPongs = (o.safety === Models.QuotingSafety.Boomerang || o.safety === Models.QuotingSafety.AK47); + + this.headerNameMod = this.hasPongs ? "➜" : ""; + + if (!this.api) return; + + this.grid.columnDefs.map((x: ColDef) => { + if (['Ktime','Kqty','Kprice','Kvalue','delta'].indexOf(x.field) > -1) + this.api.setColumnsVisible([x.field], this.hasPongs); + return x; + }); + + this.api.refreshHeader(); + + this.emitLengths(); + }; + + private addRowData = (o: Models.Trade) => { + if (!this.api) return; + + if (o === null) this.api.setGridOption('rowData', []); + else { + var node: IRowNode = this.api.getRowNode(o.tradeId); + if (o.Kqty < 0) { + if (node) + this.api.applyTransaction({remove: [node.data]}); + } else { + var edit = { + time: o.time, + quantity: Shared.str(o.quantity, this.product.tickSize), + value: Shared.str(o.value, this.product.tickPrice), + Ktime: o.Ktime, + Kqty: o.Kqty ? Shared.str(o.Kqty, this.product.tickSize) : '', + Kprice: o.Kprice ? Shared.str(o.Kprice, this.product.tickPrice) : '', + Kvalue: o.Kvalue ? Shared.str(o.Kvalue, this.product.tickPrice) : '', + delta: Shared.str(o.delta, 8), + side: o.Kqty >= o.quantity ? '⥄' : (o.side === Models.Side.Ask ? "Ask" : "Bid"), + _side: o.side === Models.Side.Ask ? "Ask" : "Bid", + }; + + if (node) node.setData(Object.assign(node.data, edit)); + else this.api.applyTransaction({add: [Object.assign(edit, { + tradeId: o.tradeId, + price: Shared.str(o.price, this.product.tickPrice) + })]}); + + if (o.loadedFromDB === false) { + if (this.audio) Shared.playAudio(o.isPong?'1':'0'); + + this.onTradesChartData.emit(new Models.TradeChart( + (o.isPong && o.Kprice)?o.Kprice:o.price, + (o.isPong && o.Kprice)?(o.side === Models.Side.Ask ? Models.Side.Bid : Models.Side.Ask):o.side, + (o.isPong && o.Kprice)?o.Kqty:o.quantity, + (o.isPong && o.Kprice)?o.Kvalue:o.value, + o.isPong + )); + } + } + } + + this.emitLengths(); + }; + + private emitLengths = () => { + this.onTradesLength.emit(this.api.getDisplayedRowCount()); + var tradesMatched: number = 0; + if (this.hasPongs) { + this.api.forEachNode((node: RowNode) => { + if (node.data.Kqty) tradesMatched++; + }); + } else tradesMatched = -1; + this.onTradesMatchedLength.emit(tradesMatched); + }; +}; diff --git a/src/bin/trading-bot/trading-bot.client/Wallet.ts b/src/bin/trading-bot/trading-bot.client/Wallet.ts new file mode 100644 index 000000000..022816bb6 --- /dev/null +++ b/src/bin/trading-bot/trading-bot.client/Wallet.ts @@ -0,0 +1,75 @@ +import {Component, Input} from '@angular/core'; + +import {Models} from 'lib/K'; + +@Component({ + selector: 'wallet', + template: `
+

+ {{ product.base }}: +
+ {{ position.base.amount.toFixed(8) }} +
+ {{ position.base.held.toFixed(8) }} +
+ {{ (position.base.amount + position.base.held).toFixed(8)}} +
+

+

+ {{ product.quote }}: +
+ {{ position.quote.amount.toFixed(product.tickPrice) }} +
+ {{ position.quote.held.toFixed(product.tickPrice) }} +
+ {{ (position.quote.amount + position.quote.held).toFixed(product.tickPrice) }} +
+

+

+ Value: +
+ {{ position.base.value.toFixed(8) }} +
+ {{ position.quote.value.toFixed(product.tickPrice) }} +

+

+ + {{ position.base.profit>=0?'+':'' }}{{ position.base.profit.toFixed(2) }}% + , + {{ position.quote.profit>=0?'+':'' }}{{ position.quote.profit.toFixed(2) }}% + +

+
` +}) +export class WalletComponent { + + @Input() product: Models.ProductAdvertisement; + + @Input() position: Models.PositionReport; +}; diff --git a/src/bin/trading-bot/trading-bot.data.h b/src/bin/trading-bot/trading-bot.data.h new file mode 100644 index 000000000..aa53f001c --- /dev/null +++ b/src/bin/trading-bot/trading-bot.data.h @@ -0,0 +1,2605 @@ +//! \file +//! \brief Trading logistics (welcome user! forked from https://github.com/michaelgrosner/tribeca). + +namespace tribeca { + enum class QuotingMode: unsigned int { + Top, Mid, Join, InverseJoin, InverseTop, HamelinRat, Depth + }; + enum class OrderPctTotal: unsigned int { + Value, Side, TBPValue, TBPSide + }; + enum class QuotingSafety: unsigned int { + Off, PingPong, PingPoing, Boomerang, AK47 + }; + enum class FairValueModel: unsigned int { + BBO, wBBO, rwBBO + }; + enum class AutoPositionMode: unsigned int { + Manual, EWMA_LS, EWMA_LMS, EWMA_4 + }; + enum class PDivMode: unsigned int { + Manual, Linear, Sine, SQRT, Switch + }; + enum class APR: unsigned int { + Off, Size, SizeWidth + }; + enum class SideAPR: unsigned int { + Off, Buy, Sell + }; + enum class SOP: unsigned int { + Off, Trades, Size, TradesSize + }; + enum class STDEV: unsigned int { + Off, OnFV, OnFVAPROff, OnTops, OnTopsAPROff, OnTop, OnTopAPROff + }; + enum class PingAt: unsigned int { + BothSides, BidSide, AskSide, + DepletedSide, DepletedBidSide, DepletedAskSide, + StopPings + }; + enum class PongAt: unsigned int { + ShortPingFair, AveragePingFair, LongPingFair, + ShortPingAggressive, AveragePingAggressive, LongPingAggressive + }; + + struct QuotingParams: public Sqlite::StructBackup, + public Client::Broadcast, + public Client::Clickable { + Price widthPing = 300.0; + double widthPingPercentage = 0.25; + Price widthPong = 300.0; + double widthPongPercentage = 0.25; + bool widthPercentage = false; + bool bestWidth = true; + Amount bestWidthSize = 0; + OrderPctTotal orderPctTotal = OrderPctTotal::Value; + double tradeSizeTBPExp = 2.0; + Amount buySize = 0.02; + double buySizePercentage = 7.0; + bool buySizeMax = false; + Amount sellSize = 0.01; + double sellSizePercentage = 7.0; + bool sellSizeMax = false; + PingAt pingAt = PingAt::BothSides; + PongAt pongAt = PongAt::ShortPingFair; + QuotingMode mode = QuotingMode::Top; + QuotingSafety safety = QuotingSafety::PingPong; + unsigned int bullets = 2; + Price range = 0.5; + double rangePercentage = 5.0; + FairValueModel fvModel = FairValueModel::BBO; + Amount targetBasePosition = 1.0; + double targetBasePositionPercentage = 50.0; + Amount targetBasePositionMin = 0.1; + double targetBasePositionPercentageMin = 10.0; + Amount targetBasePositionMax = 1; + double targetBasePositionPercentageMax = 90.0; + Amount positionDivergence = 0.9; + Amount positionDivergenceMin = 0.4; + double positionDivergencePercentage = 21.0; + double positionDivergencePercentageMin = 10.0; + PDivMode positionDivergenceMode = PDivMode::Manual; + bool percentageValues = false; + AutoPositionMode autoPositionMode = AutoPositionMode::EWMA_LS; + APR aggressivePositionRebalancing = APR::Off; + SOP superTrades = SOP::Off; + unsigned int tradesPerMinute = 1; + unsigned int tradeRateSeconds = 3; + bool protectionEwmaWidthPing = false; + bool protectionEwmaQuotePrice = true; + unsigned int protectionEwmaPeriods = 200; + STDEV quotingStdevProtection = STDEV::Off; + bool quotingStdevBollingerBands = false; + double quotingStdevProtectionFactor = 1.0; + unsigned int quotingStdevProtectionPeriods = 1200; + double ewmaSensiblityPercentage = 0.5; + bool quotingEwmaTrendProtection = false; + double quotingEwmaTrendThreshold = 2.0; + unsigned int veryLongEwmaPeriods = 400; + unsigned int longEwmaPeriods = 200; + unsigned int mediumEwmaPeriods = 100; + unsigned int shortEwmaPeriods = 50; + unsigned int extraShortEwmaPeriods = 12; + unsigned int ultraShortEwmaPeriods = 3; + double aprMultiplier = 2; + double sopWidthMultiplier = 2; + double sopSizeMultiplier = 2; + double sopTradesMultiplier = 2; + bool cancelOrdersAuto = false; + unsigned int lifetime = 0; + double cleanPongsAuto = 0.0; + double profitHourInterval = 0.5; + bool audio = false; + unsigned int delayUI = 3; + int _diffEwma = -1; + private_ref: + const KryptoNinja &K; + public: + QuotingParams(const KryptoNinja &bot) + : StructBackup(bot) + , Broadcast(bot) + , Clickable(bot) + , K(bot) + {}; + void from_json(const json &j) { + const vector previous = { + veryLongEwmaPeriods, + longEwmaPeriods, + mediumEwmaPeriods, + shortEwmaPeriods, + extraShortEwmaPeriods, + ultraShortEwmaPeriods + }; + widthPing = fmax(K.gateway->tickPrice, j.value("widthPing", widthPing)); + widthPingPercentage = fmin(1e+2, fmax(1e-3, j.value("widthPingPercentage", widthPingPercentage))); + widthPong = fmax(K.gateway->tickPrice, j.value("widthPong", widthPong)); + widthPongPercentage = fmin(1e+2, fmax(1e-3, j.value("widthPongPercentage", widthPongPercentage))); + widthPercentage = j.value("widthPercentage", widthPercentage); + bestWidth = j.value("bestWidth", bestWidth); + bestWidthSize = fmax(0, j.value("bestWidthSize", bestWidthSize)); + orderPctTotal = j.value("orderPctTotal", orderPctTotal); + tradeSizeTBPExp = j.value("tradeSizeTBPExp", tradeSizeTBPExp); + buySize = fmax(K.gateway->minSize, j.value("buySize", buySize)); + buySizePercentage = fmin(1e+2, fmax(1e-3, j.value("buySizePercentage", buySizePercentage))); + buySizeMax = j.value("buySizeMax", buySizeMax); + sellSize = fmax(K.gateway->minSize, j.value("sellSize", sellSize)); + sellSizePercentage = fmin(1e+2, fmax(1e-3, j.value("sellSizePercentage", sellSizePercentage))); + sellSizeMax = j.value("sellSizeMax", sellSizeMax); + pingAt = j.value("pingAt", pingAt); + pongAt = j.value("pongAt", pongAt); + mode = j.value("mode", mode); + safety = j.value("safety", safety); + bullets = fmin(10, fmax(1, j.value("bullets", bullets))); + range = j.value("range", range); + rangePercentage = fmin(1e+2, fmax(1e-3, j.value("rangePercentage", rangePercentage))); + fvModel = j.value("fvModel", fvModel); + targetBasePosition = j.value("targetBasePosition", targetBasePosition); + targetBasePositionPercentage = fmin(1e+2, fmax(0, j.value("targetBasePositionPercentage", targetBasePositionPercentage))); + targetBasePositionMin = j.value("targetBasePositionMin", targetBasePositionMin); + targetBasePositionPercentageMin = fmin(1e+2, fmax(0, j.value("targetBasePositionPercentageMin", targetBasePositionPercentageMin))); + targetBasePositionMax = j.value("targetBasePositionMax", targetBasePositionMax); + targetBasePositionPercentageMax = fmin(1e+2, fmax(0, j.value("targetBasePositionPercentageMax", targetBasePositionPercentageMax))); + positionDivergenceMin = j.value("positionDivergenceMin", positionDivergenceMin); + positionDivergenceMode = j.value("positionDivergenceMode", positionDivergenceMode); + positionDivergence = j.value("positionDivergence", positionDivergence); + positionDivergencePercentage = fmin(1e+2, fmax(0, j.value("positionDivergencePercentage", positionDivergencePercentage))); + positionDivergencePercentageMin = fmin(1e+2, fmax(0, j.value("positionDivergencePercentageMin", positionDivergencePercentageMin))); + percentageValues = j.value("percentageValues", percentageValues); + autoPositionMode = j.value("autoPositionMode", autoPositionMode); + aggressivePositionRebalancing = j.value("aggressivePositionRebalancing", aggressivePositionRebalancing); + superTrades = j.value("superTrades", superTrades); + tradesPerMinute = fmax(0, j.value("tradesPerMinute", tradesPerMinute)); + tradeRateSeconds = fmax(0, j.value("tradeRateSeconds", tradeRateSeconds)); + protectionEwmaWidthPing = j.value("protectionEwmaWidthPing", protectionEwmaWidthPing); + protectionEwmaQuotePrice = j.value("protectionEwmaQuotePrice", protectionEwmaQuotePrice); + protectionEwmaPeriods = fmax(1, j.value("protectionEwmaPeriods", protectionEwmaPeriods)); + quotingStdevProtection = j.value("quotingStdevProtection", quotingStdevProtection); + quotingStdevBollingerBands = j.value("quotingStdevBollingerBands", quotingStdevBollingerBands); + quotingStdevProtectionFactor = j.value("quotingStdevProtectionFactor", quotingStdevProtectionFactor); + quotingStdevProtectionPeriods = fmax(1, j.value("quotingStdevProtectionPeriods", quotingStdevProtectionPeriods)); + ewmaSensiblityPercentage = j.value("ewmaSensiblityPercentage", ewmaSensiblityPercentage); + quotingEwmaTrendProtection = j.value("quotingEwmaTrendProtection", quotingEwmaTrendProtection); + quotingEwmaTrendThreshold = j.value("quotingEwmaTrendThreshold", quotingEwmaTrendThreshold); + veryLongEwmaPeriods = fmax(1, j.value("veryLongEwmaPeriods", veryLongEwmaPeriods)); + longEwmaPeriods = fmax(1, j.value("longEwmaPeriods", longEwmaPeriods)); + mediumEwmaPeriods = fmax(1, j.value("mediumEwmaPeriods", mediumEwmaPeriods)); + shortEwmaPeriods = fmax(1, j.value("shortEwmaPeriods", shortEwmaPeriods)); + extraShortEwmaPeriods = fmax(1, j.value("extraShortEwmaPeriods", extraShortEwmaPeriods)); + ultraShortEwmaPeriods = fmax(1, j.value("ultraShortEwmaPeriods", ultraShortEwmaPeriods)); + aprMultiplier = j.value("aprMultiplier", aprMultiplier); + sopWidthMultiplier = j.value("sopWidthMultiplier", sopWidthMultiplier); + sopSizeMultiplier = j.value("sopSizeMultiplier", sopSizeMultiplier); + sopTradesMultiplier = j.value("sopTradesMultiplier", sopTradesMultiplier); + cancelOrdersAuto = j.value("cancelOrdersAuto", cancelOrdersAuto); + lifetime = fmax(K.arg("lifetime"), j.value("lifetime", lifetime)); + cleanPongsAuto = j.value("cleanPongsAuto", cleanPongsAuto); + profitHourInterval = j.value("profitHourInterval", profitHourInterval); + audio = j.value("audio", audio); + delayUI = fmax(0, j.value("delayUI", delayUI)); + if (mode == QuotingMode::Depth) + widthPercentage = false; + K.timer_ticks_factor(delayUI); + K.client_queue_delay(delayUI); + if (_diffEwma == -1) _diffEwma++; + else { + _diffEwma |= (previous[0] != veryLongEwmaPeriods) << 0; + _diffEwma |= (previous[1] != longEwmaPeriods) << 1; + _diffEwma |= (previous[2] != mediumEwmaPeriods) << 2; + _diffEwma |= (previous[3] != shortEwmaPeriods) << 3; + _diffEwma |= (previous[4] != extraShortEwmaPeriods) << 4; + _diffEwma |= (previous[5] != ultraShortEwmaPeriods) << 5; + } + K.clicked(this); + _diffEwma = 0; + }; + void click(const json &j) override { + from_json(j); + backup(); + broadcast(); + }; + mMatter about() const override { + return mMatter::QuotingParameters; + }; + private: + string explain() const override { + return "Quoting Parameters"; + }; + string explainKO() const override { + return "using default values for %"; + }; + }; + static void to_json(json &j, const QuotingParams &k) { + j = { + { "widthPing", k.widthPing }, + { "widthPingPercentage", k.widthPingPercentage }, + { "widthPong", k.widthPong }, + { "widthPongPercentage", k.widthPongPercentage }, + { "widthPercentage", k.widthPercentage }, + { "bestWidth", k.bestWidth }, + { "bestWidthSize", k.bestWidthSize }, + { "orderPctTotal", k.orderPctTotal }, + { "tradeSizeTBPExp", k.tradeSizeTBPExp }, + { "buySize", k.buySize }, + { "buySizePercentage", k.buySizePercentage }, + { "buySizeMax", k.buySizeMax }, + { "sellSize", k.sellSize }, + { "sellSizePercentage", k.sellSizePercentage }, + { "sellSizeMax", k.sellSizeMax }, + { "pingAt", k.pingAt }, + { "pongAt", k.pongAt }, + { "mode", k.mode }, + { "safety", k.safety }, + { "bullets", k.bullets }, + { "range", k.range }, + { "rangePercentage", k.rangePercentage }, + { "fvModel", k.fvModel }, + { "targetBasePosition", k.targetBasePosition }, + { "targetBasePositionPercentage", k.targetBasePositionPercentage }, + { "targetBasePositionMin", k.targetBasePositionMin }, + {"targetBasePositionPercentageMin", k.targetBasePositionPercentageMin}, + { "targetBasePositionMax", k.targetBasePositionMax }, + {"targetBasePositionPercentageMax", k.targetBasePositionPercentageMax}, + { "positionDivergence", k.positionDivergence }, + { "positionDivergencePercentage", k.positionDivergencePercentage }, + { "positionDivergenceMin", k.positionDivergenceMin }, + {"positionDivergencePercentageMin", k.positionDivergencePercentageMin}, + { "positionDivergenceMode", k.positionDivergenceMode }, + { "percentageValues", k.percentageValues }, + { "autoPositionMode", k.autoPositionMode }, + { "aggressivePositionRebalancing", k.aggressivePositionRebalancing }, + { "superTrades", k.superTrades }, + { "tradesPerMinute", k.tradesPerMinute }, + { "tradeRateSeconds", k.tradeRateSeconds }, + { "protectionEwmaWidthPing", k.protectionEwmaWidthPing }, + { "protectionEwmaQuotePrice", k.protectionEwmaQuotePrice }, + { "protectionEwmaPeriods", k.protectionEwmaPeriods }, + { "quotingStdevProtection", k.quotingStdevProtection }, + { "quotingStdevBollingerBands", k.quotingStdevBollingerBands }, + { "quotingStdevProtectionFactor", k.quotingStdevProtectionFactor }, + { "quotingStdevProtectionPeriods", k.quotingStdevProtectionPeriods }, + { "ewmaSensiblityPercentage", k.ewmaSensiblityPercentage }, + { "quotingEwmaTrendProtection", k.quotingEwmaTrendProtection }, + { "quotingEwmaTrendThreshold", k.quotingEwmaTrendThreshold }, + { "veryLongEwmaPeriods", k.veryLongEwmaPeriods }, + { "longEwmaPeriods", k.longEwmaPeriods }, + { "mediumEwmaPeriods", k.mediumEwmaPeriods }, + { "shortEwmaPeriods", k.shortEwmaPeriods }, + { "extraShortEwmaPeriods", k.extraShortEwmaPeriods }, + { "ultraShortEwmaPeriods", k.ultraShortEwmaPeriods }, + { "aprMultiplier", k.aprMultiplier }, + { "sopWidthMultiplier", k.sopWidthMultiplier }, + { "sopSizeMultiplier", k.sopSizeMultiplier }, + { "sopTradesMultiplier", k.sopTradesMultiplier }, + { "cancelOrdersAuto", k.cancelOrdersAuto }, + { "lifetime", k.lifetime }, + { "cleanPongsAuto", k.cleanPongsAuto }, + { "profitHourInterval", k.profitHourInterval }, + { "audio", k.audio }, + { "delayUI", k.delayUI } + }; + }; + static void from_json(const json &j, QuotingParams &k) { + k.from_json(j); + }; + + struct Orders: public System::Orderbook, + public Client::Broadcast { + private_ref: + const KryptoNinja &K; + public: + Orders(const KryptoNinja &bot) + : Orderbook(bot) + , Broadcast(bot) + , K(bot) + {}; + void read_from_gw(const Order &order) { + if (order.orderId.empty()) return; + broadcast(); + K.repaint(true); + }; + mMatter about() const override { + return mMatter::OrderStatusReports; + }; + bool realtime() const override { + return false; + }; + json blob() const override { + return working(false); + }; + }; + static void to_json(json &j, const Orders &k) { + j = k.blob(); + }; + + struct MarketTakers: public Client::Broadcast { + vector trades, + lastTrades; + Amount takersBuySize60s = 0, + takersSellSize60s = 0; + public: + MarketTakers(const KryptoNinja &bot) + : Broadcast(bot) + {}; + void timer_60s() { + takersSellSize60s = takersBuySize60s = 0; + for (const auto &it : trades) + (it.side == Side::Bid + ? takersSellSize60s + : takersBuySize60s + ) += it.quantity; + trades.clear(); + }; + void read_from_gw(const Trade &raw) { + trades.push_back(raw); + lastTrades.push_back(raw); + if (lastTrades.size() > 30) + lastTrades.erase(lastTrades.begin()); + if (broadcast()) + lastTrades.clear(); + }; + mMatter about() const override { + return mMatter::MarketTrade; + }; + json blob() const override { + return lastTrades; + }; + bool realtime() const override { + return false; + }; + bool read_asap() const override { + return false; + }; + }; + + struct FairLevelsPrice: public Client::Broadcast { + private_ref: + const Price &fairValue; + public: + FairLevelsPrice(const KryptoNinja &bot, const Price &f) + : Broadcast(bot) + , fairValue(f) + {}; + json to_json() const { + return { + {"price", fairValue} + }; + }; + mMatter about() const override { + return mMatter::FairValue; + }; + bool realtime() const override { + return false; + }; + bool read_same_blob() const override { + return false; + }; + bool read_asap() const override { + return false; + }; + }; + static void to_json(json &j, const FairLevelsPrice &k) { + j = k.to_json(); + }; + + struct Stdev { + Price fv, + topBid, + topAsk; + }; + static void to_json(json &j, const Stdev &k) { + j = { + { "fv", k.fv }, + {"bid", k.topBid}, + {"ask", k.topAsk} + }; + }; + static void from_json(const json &j, Stdev &k) { + k.fv = j.value("fv", 0.0); + k.topBid = j.value("bid", 0.0); + k.topAsk = j.value("ask", 0.0); + }; + + struct Stdevs: public Sqlite::VectorBackup { + double top = 0, topMean = 0, + fair = 0, fairMean = 0, + bid = 0, bidMean = 0, + ask = 0, askMean = 0; + private_ref: + const Price &fairValue; + const QuotingParams &qp; + public: + Stdevs(const KryptoNinja &bot, const Price &f, const QuotingParams &q) + : VectorBackup(bot) + , fairValue(f) + , qp(q) + {}; + void timer_1s(const Price &topBid, const Price &topAsk) { + push_back({fairValue, topBid, topAsk}); + calc(); + }; + void calc() { + if (size() < 2) return; + fair = calc(&fairMean, "fv"); + bid = calc(&bidMean, "bid"); + ask = calc(&askMean, "ask"); + top = calc(&topMean, "top"); + }; + mMatter about() const override { + return mMatter::STDEVStats; + }; + double limit() const override { + return qp.quotingStdevProtectionPeriods; + }; + Clock lifetime() const override { + return 1e+3 * limit(); + }; + private: + double calc(Price *const mean, const string &type) const { + vector values; + for (const Stdev &it : rows) + if (type == "fv") + values.push_back(it.fv); + else if (type == "bid") + values.push_back(it.topBid); + else if (type == "ask") + values.push_back(it.topAsk); + else if (type == "top") { + values.push_back(it.topBid); + values.push_back(it.topAsk); + } + return calc(mean, qp.quotingStdevProtectionFactor, values); + }; + double calc(Price *const mean, const double &factor, const vector &values) const { + unsigned int n = values.size(); + if (!n) return 0; + double sum = 0; + for (const Price &it : values) sum += it; + *mean = sum / n; + double sq_diff_sum = 0; + for (const Price &it : values) { + Price diff = it - *mean; + sq_diff_sum += diff * diff; + } + double variance = sq_diff_sum / n; + return sqrt(variance) * factor; + }; + string explainOK() const override { + return "loaded % STDEV Periods"; + }; + }; + static void to_json(json &j, const Stdevs &k) { + j = { + { "fv", k.fair }, + { "fvMean", k.fairMean}, + { "tops", k.top }, + {"topsMean", k.topMean }, + { "bid", k.bid }, + { "bidMean", k.bidMean }, + { "ask", k.ask }, + { "askMean", k.askMean } + }; + }; + + struct mFairHistory: public Sqlite::VectorBackup { + public: + mFairHistory(const KryptoNinja &bot) + : VectorBackup(bot) + {}; + mMatter about() const override { + return mMatter::MarketDataLongTerm; + }; + double limit() const override { + return 5760; + }; + Clock lifetime() const override { + return 60e+3 * limit(); + }; + private: + string explainOK() const override { + return "loaded % historical Fair Values"; + }; + }; + + struct Ewma: public Sqlite::StructBackup, + public Client::Clicked { + mFairHistory fairValue96h; + Price mgEwmaVL = 0, + mgEwmaL = 0, + mgEwmaM = 0, + mgEwmaS = 0, + mgEwmaXS = 0, + mgEwmaU = 0, + mgEwmaP = 0, + mgEwmaW = 0; + double mgEwmaTrendDiff = 0, + targetPositionAutoPercentage = 0; + private_ref: + const KryptoNinja &K; + const Price &fairValue; + const QuotingParams &qp; + public: + Ewma(const KryptoNinja &bot, const Price &f, const QuotingParams &q) + : StructBackup(bot) + , Clicked(bot, { + {&q, [&]() { calcFromHistory(); }} + }) + , fairValue96h(bot) + , K(bot) + , fairValue(f) + , qp(q) + {}; + void timer_60s(const Price &averageWidth) { + prepareHistory(); + calcProtections(averageWidth); + calcPositions(); + calcTargetPositionAutoPercentage(); + backup(); + }; + mMatter about() const override { + return mMatter::EWMAStats; + }; + Clock lifetime() const override { + return 60e+3 * max(qp.veryLongEwmaPeriods, + max(qp.longEwmaPeriods, + max(qp.mediumEwmaPeriods, + max(qp.shortEwmaPeriods, + max(qp.extraShortEwmaPeriods, + qp.ultraShortEwmaPeriods + ))))); + }; + private: + void calcFromHistory() { + if ((qp._diffEwma >> 0) & 1) calcFromHistory(&mgEwmaVL, qp.veryLongEwmaPeriods, "VeryLong"); + if ((qp._diffEwma >> 1) & 1) calcFromHistory(&mgEwmaL, qp.longEwmaPeriods, "Long"); + if ((qp._diffEwma >> 2) & 1) calcFromHistory(&mgEwmaM, qp.mediumEwmaPeriods, "Medium"); + if ((qp._diffEwma >> 3) & 1) calcFromHistory(&mgEwmaS, qp.shortEwmaPeriods, "Short"); + if ((qp._diffEwma >> 4) & 1) calcFromHistory(&mgEwmaXS, qp.extraShortEwmaPeriods, "ExtraShort"); + if ((qp._diffEwma >> 5) & 1) calcFromHistory(&mgEwmaU, qp.ultraShortEwmaPeriods, "UltraShort"); + }; + void calc(Price *const mean, const unsigned int &periods, const Price &value) { + if (*mean) { + double alpha = 2.0 / (periods + 1); + *mean = alpha * value + (1 - alpha) * *mean; + } else *mean = value; + }; + void prepareHistory() { + fairValue96h.push_back(fairValue); + }; + void calcFromHistory(Price *const mean, const unsigned int &periods, const string &name) { + unsigned int n = fairValue96h.size(); + if (!n--) return; + unsigned int x = 0; + *mean = fairValue96h.at(x); + while (n--) calc(mean, periods, fairValue96h.at(++x)); + K.log("MG", "reloaded " + to_string(*mean) + " EWMA " + name); + }; + void calcPositions() { + calc(&mgEwmaVL, qp.veryLongEwmaPeriods, fairValue); + calc(&mgEwmaL, qp.longEwmaPeriods, fairValue); + calc(&mgEwmaM, qp.mediumEwmaPeriods, fairValue); + calc(&mgEwmaS, qp.shortEwmaPeriods, fairValue); + calc(&mgEwmaXS, qp.extraShortEwmaPeriods, fairValue); + calc(&mgEwmaU, qp.ultraShortEwmaPeriods, fairValue); + if (mgEwmaXS and mgEwmaU) + mgEwmaTrendDiff = ((mgEwmaU * 1e+2) / mgEwmaXS) - 1e+2; + }; + void calcProtections(const Price &averageWidth) { + calc(&mgEwmaP, qp.protectionEwmaPeriods, fairValue); + calc(&mgEwmaW, qp.protectionEwmaPeriods, averageWidth); + }; + void calcTargetPositionAutoPercentage() { + unsigned int max3size = fmin(3, fairValue96h.size()); + Price SMA3 = accumulate(fairValue96h.end() - max3size, fairValue96h.end(), Price(), + [](Price sma3, const Price &it) { return sma3 + it; } + ) / max3size; + double targetPosition = 0; + if (qp.autoPositionMode == AutoPositionMode::EWMA_LMS) { + double newTrend = ((SMA3 * 1e+2 / mgEwmaL) - 1e+2); + double newEwmacrossing = ((mgEwmaS * 1e+2 / mgEwmaM) - 1e+2); + targetPosition = ((newTrend + newEwmacrossing) / 2) * (1 / qp.ewmaSensiblityPercentage); + } else if (qp.autoPositionMode == AutoPositionMode::EWMA_LS) + targetPosition = ((mgEwmaS * 1e+2 / mgEwmaL) - 1e+2) * (1 / qp.ewmaSensiblityPercentage); + else if (qp.autoPositionMode == AutoPositionMode::EWMA_4) { + if (mgEwmaL < mgEwmaVL) targetPosition = -1; + else targetPosition = ((mgEwmaS * 1e+2 / mgEwmaM) - 1e+2) * (1 / qp.ewmaSensiblityPercentage); + } + targetPositionAutoPercentage = ((1 + max(-1.0, min(1.0, targetPosition))) / 2) * 1e+2; + }; + string explain() const override { + return "EWMA Values"; + }; + string explainKO() const override { + return "consider to warm up some %"; + }; + }; + static void to_json(json &j, const Ewma &k) { + j = { + { "ewmaVeryLong", k.mgEwmaVL }, + { "ewmaLong", k.mgEwmaL }, + { "ewmaMedium", k.mgEwmaM }, + { "ewmaShort", k.mgEwmaS }, + {"ewmaExtraShort", k.mgEwmaXS }, + {"ewmaUltraShort", k.mgEwmaU }, + { "ewmaQuote", k.mgEwmaP }, + { "ewmaWidth", k.mgEwmaW }, + { "ewmaTrendDiff", k.mgEwmaTrendDiff} + }; + }; + static void from_json(const json &j, Ewma &k) { + k.mgEwmaVL = j.value("ewmaVeryLong", 0.0); + k.mgEwmaL = j.value("ewmaLong", 0.0); + k.mgEwmaM = j.value("ewmaMedium", 0.0); + k.mgEwmaS = j.value("ewmaShort", 0.0); + k.mgEwmaXS = j.value("ewmaExtraShort", 0.0); + k.mgEwmaU = j.value("ewmaUltraShort", 0.0); + }; + + struct MarketStats: public Client::Broadcast { + Ewma ewma; + Stdevs stdev; + MarketTakers takerTrades; + public: + MarketStats(const KryptoNinja &bot, const Price &f, const QuotingParams &q) + : Broadcast(bot) + , ewma(bot, f, q) + , stdev(bot, f, q) + , takerTrades(bot) + {}; + mMatter about() const override { + return mMatter::MarketChart; + }; + bool realtime() const override { + return false; + }; + }; + static void to_json(json &j, const MarketStats &k) { + j = { + { "ewma", k.ewma }, + { "stdevWidth", k.stdev }, + { "tradesBuySize", k.takerTrades.takersBuySize60s }, + {"tradesSellSize", k.takerTrades.takersSellSize60s} + }; + }; + + struct LevelsDiff: public Levels, + public Client::Broadcast { + bool patched = false; + private_ref: + const Levels &unfiltered; + const QuotingParams &qp; + public: + LevelsDiff(const KryptoNinja &bot, const Levels &u, const QuotingParams &q) + : Broadcast(bot) + , unfiltered(u) + , qp(q) + {}; + bool empty() const { + return patched + ? bids.empty() and asks.empty() + : bids.empty() or asks.empty(); + }; + void send_patch() { + const bool full = empty(); + if (full) unfilter(); + else { + if (ratelimit()) return; + diff(); + } + if (!empty() and read) read(); + if (!full) unfilter(); + }; + mMatter about() const override { + return mMatter::MarketData; + }; + json hello() override { + unfilter(); + return Broadcast::hello(); + }; + private: + bool ratelimit() { + return unfiltered.bids.empty() or unfiltered.asks.empty() or empty() + or !read_soon(qp.delayUI * 1e+3); + }; + void unfilter() { + bids = unfiltered.bids; + asks = unfiltered.asks; + patched = false; + }; + void diff() { + bids = diff(bids, unfiltered.bids); + asks = diff(asks, unfiltered.asks); + patched = true; + }; + vector diff(const vector &from, vector to) const { + vector patch; + for (const Level &it : from) { + auto it_ = find_if( + to.begin(), to.end(), + [&](const Level &_it) { + return it.price == _it.price; + } + ); + Amount size = 0; + if (it_ != to.end()) { + size = it_->size; + to.erase(it_); + } + if (size != it.size) + patch.push_back({it.price, size}); + } + if (!to.empty()) + patch.insert(patch.end(), to.begin(), to.end()); + return patch; + }; + }; + static void to_json(json &j, const LevelsDiff &k) { + to_json(j, (Levels)k); + if (k.patched) + j["diff"] = true; + }; + struct MarketLevels: public Levels { + unsigned int averageCount = 0; + Price averageWidth = 0, + fairValue = 0; + Levels unfiltered; + LevelsDiff diff; + MarketStats stats; + FairLevelsPrice fairPrice; + private: + unordered_map filterBidOrders, + filterAskOrders; + private_ref: + const KryptoNinja &K; + const QuotingParams &qp; + const Orders &orders; + public: + MarketLevels(const KryptoNinja &bot, const QuotingParams &q, const Orders &o) + : diff(bot, unfiltered, q) + , stats(bot, fairValue, q) + , fairPrice(bot, fairValue) + , K(bot) + , qp(q) + , orders(o) + {}; + void timer_1s() { + stats.stdev.timer_1s(bids.cbegin()->price, asks.cbegin()->price); + }; + void timer_60s() { + stats.takerTrades.timer_60s(); + stats.ewma.timer_60s(resetAverageWidth()); + stats.broadcast(); + }; + Price calcQuotesWidth(bool *const superSpread) const { + const Price widthPing = fmax( + qp.widthPercentage + ? qp.widthPingPercentage * fairValue / 100 + : qp.widthPing, + qp.protectionEwmaWidthPing and stats.ewma.mgEwmaW + ? stats.ewma.mgEwmaW + : 0 + ); + *superSpread = asks.cbegin()->price - bids.cbegin()->price > widthPing * qp.sopWidthMultiplier; + return widthPing; + }; + void clear() { + bids.clear(); + asks.clear(); + }; + bool ready() { + filter(); + if (!fairValue and Tspent > 21e+3) + K.warn("QE", "Unable to calculate quote, missing market data", 10e+3); + return fairValue; + }; + void read_from_gw(const Levels &raw) { + unfiltered.bids = raw.bids; + unfiltered.asks = raw.asks; + filter(); + if (fairPrice.broadcast()) K.repaint(); + diff.send_patch(); + }; + private: + void filter() { + orders.resetFilters(&filterBidOrders, &filterAskOrders); + bids = filter(unfiltered.bids, &filterBidOrders); + asks = filter(unfiltered.asks, &filterAskOrders); + calcFairValue(); + calcAverageWidth(); + }; + void calcAverageWidth() { + if (bids.empty() or asks.empty()) return; + averageWidth = ( + (averageWidth * averageCount) + + asks.cbegin()->price + - bids.cbegin()->price + ); + averageWidth /= ++averageCount; + }; + Price resetAverageWidth() { + averageCount = 0; + return averageWidth; + }; + void calcFairValue() { + if (bids.empty() or asks.empty()) + fairValue = 0; + else if (qp.fvModel == FairValueModel::BBO) + fairValue = (asks.cbegin()->price + + bids.cbegin()->price) / 2; + else if (qp.fvModel == FairValueModel::wBBO) + fairValue = ( + bids.cbegin()->price * bids.cbegin()->size + + asks.cbegin()->price * asks.cbegin()->size + ) / (asks.cbegin()->size + + bids.cbegin()->size + ); + else + fairValue = ( + bids.cbegin()->price * asks.cbegin()->size + + asks.cbegin()->price * bids.cbegin()->size + ) / (asks.cbegin()->size + + bids.cbegin()->size + ); + if (fairValue) + fairValue = K.gateway->decimal.price.round(fairValue); + }; + vector filter(vector levels, unordered_map *const filterOrders) { + if (!filterOrders->empty()) + for (auto it = levels.begin(); it != levels.end();) { + for (auto it_ = filterOrders->begin(); it_ != filterOrders->end();) + if (abs(it->price - it_->first) < K.gateway->tickPrice) { + it->size -= it_->second; + filterOrders->erase(it_); + break; + } else ++it_; + if (it->size < K.gateway->minSize) it = levels.erase(it); + else ++it; + if (filterOrders->empty()) break; + } + return levels; + }; + }; + + struct Profit { + Amount baseValue, + quoteValue; + Clock time; + }; + static void to_json(json &j, const Profit &k) { + j = { + { "baseValue", k.baseValue }, + {"quoteValue", k.quoteValue}, + { "time", k.time } + }; + }; + static void from_json(const json &j, Profit &k) { + k.baseValue = j.value("baseValue", 0.0); + k.quoteValue = j.value("quoteValue", 0.0); + k.time = j.value("time", (Clock)0); + }; + struct Profits: public Sqlite::VectorBackup { + private_ref: + const KryptoNinja &K; + const QuotingParams &qp; + public: + Profits(const KryptoNinja &bot, const QuotingParams &q) + : VectorBackup(bot) + , K(bot) + , qp(q) + {}; + bool ratelimit() const { + return !empty() and crbegin()->time + 21e+3 > Tstamp; + }; + double calcBaseDiff() const { + return calcDiffPercent( + cbegin()->baseValue, + crbegin()->baseValue + ); + }; + double calcQuoteDiff() const { + return calcDiffPercent( + cbegin()->quoteValue, + crbegin()->quoteValue + ); + }; + double calcDiffPercent(Amount older, Amount newer) const { + return K.gateway->decimal.percent.round(((newer - older) / (older?:1)) * 1e+2); + }; + mMatter about() const override { + return mMatter::Profit; + }; + void erase() override { + const Clock now = Tstamp; + for (auto it = begin(); it != end();) + if (it->time + lifetime() > now) ++it; + else it = rows.erase(it); + }; + double limit() const override { + return qp.profitHourInterval; + }; + Clock lifetime() const override { + return 3600e+3 * limit(); + }; + private: + string explainOK() const override { + return "loaded % historical Profits"; + }; + }; + + struct OrderFilled: public Trade { + string tradeId; + Amount value, + feeCharged, + Kqty, + Kvalue, + delta; + Price Kprice; + Clock Ktime; + bool isPong, + loadedFromDB; + }; + static void to_json(json &j, const OrderFilled &k) { + j = { + { "tradeId", k.tradeId }, + { "time", k.time }, + { "price", k.price }, + { "quantity", k.quantity }, + { "side", k.side }, + { "value", k.value }, + { "Ktime", k.Ktime }, + { "Kqty", k.Kqty }, + { "Kprice", k.Kprice }, + { "Kvalue", k.Kvalue }, + { "delta", k.delta }, + { "feeCharged", k.feeCharged }, + { "isPong", k.isPong }, + {"loadedFromDB", k.loadedFromDB} + }; + }; + static void from_json(const json &j, OrderFilled &k) { + k.tradeId = j.value("tradeId", ""); + k.price = j.value("price", 0.0); + k.quantity = j.value("quantity", 0.0); + k.side = j.value("side", (Side)0); + k.time = j.value("time", (Clock)0); + k.value = j.value("value", 0.0); + k.Ktime = j.value("Ktime", (Clock)0); + k.Kqty = j.value("Kqty", 0.0); + k.Kprice = j.value("Kprice", 0.0); + k.Kvalue = j.value("Kvalue", 0.0); + k.delta = j.value("delta", + j.value("Kdiff", 0.0) + ); + k.feeCharged = j.value("feeCharged", 0.0); + k.isPong = j.value("isPong", false); + k.loadedFromDB = true; + }; + + struct ButtonSubmitNewOrder: public Client::Clickable { + private_ref: + const KryptoNinja &K; + public: + ButtonSubmitNewOrder(const KryptoNinja &bot) + : Clickable(bot) + , K(bot) + {}; + void click(const json &j) override { + if (j.is_object() + and !j.value("symbol", "").empty() + and j.value("price", 0.0) + and j.value("quantity", 0.0) + ) { + json order = j; + order["manual"] = true; + order["orderId"] = K.gateway->randId(); + K.clicked(this, order); + } + }; + mMatter about() const override { + return mMatter::SubmitNewOrder; + }; + }; + struct ButtonCancelOrder: public Client::Clickable { + private_ref: + const KryptoNinja &K; + public: + ButtonCancelOrder(const KryptoNinja &bot) + : Clickable(bot) + , K(bot) + {}; + void click(const json &j) override { + if (j.is_object() and !j.value("orderId", "").empty()) + K.clicked(this, j.at("orderId").get()); + }; + mMatter about() const override { + return mMatter::CancelOrder; + }; + }; + struct ButtonCancelAllOrders: public Client::Clickable { + private_ref: + const KryptoNinja &K; + public: + ButtonCancelAllOrders(const KryptoNinja &bot) + : Clickable(bot) + , K(bot) + {}; + void click(const json&) override { + K.clicked(this); + }; + mMatter about() const override { + return mMatter::CancelAllOrders; + }; + }; + struct ButtonCleanAllClosedTrades: public Client::Clickable { + private_ref: + const KryptoNinja &K; + public: + ButtonCleanAllClosedTrades(const KryptoNinja &bot) + : Clickable(bot) + , K(bot) + {}; + void click(const json&) override { + K.clicked(this); + }; + mMatter about() const override { + return mMatter::CleanAllClosedTrades; + }; + }; + struct ButtonCleanAllTrades: public Client::Clickable { + private_ref: + const KryptoNinja &K; + public: + ButtonCleanAllTrades(const KryptoNinja &bot) + : Clickable(bot) + , K(bot) + {}; + void click(const json&) override { + K.clicked(this); + }; + mMatter about() const override { + return mMatter::CleanAllTrades; + }; + }; + struct ButtonCleanTrade: public Client::Clickable { + private_ref: + const KryptoNinja &K; + public: + ButtonCleanTrade(const KryptoNinja &bot) + : Clickable(bot) + , K(bot) + {}; + void click(const json &j) override { + if (j.is_object() and !j.value("tradeId", "").empty()) + K.clicked(this, j.at("tradeId").get()); + }; + mMatter about() const override { + return mMatter::CleanTrade; + }; + }; + struct Notepad: public Client::Broadcast, + public Client::Clickable { + public: + string content; + public: + Notepad(const KryptoNinja &bot) + : Broadcast(bot) + , Clickable(bot) + {}; + void click(const json &j) override { + if (j.is_array() and j.size() and j.at(0).is_string()) + content = j.at(0); + }; + mMatter about() const override { + return mMatter::Notepad; + }; + }; + static void to_json(json &j, const Notepad &k) { + j = k.content; + }; + + struct Buttons { + Notepad notepad; + ButtonSubmitNewOrder submit; + ButtonCancelOrder cancel; + ButtonCancelAllOrders cancelAll; + ButtonCleanAllClosedTrades cleanTradesClosed; + ButtonCleanAllTrades cleanTrades; + ButtonCleanTrade cleanTrade; + Buttons(const KryptoNinja &bot) + : notepad(bot) + , submit(bot) + , cancel(bot) + , cancelAll(bot) + , cleanTradesClosed(bot) + , cleanTrades(bot) + , cleanTrade(bot) + {}; + }; + + struct TradesHistory: public Sqlite::VectorBackup, + public Client::Broadcast, + public Client::Clicked { + private_ref: + const KryptoNinja &K; + const QuotingParams &qp; + public: + TradesHistory(const KryptoNinja &bot, const QuotingParams &q, const Buttons &b) + : VectorBackup(bot) + , Broadcast(bot) + , Clicked(bot, { + {&b.cleanTrade, [&](const json &j) { clearOne(j); }}, + {&b.cleanTrades, [&]() { clearAll(); }}, + {&b.cleanTradesClosed, [&]() { clearClosed(); }} + }) + , K(bot) + , qp(q) + {}; + void insert(const Order &last) { + const Amount fee = 0; + const Clock time = Tstamp; + OrderFilled filled = { + last.side, + last.price, + last.qtyFilled, + time, + to_string(time), + abs(last.price * last.qtyFilled), + fee, + 0, 0, 0, 0, 0, + last.isPong, + false + }; + const bool is_bid = last.side == Side::Bid; + K.log("GW " + K.gateway->exchange, + string(is_bid + ? ANSI_HIGH_CYAN + (last.isPong?"PONG":"PING") + " TRADE BUY " + : ANSI_PUKE_MAGENTA + (last.isPong?"PONG":"PING") + " TRADE SELL " + ) + + K.gateway->decimal.amount.str(filled.quantity) + ' ' + K.gateway->base + " at price " + + K.gateway->decimal.price.str(filled.price) + ' ' + K.gateway->quote + " (value " + + K.gateway->decimal.price.str(filled.value) + ' ' + K.gateway->quote + ")" + ); + if (qp.safety == QuotingSafety::Off + or qp.safety == QuotingSafety::PingPong + or qp.safety == QuotingSafety::PingPoing + ) broadcast_push_back(filled); + else { + Price widthPong = qp.widthPercentage + ? qp.widthPongPercentage * filled.price / 100 + : qp.widthPong; + map matches; + for (OrderFilled &it : rows) + if (it.quantity - it.Kqty > 0 and it.side != filled.side) { + const Price combinedFee = K.gateway->makeFee * (it.price + filled.price); + if (is_bid + ? (it.price > filled.price + widthPong + combinedFee) + : (it.price < filled.price - widthPong - combinedFee) + ) matches[it.price] = it.tradeId; + } + matchPong( + matches, + filled, + (qp.pongAt == PongAt::LongPingFair or qp.pongAt == PongAt::LongPingAggressive) + ? !is_bid + : is_bid + ); + } + if (qp.cleanPongsAuto) + clearPongsAuto(); + }; + mMatter about() const override { + return mMatter::Trades; + }; + void erase() override { + if (crbegin()->Kqty < 0) rows.pop_back(); + }; + json blob() const override { + if (crbegin()->Kqty == -1) return nullptr; + else return VectorBackup::blob(); + }; + string increment() const override { + return crbegin()->tradeId; + }; + json hello() override { + for (OrderFilled &it : rows) + it.loadedFromDB = true; + return rows; + }; + private: + void clearAll() { + clear_if([](iterator) { + return true; + }); + }; + void clearOne(const string &tradeId) { + clear_if([&tradeId](iterator it) { + return it->tradeId == tradeId; + }, true); + }; + void clearClosed() { + clear_if([](iterator it) { + return it->Kqty >= it->quantity; + }); + }; + void clearPongsAuto() { + const Clock expire = Tstamp - (abs(qp.cleanPongsAuto) * 86400e3); + const bool forcedClean = qp.cleanPongsAuto < 0; + clear_if([&expire, &forcedClean](iterator it) { + return (it->Ktime?:it->time) < expire and ( + forcedClean + or it->Kqty >= it->quantity + ); + }); + }; + void clear_if(const function &fn, const bool &onlyOne = false) { + for (auto it = begin(); it != end();) + if (fn(it)) { + it->Kqty = -1; + it = send_push_erase(it); + if (onlyOne) break; + } else ++it; + }; + void matchPong(const map &matches, OrderFilled pong, const bool &reverse) { + if (reverse) for (auto it = matches.crbegin(); it != matches.crend(); ++it) { + if (!matchPong(it->second, &pong)) break; + } else for (const auto &it : matches) + if (!matchPong(it.second, &pong)) break; + if (pong.quantity > 0) { + bool eq = false; + for (auto it = begin(); it != end(); ++it) { + if (it->price!=pong.price or it->side!=pong.side or it->quantity<=it->Kqty) continue; + eq = true; + it->time = pong.time; + it->quantity = it->quantity + pong.quantity; + it->value = it->value + pong.value; + it->isPong = false; + it->loadedFromDB = false; + it = send_push_erase(it); + break; + } + if (!eq) broadcast_push_back(pong); + } + }; + bool matchPong(const string &match, OrderFilled *const pong) { + for (auto it = begin(); it != end(); ++it) { + if (it->tradeId != match) continue; + Amount Kqty = fmin(pong->quantity, it->quantity - it->Kqty); + it->Ktime = pong->time; + it->Kprice = ((Kqty*pong->price) + (it->Kqty*it->Kprice)) / (it->Kqty+Kqty); + it->Kqty = it->Kqty + Kqty; + it->Kvalue = abs(it->Kqty*it->Kprice); + pong->quantity = pong->quantity - Kqty; + pong->value = abs(pong->price*pong->quantity); + if (it->quantity <= it->Kqty) + it->delta = ((it->quantity * it->price) - (it->Kqty * it->Kprice)) + * (it->side == Side::Ask ? 1 : -1); + it->isPong = true; + it->loadedFromDB = false; + it = send_push_erase(it); + break; + } + return pong->quantity > 0; + }; + void broadcast_push_back(const OrderFilled &row) { + rows.push_back(row); + backup(); + if (crbegin()->Kqty < 0) rbegin()->Kqty = -2; + broadcast(); + }; + iterator send_push_erase(iterator it) { + OrderFilled row = *it; + it = rows.erase(it); + broadcast_push_back(row); + erase(); + return it; + }; + string explainOK() const override { + return "loaded % historical Trades"; + }; + }; + + struct RecentTrade { + Price price = 0; + Amount quantity = 0; + Clock time = 0; + RecentTrade(const Price &p, const Amount &q) + : price(p) + , quantity(q) + , time(Tstamp) + {}; + }; + struct RecentTrades { + private_ref: + const QuotingParams &qp; + public: + RecentTrades(const QuotingParams &q) + : qp(q) + {}; + multimap buys, + sells; + Amount sumBuys = 0, + sumSells = 0; + Price lastBuyPrice = 0, + lastSellPrice = 0; + void insert(const Order &last) { + (last.side == Side::Bid + ? lastBuyPrice + : lastSellPrice + ) = last.price; + (last.side == Side::Bid + ? buys + : sells + ).insert(pair( + last.price, + RecentTrade(last.price, last.qtyFilled) + )); + }; + void expire() { + if (!buys.empty()) expire(&buys); + if (!sells.empty()) expire(&sells); + recent(); + sumBuys = sum(&buys); + sumSells = sum(&sells); + }; + private: + Amount sum(multimap *const k) const { + Amount sum = 0; + for (const auto &it : *k) + sum += it.second.quantity; + return sum; + }; + void expire(multimap *const k) { + const Clock now = Tstamp; + for (auto it = k->begin(); it != k->end();) + if (it->second.time + qp.tradeRateSeconds * 1e+3 > now) ++it; + else it = k->erase(it); + }; + void recent() { + while (!(buys.empty() or sells.empty())) { + RecentTrade &buy = buys.rbegin()->second; + RecentTrade &sell = sells.begin()->second; + if (sell.price < buy.price) break; + const Amount buyQty = buy.quantity; + buy.quantity -= sell.quantity; + sell.quantity -= buyQty; + if (buy.quantity <= 0) + buys.erase(buys.rbegin()->first); + if (sell.quantity <= 0) + sells.erase(sells.begin()->first); + } + }; + }; + + struct Safety: public Client::Broadcast { + double buy = 0, + sell = 0, + combined = 0; + Price buyPing = 0, + sellPing = 0; + Amount buySize = 0, + sellSize = 0; + TradesHistory trades; + RecentTrades recentTrades; + private_ref: + const QuotingParams &qp; + const Wallet &base; + const Price &fairValue; + const Amount &targetBasePosition; + const Amount &positionDivergence; + public: + Safety(const KryptoNinja &bot, const QuotingParams &q, const Wallet &w, const Buttons &b, const Price &f, const Amount &t, const Amount &p) + : Broadcast(bot) + , trades(bot, q, b) + , recentTrades(q) + , qp(q) + , base(w) + , fairValue(f) + , targetBasePosition(t) + , positionDivergence(p) + {}; + void timer_1s() { + calc(); + }; + void insertTrade(const Order &last) { + if (!last.isPong) + recentTrades.insert(last); + trades.insert(last); + calc(); + }; + void calc() { + if (!base.value or !fairValue) return; + calcSizes(); + calcPrices(); + recentTrades.expire(); + if (empty()) return; + buy = recentTrades.sumBuys / buySize; + sell = recentTrades.sumSells / sellSize; + combined = (recentTrades.sumBuys + recentTrades.sumSells) / (buySize + sellSize); + broadcast(); + }; + bool empty() const { + return !base.value or !buySize or !sellSize; + }; + mMatter about() const override { + return mMatter::TradeSafetyValue; + }; + bool read_same_blob() const override { + return false; + }; + private: + void calcSizes() { + if (qp.percentageValues) { + sellSize = qp.sellSizePercentage / 1e+2; + buySize = qp.buySizePercentage / 1e+2; + const Amount pdivMin = fmax(0, targetBasePosition - positionDivergence), + pdivMax = fmin(base.value, targetBasePosition + positionDivergence); + if (qp.orderPctTotal == OrderPctTotal::Side) { + sellSize *= base.total; + buySize *= base.value - base.total; + } else { + if (qp.orderPctTotal == OrderPctTotal::TBPSide) { + sellSize *= pow((base.total - pdivMin) / (base.value - pdivMin), qp.tradeSizeTBPExp); + buySize *= pow((pdivMax - base.total) / pdivMax, qp.tradeSizeTBPExp); + } + sellSize *= base.value; + buySize *= base.value; + if (qp.orderPctTotal == OrderPctTotal::TBPSide + or qp.orderPctTotal == OrderPctTotal::TBPValue + ) { + const double exp = qp.orderPctTotal == OrderPctTotal::TBPValue + ? 1.0 + : qp.tradeSizeTBPExp; + if (targetBasePosition * 2 < base.value) + buySize *= pow( + (targetBasePosition - pdivMin) * pdivMax / ((base.value - pdivMin) * (pdivMax - targetBasePosition)), + exp + ); + else + sellSize *= pow( + (base.value - pdivMin) * (pdivMax - targetBasePosition) / ((targetBasePosition - pdivMin) * pdivMax), + exp + ); + } + } + sellSize = fmax(0.0, sellSize); + buySize = fmax(0.0, buySize); + } else { + sellSize = qp.sellSize; + buySize = qp.buySize; + } + if (qp.aggressivePositionRebalancing == APR::Off) return; + if (qp.buySizeMax) + buySize = fmax(buySize, targetBasePosition - base.total); + if (qp.sellSizeMax) + sellSize = fmax(sellSize, base.total - targetBasePosition); + }; + void calcPrices() { + if (qp.safety == QuotingSafety::PingPong) { + buyPing = recentTrades.lastBuyPrice; + sellPing = recentTrades.lastSellPrice; + } else { + buyPing = sellPing = 0; + if (qp.safety == QuotingSafety::Off) return; + Price widthPong = qp.widthPercentage + ? qp.widthPongPercentage * fairValue / 100 + : qp.widthPong; + if (qp.safety == QuotingSafety::PingPoing) { + if (recentTrades.lastBuyPrice and fairValue > recentTrades.lastBuyPrice - widthPong) + buyPing = recentTrades.lastBuyPrice; + if (recentTrades.lastSellPrice and fairValue < recentTrades.lastSellPrice + widthPong) + sellPing = recentTrades.lastSellPrice; + } else { + map tradesBuy; + map tradesSell; + for (const OrderFilled &it: trades) + (it.side == Side::Bid ? tradesBuy : tradesSell)[it.price] = it; + Amount buyQty = 0, + sellQty = 0; + if (qp.pongAt == PongAt::ShortPingFair or qp.pongAt == PongAt::ShortPingAggressive) { + matchBestPing(&tradesBuy, &buyPing, &buyQty, sellSize, widthPong, true); + matchBestPing(&tradesSell, &sellPing, &sellQty, buySize, widthPong); + if (!buyQty) matchFirstPing(&tradesBuy, &buyPing, &buyQty, sellSize, widthPong*-1, true); + if (!sellQty) matchFirstPing(&tradesSell, &sellPing, &sellQty, buySize, widthPong*-1); + } else if (qp.pongAt == PongAt::LongPingFair or qp.pongAt == PongAt::LongPingAggressive) { + matchLastPing(&tradesBuy, &buyPing, &buyQty, sellSize, widthPong); + matchLastPing(&tradesSell, &sellPing, &sellQty, buySize, widthPong, true); + } else if (qp.pongAt == PongAt::AveragePingFair or qp.pongAt == PongAt::AveragePingAggressive) { + matchAllPing(&tradesBuy, &buyPing, &buyQty, sellSize, widthPong); + matchAllPing(&tradesSell, &sellPing, &sellQty, buySize, widthPong); + } + if (buyQty) buyPing /= buyQty; + if (sellQty) sellPing /= sellQty; + } + } + }; + void matchFirstPing(map *tradesSide, Price *ping, Amount *qty, Amount qtyMax, Price width, bool reverse = false) { + matchPing(true, true, tradesSide, ping, qty, qtyMax, width, reverse); + }; + void matchBestPing(map *tradesSide, Price *ping, Amount *qty, Amount qtyMax, Price width, bool reverse = false) { + matchPing(true, false, tradesSide, ping, qty, qtyMax, width, reverse); + }; + void matchLastPing(map *tradesSide, Price *ping, Amount *qty, Amount qtyMax, Price width, bool reverse = false) { + matchPing(false, true, tradesSide, ping, qty, qtyMax, width, reverse); + }; + void matchAllPing(map *tradesSide, Price *ping, Amount *qty, Amount qtyMax, Price width) { + matchPing(false, false, tradesSide, ping, qty, qtyMax, width); + }; + void matchPing(bool _near, bool _far, map *tradesSide, Price *ping, Amount *qty, Amount qtyMax, Price width, bool reverse = false) { + int dir = width > 0 ? 1 : -1; + if (reverse) for (auto it = tradesSide->crbegin(); it != tradesSide->crend(); ++it) { + if (matchPing(_near, _far, ping, qty, qtyMax, width, dir * fairValue, dir * it->second.price, it->second.quantity, it->second.price, it->second.Kqty, reverse)) + break; + } else for (const auto &it : *tradesSide) + if (matchPing(_near, _far, ping, qty, qtyMax, width, dir * fairValue, dir * it.second.price, it.second.quantity, it.second.price, it.second.Kqty, reverse)) + break; + }; + bool matchPing(bool _near, bool _far, Price *ping, Amount *qty, Amount qtyMax, Price width, Price fv, Price price, Amount qtyTrade, Price priceTrade, Amount KqtyTrade, bool reverse) { + if (reverse) { fv *= -1; price *= -1; width *= -1; } + if (((!_near and !_far) or *qty < qtyMax) + and (_far ? fv > price : true) + and (_near ? (reverse ? fv - width : fv + width) < price : true) + and KqtyTrade < qtyTrade + ) { + Amount qty_ = qtyTrade; + if (_near or _far) + qty_ = fmin(qtyMax - *qty, qty_); + *ping += priceTrade * qty_; + *qty += qty_; + } + return *qty >= qtyMax and (_near or _far); + }; + }; + static void to_json(json &j, const Safety &k) { + j = { + { "buy", k.buy }, + { "sell", k.sell }, + {"combined", k.combined}, + { "buyPing", k.buyPing }, + {"sellPing", k.sellPing} + }; + }; + + struct Target: public Sqlite::StructBackup, + public Client::Broadcast { + Amount targetBasePosition = 0, + positionDivergence = 0; + private_ref: + const KryptoNinja &K; + const QuotingParams &qp; + const double &targetPositionAutoPercentage; + const Amount &baseValue; + public: + Target(const KryptoNinja &bot, const QuotingParams &q, const double &t, const Amount &v) + : StructBackup(bot) + , Broadcast(bot) + , K(bot) + , qp(q) + , targetPositionAutoPercentage(t) + , baseValue(v) + {}; + void calcTargetBasePos() { + if (!baseValue) { + if (Tspent > 21e+3) + K.warn("QE", "Unable to calculate TBP, missing wallet data", 3e+3); + return; + } + targetBasePosition = K.gateway->decimal.funds.round( + qp.autoPositionMode == AutoPositionMode::Manual + ? (qp.percentageValues + ? qp.targetBasePositionPercentage * baseValue / 1e+2 + : qp.targetBasePosition) + : (qp.percentageValues + ? ( qp.targetBasePositionPercentageMin + ( qp.targetBasePositionPercentageMax - qp.targetBasePositionPercentageMin ) * targetPositionAutoPercentage / 1e+2 ) / 1e+2 * baseValue + : ( qp.targetBasePositionMin + ( qp.targetBasePositionMax - qp.targetBasePositionMin ) * targetPositionAutoPercentage / 1e+2 )) + ); + calcPDiv(); + if (broadcast()) { + backup(); + if (K.arg("heartbeat")) report(); + } + }; + bool realtime() const override { + return false; + }; + mMatter about() const override { + return mMatter::TargetBasePosition; + }; + bool read_same_blob() const override { + return false; + }; + private: + void calcPDiv() { + Amount pDiv = qp.percentageValues + ? qp.positionDivergencePercentage * baseValue / 1e+2 + : qp.positionDivergence; + if (qp.autoPositionMode == AutoPositionMode::Manual or PDivMode::Manual == qp.positionDivergenceMode) + positionDivergence = pDiv; + else { + Amount pDivMin = qp.percentageValues + ? qp.positionDivergencePercentageMin * baseValue / 1e+2 + : qp.positionDivergenceMin; + double divCenter = 1 - abs((targetBasePosition / baseValue * 2) - 1); + if (PDivMode::Linear == qp.positionDivergenceMode) positionDivergence = pDivMin + (divCenter * (pDiv - pDivMin)); + else if (PDivMode::Sine == qp.positionDivergenceMode) positionDivergence = pDivMin + (sin(divCenter*M_PI_2) * (pDiv - pDivMin)); + else if (PDivMode::SQRT == qp.positionDivergenceMode) positionDivergence = pDivMin + (sqrt(divCenter) * (pDiv - pDivMin)); + else if (PDivMode::Switch == qp.positionDivergenceMode) positionDivergence = divCenter < 1e-1 ? pDivMin : pDiv; + } + positionDivergence = K.gateway->decimal.funds.round(positionDivergence); + }; + void report() const { + K.log("HB", "TBP: " + + to_string((int)(targetBasePosition / baseValue * 1e+2)) + "% = " + K.gateway->decimal.funds.str(targetBasePosition) + + " " + K.gateway->base + ", pDiv: " + + to_string((int)(positionDivergence / baseValue * 1e+2)) + "% = " + K.gateway->decimal.funds.str(positionDivergence) + + " " + K.gateway->base); + }; + string explain() const override { + return to_string(targetBasePosition); + }; + string explainOK() const override { + return "loaded TBP = % " + K.gateway->base; + }; + }; + static void to_json(json &j, const Target &k) { + j = { + { "tbp", k.targetBasePosition}, + {"pDiv", k.positionDivergence} + }; + }; + static void from_json(const json &j, Target &k) { + k.targetBasePosition = j.value("tbp", 0.0); + k.positionDivergence = j.value("pDiv", 0.0); + }; + + struct Wallets: public Client::Broadcast { + Wallet base, + quote; + Target target; + Safety safety; + Profits profits; + private_ref: + const KryptoNinja &K; + const Orders &orders; + const Price &fairValue; + public: + Wallets(const KryptoNinja &bot, const QuotingParams &q, const Orders &o, const Buttons &b, const MarketLevels &l) + : Broadcast(bot) + , target(bot, q, l.stats.ewma.targetPositionAutoPercentage, base.value) + , safety(bot, q, base, b, l.fairValue, target.targetBasePosition, target.positionDivergence) + , profits(bot, q) + , K(bot) + , orders(o) + , fairValue(l.fairValue) + {}; + bool ready() const { + return !safety.empty(); + }; + void read_from_gw(const Wallet &raw) { + if (raw.currency == K.gateway->base) base = raw; + else if (raw.currency == K.gateway->quote) quote = raw; + else return; + if (base.currency.empty() or quote.currency.empty() or !fairValue) return; + calcMaxFunds(); + calcFunds(); + }; + void calcFunds() { + calcFundsSilently(); + broadcast(); + }; + void calcFundsAfterOrder() { + if (!orders.last) return; + calcHeldAmount(orders.last->side); + calcFundsSilently(); + if (orders.last->qtyFilled) + safety.insertTrade(*orders.last); + }; + mMatter about() const override { + return mMatter::Position; + }; + bool realtime() const override { + return false; + }; + bool read_asap() const override { + return false; + }; + bool read_same_blob() const override { + return false; + }; + private: + void calcFundsSilently() { + if (base.currency.empty() or quote.currency.empty() or !fairValue) return; + calcValues(); + calcProfits(); + target.calcTargetBasePos(); + }; + void calcHeldAmount(const Side &side) { + const Amount heldSide = orders.held(side); + if (side == Side::Ask) + base = {base.total - heldSide, heldSide, base.currency}; + else if (side == Side::Bid) + quote = {quote.total - heldSide, heldSide, quote.currency}; + }; + void calcValues() { + base.value = base.total + (quote.total / fairValue); + quote.value = quote.total + (base.total * fairValue); + }; + void calcProfits() { + if (!profits.ratelimit()) + profits.push_back({base.value, quote.value, Tstamp}); + base.profit = profits.calcBaseDiff(); + quote.profit = profits.calcQuoteDiff(); + }; + void calcMaxFunds() { + auto limit = K.arg("wallet-limit"); + if (limit) { + limit -= quote.held / fairValue; + if (limit > 0 and quote.amount / fairValue > limit) { + quote.amount = limit * fairValue; + base.amount = limit = 0; + } else limit -= quote.amount / fairValue; + limit -= base.held; + if (limit > 0 and base.amount > limit) + base.amount = limit; + } + }; + }; + static void to_json(json &j, const Wallets &k) { + j = { + { "base", k.base }, + {"quote", k.quote} + }; + }; + + struct DummyMarketMaker: public Client::Clicked { + private: + void (*calcRawQuotesFromMarket)( + const MarketLevels&, + const Price&, + const Price&, + const Amount&, + const Amount&, + System::Quote&, + System::Quote& + ) = nullptr; + private_ref: + const KryptoNinja &K; + const QuotingParams &qp; + const MarketLevels &levels; + const Wallets &wallet; + System::Quote &bid; + System::Quote &ask; + public: + DummyMarketMaker(const KryptoNinja &bot, const QuotingParams &q, const MarketLevels &l, const Wallets &w, System::Quote &b, System::Quote &a) + : Clicked(bot, { + {&q, [&]() { mode(); }} + }) + , K(bot) + , qp(q) + , levels(l) + , wallet(w) + , bid(b) + , ask(a) + {}; + void calcRawQuotes(bool *const superSpread) const { + calcRawQuotesFromMarket( + levels, + K.gateway->tickPrice, + levels.calcQuotesWidth(superSpread), + wallet.safety.buySize, + wallet.safety.sellSize, + bid, + ask + ); + }; + private: + void mode() { + if (qp.mode == QuotingMode::Top) calcRawQuotesFromMarket = calcTopOfMarket; + else if (qp.mode == QuotingMode::Mid) calcRawQuotesFromMarket = calcMidOfMarket; + else if (qp.mode == QuotingMode::Join) calcRawQuotesFromMarket = calcJoinMarket; + else if (qp.mode == QuotingMode::InverseJoin) calcRawQuotesFromMarket = calcInverseJoinMarket; + else if (qp.mode == QuotingMode::InverseTop) calcRawQuotesFromMarket = calcInverseTopOfMarket; + else if (qp.mode == QuotingMode::HamelinRat) calcRawQuotesFromMarket = calcColossusOfMarket; + else if (qp.mode == QuotingMode::Depth) calcRawQuotesFromMarket = calcDepthOfMarket; + else error("QE", "Invalid quoting mode saved, consider to remove the database file"); + }; + static void quoteAtTopOfMarket(const MarketLevels &levels, const Price &tickPrice, System::Quote &bid, System::Quote &ask) { + const Level &topBid = levels.bids.begin()->size > tickPrice + ? levels.bids.at(0) + : levels.bids.at(levels.bids.size() > 1); + const Level &topAsk = levels.asks.begin()->size > tickPrice + ? levels.asks.at(0) + : levels.asks.at(levels.asks.size() > 1); + bid.price = topBid.price; + ask.price = topAsk.price; + }; + static void calcTopOfMarket( + const MarketLevels &levels, + const Price &tickPrice, + const Price &widthPing, + const Amount &bidSize, + const Amount &askSize, + System::Quote &bid, + System::Quote &ask + ) { + quoteAtTopOfMarket(levels, tickPrice, bid, ask); + bid.price = fmin(levels.fairValue - widthPing / 2.0, bid.price + tickPrice); + ask.price = fmax(levels.fairValue + widthPing / 2.0, ask.price - tickPrice); + bid.size = bidSize; + ask.size = askSize; + }; + static void calcMidOfMarket( + const MarketLevels &levels, + const Price &, + const Price &widthPing, + const Amount &bidSize, + const Amount &askSize, + System::Quote &bid, + System::Quote &ask + ) { + bid.price = fmax(levels.fairValue - widthPing, 0); + ask.price = levels.fairValue + widthPing; + bid.size = bidSize; + ask.size = askSize; + }; + static void calcJoinMarket( + const MarketLevels &levels, + const Price &tickPrice, + const Price &widthPing, + const Amount &bidSize, + const Amount &askSize, + System::Quote &bid, + System::Quote &ask + ) { + quoteAtTopOfMarket(levels, tickPrice, bid, ask); + bid.price = fmin(levels.fairValue - widthPing / 2.0, bid.price); + ask.price = fmax(levels.fairValue + widthPing / 2.0, ask.price); + bid.size = bidSize; + ask.size = askSize; + }; + static void calcInverseJoinMarket( + const MarketLevels &levels, + const Price &tickPrice, + const Price &widthPing, + const Amount &bidSize, + const Amount &askSize, + System::Quote &bid, + System::Quote &ask + ) { + quoteAtTopOfMarket(levels, tickPrice, bid, ask); + Price mktWidth = abs(ask.price - bid.price); + if (mktWidth > widthPing) { + ask.price = ask.price + widthPing; + bid.price = bid.price - widthPing; + } + if (mktWidth < (2.0 * widthPing / 3.0)) { + ask.price = ask.price + widthPing / 4.0; + bid.price = bid.price - widthPing / 4.0; + } + bid.size = bidSize; + ask.size = askSize; + }; + static void calcInverseTopOfMarket( + const MarketLevels &levels, + const Price &tickPrice, + const Price &widthPing, + const Amount &bidSize, + const Amount &askSize, + System::Quote &bid, + System::Quote &ask + ) { + quoteAtTopOfMarket(levels, tickPrice, bid, ask); + Price mktWidth = abs(ask.price - bid.price); + if (mktWidth > widthPing) { + ask.price = ask.price + widthPing; + bid.price = bid.price - widthPing; + } + bid.price = bid.price + tickPrice; + ask.price = ask.price - tickPrice; + if (mktWidth < (2.0 * widthPing / 3.0)) { + ask.price = ask.price + widthPing / 4.0; + bid.price = bid.price - widthPing / 4.0; + } + bid.size = bidSize; + ask.size = askSize; + }; + static void calcColossusOfMarket( + const MarketLevels &levels, + const Price &tickPrice, + const Price &, + const Amount &bidSize, + const Amount &askSize, + System::Quote &bid, + System::Quote &ask + ) { + quoteAtTopOfMarket(levels, tickPrice, bid, ask); + bid.size = 0; + ask.size = 0; + for (const Level &it : levels.bids) + if (bid.size < it.size and it.price <= bid.price) { + bid.size = it.size; + bid.price = it.price; + } + for (const Level &it : levels.asks) + if (ask.size < it.size and it.price >= ask.price) { + ask.size = it.size; + ask.price = it.price; + } + if (bid.size) bid.price += tickPrice; + if (ask.size) ask.price -= tickPrice; + bid.size = bidSize; + ask.size = askSize; + }; + static void calcDepthOfMarket( + const MarketLevels &levels, + const Price &, + const Price &depth, + const Amount &bidSize, + const Amount &askSize, + System::Quote &bid, + System::Quote &ask + ) { + Price bidPx = levels.bids.cbegin()->price; + Amount bidDepth = 0; + for (const Level &it : levels.bids) { + bidDepth += it.size; + if (bidDepth >= depth) break; + else bidPx = it.price; + } + Price askPx = levels.asks.cbegin()->price; + Amount askDepth = 0; + for (const Level &it : levels.asks) { + askDepth += it.size; + if (askDepth >= depth) break; + else askPx = it.price; + } + bid.price = bidPx; + ask.price = askPx; + bid.size = bidSize; + ask.size = askSize; + }; + }; + + struct AntonioCalculon: public System::Quotes { + DummyMarketMaker dummyMM; + unsigned int AK47inc = 0; + SideAPR sideAPR = SideAPR::Off; + bool superSpread = false; + private_ref: + const KryptoNinja &K; + const QuotingParams &qp; + const MarketLevels &levels; + const Wallets &wallet; + public: + AntonioCalculon(const KryptoNinja &bot, const QuotingParams &q, const MarketLevels &l, const Wallets &w) + : Quotes(bot) + , dummyMM(bot, q, l, w, bid, ask) + , K(bot) + , qp(q) + , levels(l) + , wallet(w) + {}; + private: + string explainState(const System::Quote "e) const override { + string reason = ""; + if (quote.state == QuoteState::Live) + reason = " LIVE " + ANSI_PUKE_WHITE + + "because of reasons (ping: " + + K.gateway->decimal.price.str(quote.price) + " " + K.gateway->quote + + ", fair value: " + + K.gateway->decimal.price.str(levels.fairValue) + " " + K.gateway->quote + +")"; + else if (quote.state == QuoteState::DepletedFunds) + reason = " PAUSED " + ANSI_PUKE_WHITE + + "because not enough available funds (" + + (quote.side == Side::Bid + ? K.gateway->decimal.price.str(wallet.quote.amount) + " " + K.gateway->quote + : K.gateway->decimal.amount.str(wallet.base.amount) + " " + K.gateway->base + ) + ")"; + else if (quote.state == QuoteState::WaitingPing) + reason = " PAUSED " + ANSI_PUKE_WHITE + + "because waiting for a ping on " + + (quote.side == Side::Bid ? "ask" : "bid") + + " side"; + else if (quote.state == QuoteState::UpTrendHeld + or quote.state == QuoteState::DownTrendHeld) + reason = " PAUSED " + ANSI_PUKE_WHITE + + "because ewmaTrend limit was reached"; + else if (quote.state == QuoteState::TBPHeld) + reason = " PAUSED " + ANSI_PUKE_WHITE + + "because target base position limit was reached"; + else if (quote.state == QuoteState::MaxTradesSeconds) + reason = " PAUSED " + ANSI_PUKE_WHITE + + "because trades/sec limit was reached"; + else if (quote.state == QuoteState::DisabledQuotes) + reason = "DISABLED " + ANSI_PUKE_WHITE + + "because an admin considered it"; + return reason; + }; + void calcRawQuotes() override { + dummyMM.calcRawQuotes(&superSpread); + }; + void applyQuotingParameters() override { + debug("?"); applySuperTrades(); + debug("A"); applyEwmaProtection(); + debug("B"); applyTotalBasePosition(); + debug("C"); applyStdevProtection(); + debug("D"); applyAggressivePositionRebalancing(); + debug("E"); applyAK47Increment(); + debug("F"); applyBestWidth(); + debug("G"); applyTradesPerMinute(); + debug("H"); applyRoundPrice(); + debug("I"); applyRoundSize(); + debug("J"); applyDepleted(); + debug("K"); applyWaitingPing(); + debug("L"); applyEwmaTrendProtection(); + debug("!"); + }; + void applySuperTrades() { + if (!superSpread + or (qp.superTrades != SOP::Size and qp.superTrades != SOP::TradesSize) + ) return; + if (!qp.buySizeMax and !bid.empty()) + bid.size = fmin( + qp.sopSizeMultiplier * bid.size, + (wallet.quote.amount / bid.price) / 2 + ); + if (!qp.sellSizeMax and !ask.empty()) + ask.size = fmin( + qp.sopSizeMultiplier * ask.size, + wallet.base.amount / 2 + ); + }; + void applyEwmaProtection() { + if (!qp.protectionEwmaQuotePrice or !levels.stats.ewma.mgEwmaP) return; + if (!ask.empty()) + ask.price = fmax(levels.stats.ewma.mgEwmaP, ask.price); + if (!bid.empty()) + bid.price = fmin(levels.stats.ewma.mgEwmaP, bid.price); + }; + void applyTotalBasePosition() { + if (wallet.base.total < wallet.target.targetBasePosition - wallet.target.positionDivergence) { + ask.skip(QuoteState::TBPHeld); + if (!bid.empty() and qp.aggressivePositionRebalancing != APR::Off) { + sideAPR = SideAPR::Buy; + if (!qp.buySizeMax) + bid.size = fmin( + qp.aprMultiplier * bid.size, + wallet.target.targetBasePosition - wallet.base.total + ); + } + } + else if (wallet.base.total >= wallet.target.targetBasePosition + wallet.target.positionDivergence) { + bid.skip(QuoteState::TBPHeld); + if (!ask.empty() and qp.aggressivePositionRebalancing != APR::Off) { + sideAPR = SideAPR::Sell; + if (!qp.sellSizeMax) + ask.size = fmin( + qp.aprMultiplier * ask.size, + wallet.base.total - wallet.target.targetBasePosition + ); + } + } + else sideAPR = SideAPR::Off; + }; + void applyStdevProtection() { + if (qp.quotingStdevProtection == STDEV::Off or !levels.stats.stdev.fair) return; + if (!ask.empty() and ( + qp.quotingStdevProtection == STDEV::OnFV + or qp.quotingStdevProtection == STDEV::OnTops + or qp.quotingStdevProtection == STDEV::OnTop + or sideAPR != SideAPR::Sell + )) + ask.price = fmax( + (qp.quotingStdevBollingerBands + ? (qp.quotingStdevProtection == STDEV::OnFV or qp.quotingStdevProtection == STDEV::OnFVAPROff) + ? levels.stats.stdev.fairMean + : ((qp.quotingStdevProtection == STDEV::OnTops or qp.quotingStdevProtection == STDEV::OnTopsAPROff) + ? levels.stats.stdev.topMean + : levels.stats.stdev.askMean) + : levels.fairValue + ) + ((qp.quotingStdevProtection == STDEV::OnFV or qp.quotingStdevProtection == STDEV::OnFVAPROff) + ? levels.stats.stdev.fair + : ((qp.quotingStdevProtection == STDEV::OnTops or qp.quotingStdevProtection == STDEV::OnTopsAPROff) + ? levels.stats.stdev.top + : levels.stats.stdev.ask)), + ask.price + ); + if (!bid.empty() and ( + qp.quotingStdevProtection == STDEV::OnFV + or qp.quotingStdevProtection == STDEV::OnTops + or qp.quotingStdevProtection == STDEV::OnTop + or sideAPR != SideAPR::Buy + )) + bid.price = fmin( + (qp.quotingStdevBollingerBands + ? (qp.quotingStdevProtection == STDEV::OnFV or qp.quotingStdevProtection == STDEV::OnFVAPROff) + ? levels.stats.stdev.fairMean + : ((qp.quotingStdevProtection == STDEV::OnTops or qp.quotingStdevProtection == STDEV::OnTopsAPROff) + ? levels.stats.stdev.topMean + : levels.stats.stdev.bidMean) + : levels.fairValue + ) - ((qp.quotingStdevProtection == STDEV::OnFV or qp.quotingStdevProtection == STDEV::OnFVAPROff) + ? levels.stats.stdev.fair + : ((qp.quotingStdevProtection == STDEV::OnTops or qp.quotingStdevProtection == STDEV::OnTopsAPROff) + ? levels.stats.stdev.top + : levels.stats.stdev.bid)), + bid.price + ); + }; + void applyAggressivePositionRebalancing() { + if (qp.safety == QuotingSafety::Off) return; + const Price widthPong = qp.widthPercentage + ? qp.widthPongPercentage * levels.fairValue / 100 + : qp.widthPong; + if (!ask.empty() and wallet.safety.buyPing) { + const Price sellPong = (wallet.safety.buyPing * (1 + K.gateway->makeFee) + widthPong) / (1 - K.gateway->makeFee); + if ((qp.aggressivePositionRebalancing == APR::SizeWidth and sideAPR == SideAPR::Sell) + or ((qp.safety == QuotingSafety::PingPong or qp.safety == QuotingSafety::PingPoing) + ? ask.price < sellPong + : qp.pongAt == PongAt::ShortPingAggressive + or qp.pongAt == PongAt::AveragePingAggressive + or qp.pongAt == PongAt::LongPingAggressive + ) + ) ask.price = fmax(levels.bids.at(0).price + K.gateway->tickPrice, sellPong); + ask.isPong = ask.price >= sellPong; + } + if (!bid.empty() and wallet.safety.sellPing) { + const Price buyPong = (wallet.safety.sellPing * (1 - K.gateway->makeFee) - widthPong) / (1 + K.gateway->makeFee); + if ((qp.aggressivePositionRebalancing == APR::SizeWidth and sideAPR == SideAPR::Buy) + or ((qp.safety == QuotingSafety::PingPong or qp.safety == QuotingSafety::PingPoing) + ? bid.price > buyPong + : qp.pongAt == PongAt::ShortPingAggressive + or qp.pongAt == PongAt::AveragePingAggressive + or qp.pongAt == PongAt::LongPingAggressive + ) + ) bid.price = fmin(levels.asks.at(0).price - K.gateway->tickPrice, buyPong); + bid.isPong = bid.price <= buyPong; + } + }; + void applyAK47Increment() { + if (qp.safety != QuotingSafety::AK47) return; + const Price range = qp.percentageValues + ? qp.rangePercentage * levels.fairValue / 100 + : qp.range; + if (!bid.empty()) + bid.price -= AK47inc * range; + if (!ask.empty()) + ask.price += AK47inc * range; + if (++AK47inc > qp.bullets) AK47inc = 0; + }; + void applyBestWidth() { + if (!qp.bestWidth) return; + const Amount bestWidthSize = (sideAPR == SideAPR::Off ? qp.bestWidthSize : 0); + Amount depth = 0; + if (!ask.empty()) + for (const Level &it : levels.asks) + if (it.price > ask.price) { + depth += it.size; + if (depth <= bestWidthSize) continue; + const Price bestAsk = it.price - K.gateway->tickPrice; + if (bestAsk > ask.price) + ask.price = bestAsk; + break; + } + depth = 0; + if (!bid.empty()) + for (const Level &it : levels.bids) + if (it.price < bid.price) { + depth += it.size; + if (depth <= bestWidthSize) continue; + const Price bestBid = it.price + K.gateway->tickPrice; + if (bestBid < bid.price) + bid.price = bestBid; + break; + } + }; + void applyTradesPerMinute() { + const double factor = (superSpread and ( + qp.superTrades == SOP::Trades or qp.superTrades == SOP::TradesSize + )) ? qp.sopWidthMultiplier + : 1; + if (!ask.isPong and wallet.safety.sell >= qp.tradesPerMinute * factor) + ask.skip(QuoteState::MaxTradesSeconds); + if (!bid.isPong and wallet.safety.buy >= qp.tradesPerMinute * factor) + bid.skip(QuoteState::MaxTradesSeconds); + }; + void applyRoundPrice() { + if (!bid.empty()) + bid.price = fmax( + 0, + K.gateway->decimal.price.round(bid.price) + ); + if (!ask.empty()) + ask.price = fmax( + bid.price + K.gateway->tickPrice, + K.gateway->decimal.price.round(ask.price) + ); + }; + void applyRoundSize() { + if (!bid.empty()) { + const Amount minBid = K.gateway->minValue + ? fmax(K.gateway->minSize, K.gateway->minValue / bid.price) + : K.gateway->minSize; + const Amount maxBid = wallet.quote.total / bid.price; + bid.size = K.gateway->decimal.amount.round( + fmax(minBid * (1.0 + K.gateway->takeFee * 1e+2), fmin( + bid.size, + K.gateway->decimal.amount.floor(maxBid) + )) + ); + } + if (!ask.empty()) { + const Amount minAsk = K.gateway->minValue + ? fmax(K.gateway->minSize, K.gateway->minValue / ask.price) + : K.gateway->minSize; + const Amount maxAsk = wallet.base.total; + ask.size = K.gateway->decimal.amount.round( + fmax(minAsk * (1.0 + K.gateway->takeFee * 1e+2), fmin( + ask.size, + K.gateway->decimal.amount.floor(maxAsk) + )) + ); + } + }; + void applyDepleted() { + if (!bid.empty() and wallet.quote.total / bid.price < bid.size) + bid.skip(QuoteState::DepletedFunds); + if (!ask.empty() and wallet.base.total < ask.size) + ask.skip(QuoteState::DepletedFunds); + }; + void applyWaitingPing() { + if (qp.safety == QuotingSafety::Off) return; + if (!ask.isPong and ( + (bid.state != QuoteState::DepletedFunds and (qp.pingAt == PingAt::DepletedSide or qp.pingAt == PingAt::DepletedBidSide)) + or qp.pingAt == PingAt::StopPings + or qp.pingAt == PingAt::BidSide + or qp.pingAt == PingAt::DepletedAskSide + )) ask.skip(QuoteState::WaitingPing); + if (!bid.isPong and ( + (ask.state != QuoteState::DepletedFunds and (qp.pingAt == PingAt::DepletedSide or qp.pingAt == PingAt::DepletedAskSide)) + or qp.pingAt == PingAt::StopPings + or qp.pingAt == PingAt::AskSide + or qp.pingAt == PingAt::DepletedBidSide + )) bid.skip(QuoteState::WaitingPing); + }; + void applyEwmaTrendProtection() { + if (!qp.quotingEwmaTrendProtection or !levels.stats.ewma.mgEwmaTrendDiff) return; + if (levels.stats.ewma.mgEwmaTrendDiff > qp.quotingEwmaTrendThreshold) + ask.skip(QuoteState::UpTrendHeld); + else if (levels.stats.ewma.mgEwmaTrendDiff < -qp.quotingEwmaTrendThreshold) + bid.skip(QuoteState::DownTrendHeld); + }; + }; + + struct Semaphore: public Client::Broadcast, + public Client::Clickable, + public Hotkey::Keymap { + Connectivity greenButton = Connectivity::Disconnected, + greenGateway = Connectivity::Disconnected; + private_ref: + const KryptoNinja &K; + public: + Semaphore(const KryptoNinja &bot) + : Broadcast(bot) + , Clickable(bot) + , Keymap(bot, { + {'Q', [&]() { exit(); }}, + {'q', [&]() { exit(); }}, + {'\e', [&]() { toggle(); }} + }) + , K(bot) + {}; + void click(const json &j) override { + if (j.is_object() + and j.at("agree").is_number() + and j.at("agree").get() != K.gateway->adminAgreement + ) toggle(); + }; + bool paused() const { + return !(bool)greenButton; + }; + bool offline() const { + return !(bool)greenGateway; + }; + void read_from_gw(const Connectivity &raw) { + greenGateway = raw; + switchButton(); + }; + mMatter about() const override { + return mMatter::Connectivity; + }; + private: + void toggle() { + K.gateway->adminAgreement = (Connectivity)!(bool)K.gateway->adminAgreement; + switchButton(); + }; + void switchButton() { + greenButton = (Connectivity)( + (bool)greenGateway and (bool)K.gateway->adminAgreement + ); + broadcast(); + K.repaint(); + }; + }; + static void to_json(json &j, const Semaphore &k) { + j = { + { "agree", k.greenButton }, + {"online", k.greenGateway} + }; + }; + + struct Product: public Client::Broadcast { + private_ref: + const KryptoNinja &K; + public: + Product(const KryptoNinja &bot) + : Broadcast(bot) + , K(bot) + {}; + json to_json() const { + return { + { "exchange", K.gateway->exchange }, + { "base", K.gateway->base }, + { "quote", K.gateway->quote }, + { "symbol", K.gateway->symbol }, + { "webMarket", K.gateway->web() }, + { "webOrders", K.gateway->web(true) }, + { "tickPrice", K.gateway->decimal.price.stream.precision() }, + { "tickSize", K.gateway->decimal.amount.stream.precision()}, + { "stepPrice", K.gateway->decimal.price.step }, + { "stepSize", K.gateway->decimal.amount.step }, + { "minSize", K.gateway->minSize }, + { "inet", K.arg("interface") }, + {"environment", K.arg("title") }, + { "matryoshka", K.arg("matryoshka") }, + { "source", K_SOURCE " " K_BUILD } + }; + }; + mMatter about() const override { + return mMatter::ProductAdvertisement; + }; + }; + static void to_json(json &j, const Product &k) { + j = k.to_json(); + }; + + struct Memory: public Client::Broadcast { + public: + unsigned int orders_60s = 0; + private: + Product product; + private_ref: + const KryptoNinja &K; + public: + Memory(const KryptoNinja &bot) + : Broadcast(bot) + , product(bot) + , K(bot) + {}; + void timer_60s() { + broadcast(); + orders_60s = 0; + }; + json to_json() const { + return { + { "addr", K.gateway->unlock }, + { "freq", orders_60s }, + { "theme", K.arg("ignore-moon") + + K.arg("ignore-sun")}, + {"memory", K.memSize() }, + {"dbsize", K.dbSize() } + }; + }; + mMatter about() const override { + return mMatter::ApplicationState; + }; + }; + static void to_json(json &j, const Memory &k) { + j = k.to_json(); + }; + + struct Broker: public Client::Broadcast, + public Client::Clicked { + Memory memory; + Semaphore semaphore; + AntonioCalculon quotes; + private_ref: + const KryptoNinja &K; + const QuotingParams &qp; + Orders &orders; + public: + Broker(const KryptoNinja &bot, const QuotingParams &q, Orders &o, const Buttons &b, const MarketLevels &l, const Wallets &w) + : Broadcast(bot) + , Clicked(bot, { + {&b.submit, [&](const json &j) { K.place(j); }}, + {&b.cancel, [&](const json &j) { K.cancel(j); }}, + {&b.cancelAll, [&]() { K.cancel(); }} + }) + , memory(bot) + , semaphore(bot) + , quotes(bot, q, l, w) + , K(bot) + , qp(q) + , orders(o) + {}; + bool ready() { + if (semaphore.offline()) { + quotes.offline(); + return false; + } + return true; + }; + void calcQuotes() { + if (semaphore.paused()) { + quotes.paused(); + K.cancel(); + } else { + quotes.calcQuotes(); + quote2orders(quotes.ask); + quote2orders(quotes.bid); + } + }; + void nuke() { + K.cancel(); + K.gateway->cancel(); + }; + void quit() { + unsigned int n = 0; + for (Order *const it : orders.open()) { + K.gateway->cancel(it); + n++; + } + if (n) + K.log("QE", "Canceled " + to_string(n) + " open order" + string(n != 1, 's') + " before quit"); + }; + json to_json() const { + return { + { "bidStatus", quotes.bid.state }, + { "askStatus", quotes.ask.state }, + { "sideAPR", quotes.sideAPR }, + {"quotesInMemoryWaiting", orders.zombies.countWaiting}, + {"quotesInMemoryWorking", orders.zombies.countWorking}, + {"quotesInMemoryZombies", orders.zombies.countZombies} + }; + }; + mMatter about() const override { + return mMatter::QuoteStatus; + }; + bool realtime() const override { + return false; + }; + bool read_same_blob() const override { + return false; + }; + private: + bool abandon(const Order &order, const Price ¤tPrice, System::Quote "e, unsigned int &limit) { + if (orders.zombies.stillAlive(order)) { + if ((order.status == Status::Waiting + or abs(order.price - currentPrice) < K.gateway->tickPrice + or (qp.lifetime and order.time + qp.lifetime > Tstamp) + ) and (qp.safety != QuotingSafety::AK47 + or (!limit or !--limit) + )) quote.skip(); + else return true; + } + return false; + }; + vector abandon(System::Quote "e) { + vector abandoned; + unsigned int limit = qp.bullets; + const Price currentPrice = quote.price; + for (Order *const it : orders.at(quote.side)) + if (!currentPrice or abandon(*it, currentPrice, quote, limit)) + abandoned.push_back(it); + return abandoned; + }; + void quote2orders(System::Quote "e) { + const vector abandoned = abandon(quote); + const unsigned int replace = K.gateway->askForReplace and !( + quote.empty() or abandoned.empty() + ); + for ( + auto it = abandoned.end() - replace; + it --> abandoned.begin(); + K.cancel(*it) + ); + if (quote.empty()) return; + if (replace) + K.replace(quote.price, quote.isPong, abandoned.back()); + else K.place({ + K.gateway->symbol, + quote.side, + quote.price, + quote.size, + Tstamp, + quote.isPong, + K.gateway->randId() + }); + memory.orders_60s++; + }; + }; + static void to_json(json &j, const Broker &k) { + j = k.to_json(); + }; + + class Engine { + public: + QuotingParams qp; + Orders orders; + Buttons button; + MarketLevels levels; + Wallets wallet; + Broker broker; + public: + Engine(const KryptoNinja &bot) + : qp(bot) + , orders(bot) + , button(bot) + , levels(bot, qp, orders) + , wallet(bot, qp, orders, button, levels) + , broker(bot, qp, orders, button, levels, wallet) + {}; + public: + void read(const Connectivity &rawdata) { + broker.semaphore.read_from_gw(rawdata); + }; + void read(const Wallet &rawdata) { + wallet.read_from_gw(rawdata); + }; + void read(const Levels &rawdata) { + levels.read_from_gw(rawdata); + wallet.calcFunds(); + calcQuotes(); + }; + void read(const Order &rawdata) { + orders.read_from_gw(rawdata); + wallet.calcFundsAfterOrder(); + }; + void read(const Trade &rawdata) { + levels.stats.takerTrades.read_from_gw(rawdata); + }; + void timer_1s(const unsigned int &tick) { + if (levels.ready()) { + if (qp.cancelOrdersAuto + and !(tick % 300) + ) broker.nuke(); + levels.timer_1s(); + if (!(tick % 60)) { + levels.timer_60s(); + broker.memory.timer_60s(); + } + wallet.safety.timer_1s(); + calcQuotes(); + } + }; + void quit() { + broker.quit(); + }; + private: + void calcQuotes() { + if (broker.ready() and levels.ready() and wallet.ready()) + broker.calcQuotes(); + broker.broadcast(); + orders.zombies.purge(); + }; + }; +} diff --git a/src/bin/trading-bot/trading-bot.disk.S b/src/bin/trading-bot/trading-bot.disk.S new file mode 100644 index 000000000..31ab10d59 --- /dev/null +++ b/src/bin/trading-bot/trading-bot.disk.S @@ -0,0 +1,20 @@ +//! \file +//! \brief Lazy disk file builder using global external linkage. +//! \note Line 11 can be removed; just exemplifies each column. +//! \note See src/lib/Krypto.ninja-disk.S for info about DISK() +//! \note Webserver 404 page is loaded using an empty url path. +//! \note Filesystem paths at www/* are included from lib/, at: +//! - /var/lib/K/client/www/* +//! - ./src/lib/Krypto.ninja-client/www/* + +#define DISK(file) \ +/*file( id , www/filesystem/path/to/files , /webserver/url/paths )*/\ + file( 01 , www/index.html , / ) \ + file( 02 , www/favicon.ico , /favicon.ico ) \ + file( 03 , www/js/client.min.js , /js/client.min.js ) \ + file( 04 , www/css/bootstrap.min.css , /css/bootstrap.min.css ) \ + file( 05 , www/css/bootstrap-dark.min.css , /css/bootstrap-dark.min.css ) \ + file( 06 , www/font/beacons.woff2 , /font/beacons.woff2 ) \ + file( 07 , www/audio/0.mp3 , /audio/0.mp3 ) \ + file( 08 , www/audio/1.mp3 , /audio/1.mp3 ) \ + file( 00 , www/.bomb.gzip , ) diff --git a/src/bin/trading-bot/trading-bot.main.h b/src/bin/trading-bot/trading-bot.main.h new file mode 100644 index 000000000..43a8b55bb --- /dev/null +++ b/src/bin/trading-bot/trading-bot.main.h @@ -0,0 +1,146 @@ +class TradingBot: public KryptoNinja { + public: + tribeca::Engine engine; + public: + TradingBot() + : engine(*this) + { + display = { terminal }; + events = { + [&](const Connectivity &rawdata) { engine.read(rawdata); }, + [&](const Wallet &rawdata) { engine.read(rawdata); }, + [&](const Levels &rawdata) { engine.read(rawdata); }, + [&](const Order &rawdata) { engine.read(rawdata); }, + [&](const Trade &rawdata) { engine.read(rawdata); }, + [&](const unsigned int &tick ) { engine.timer_1s(tick); }, + [&]( ) { engine.quit(); } + }; + arguments = { + { + {"maker-fee", "AMOUNT", "0", "set custom percentage of maker fee, like '0.1'"}, + {"taker-fee", "AMOUNT", "0", "set custom percentage of taker fee, like '0.1'"}, + {"min-size", "AMOUNT", "0", "set custom minimum order size, like '0.01'"}, + {"wallet-limit", "AMOUNT", "0", "set AMOUNT in base currency to limit the balance," + ANSI_NEW_LINE "otherwise the full available balance can be used"} + } + }; + }; + private: + static string terminal(); +} K; + +string TradingBot::terminal() { + const string baseValue = K.gateway->decimal.funds.str(K.engine.wallet.base.value), + quoteValue = K.gateway->decimal.price.str(K.engine.wallet.quote.value); + const string coins = ANSI_HIGH_MAGENTA + baseValue + + ANSI_PUKE_MAGENTA + ' ' + K.gateway->base + + ANSI_PUKE_GREEN + " or " + + ANSI_HIGH_CYAN + quoteValue + + ANSI_PUKE_CYAN + ' ' + K.gateway->quote + + ANSI_PUKE_WHITE + " ├"; + const string quit = "┤ [" + ANSI_HIGH_WHITE + + "ESC" + ANSI_PUKE_WHITE + "]: " + + (K.engine.broker.semaphore.paused() + ? "Start" + : "Stop?" + ) + "!" + + ", [" + ANSI_HIGH_WHITE + + "q" + ANSI_PUKE_WHITE + "]: Quit!"; + const string title = K.arg("exchange") + + ANSI_PUKE_GREEN + + ' ' + (K.arg("headless") + ? "headless" + : "UI at " + K.location() + ) + ' '; + const string space = string(fmax(0, + coins.length() + - title.length() + - ANSI_SYMBOL_SIZE(1) + - ANSI_COLORS_SIZE(5) + ), ' '); + const string top = "┌───────┐ K │ " + + ANSI_HIGH_GREEN + title + space + + ANSI_PUKE_WHITE + "├"; + string top_line; + for ( + unsigned int i = fmax(0, + K.display.width + - 1 + - top.length() + - quit.length() + + ANSI_SYMBOL_SIZE(12) + + ANSI_COLORS_SIZE(7) + ); + i --> 0; + top_line += "─" + ); + string coins_line; + for ( + unsigned int i = fmax(0, + title.length() + + space.length() + - coins.length() + + ANSI_SYMBOL_SIZE(1) + + ANSI_COLORS_SIZE(5) + ); + i --> 0; + coins_line += "─" + ); + const vector openOrders = K.engine.orders.working(true); + unsigned int orders = openOrders.size(); + unsigned int rows = 0; + string data = ANSI_PUKE_WHITE + + (openOrders.empty() and K.engine.broker.semaphore.paused() + ? "└" + : "├" + ) + "───┤< ("; + if (K.engine.broker.semaphore.offline()) { + data += ANSI_HIGH_RED + "DISCONNECTED" + + ANSI_PUKE_WHITE + ")" + + ANSI_END_LINE; + } else { + if (K.engine.broker.semaphore.paused()) + data += ANSI_WAVE_YELLOW + "press START to trade" + + ANSI_PUKE_WHITE + ")"; + else + data += ANSI_PUKE_YELLOW + to_string(orders) + + ANSI_PUKE_WHITE + ") Open Orders"; + data += " while " + + ANSI_PUKE_GREEN + + "1 " + K.gateway->base + + " = " + + ANSI_HIGH_GREEN + + K.gateway->decimal.price.str(K.engine.levels.fairValue) + + ANSI_PUKE_GREEN + + " " + K.gateway->quote + + (K.engine.broker.semaphore.paused() ? ' ' : ':') + + ANSI_END_LINE; + for (const auto &it : openOrders) { + data += ANSI_PUKE_WHITE + "├" + + (it.side == Side::Bid ? ANSI_HIGH_CYAN + "BID" : ANSI_PUKE_MAGENTA + "ASK") + + " > " + + K.gateway->decimal.amount.str(it.quantity) + + ' ' + K.gateway->base + " at price " + + K.gateway->decimal.price.str(it.price) + + ' ' + K.gateway->quote + " (value " + + K.gateway->decimal.price.str(abs(it.price * it.quantity)) + + ' ' + K.gateway->quote + ")" + + ANSI_END_LINE; + ++rows; + } + if (!K.engine.broker.semaphore.paused()) + while (orders < 2) { + data += ANSI_PUKE_WHITE + "├" + + ANSI_END_LINE; + ++orders; + ++rows; + } + } + return ANSI_PUKE_WHITE + + top + top_line + quit + + ANSI_END_LINE + + "│ " + K.spin() + " └───┤ " + coins + coins_line + "┘" + + ANSI_END_LINE + + K.logs(rows + 4, "│ ") + + data; +}; diff --git a/src/bin/trading-bot/trading-bot.test.h b/src/bin/trading-bot/trading-bot.test.h new file mode 100644 index 000000000..034c335ed --- /dev/null +++ b/src/bin/trading-bot/trading-bot.test.h @@ -0,0 +1,648 @@ +SCENARIO_METHOD(TradingBot, "ANY BTC/EUR") { + gateway = Gw::new_Gw("ANY"); + gateway->exchange = "ANY"; + gateway->base = "BTC"; + gateway->quote = "EUR"; + gateway->tickPrice = + gateway->tickSize = + gateway->minSize = 1e-2; + gateway->decimal.price.precision(gateway->tickPrice); + gateway->decimal.amount.precision(gateway->tickSize); + GIVEN("MarketLevels") { + WHEN("defaults") { + THEN("fair value") { + REQUIRE_FALSE(engine.levels.fairValue); + REQUIRE_NOTHROW(engine.levels.fairPrice.read = [&]() { + REQUIRE(engine.levels.fairPrice.blob().dump() == "{\"price\":0.0}"); + }); + REQUIRE_FALSE(engine.levels.ready()); + REQUIRE_FALSE(engine.levels.fairValue); + } + } + WHEN("assigned") { + for (Order *const it : engine.orders.open()) + engine.orders.purge(it); + REQUIRE_NOTHROW(engine.levels.diff.read = [&]() { + REQUIRE(engine.levels.diff.blob().dump() == "{" + "\"asks\":[{\"price\":1234.6,\"size\":1.23456789},{\"price\":1234.69,\"size\":0.11234569}]," + "\"bids\":[{\"price\":1234.5,\"size\":0.12345678},{\"price\":1234.55,\"size\":0.01234567}]" + "}"); + }); + REQUIRE_NOTHROW(engine.levels.fairPrice.read = [&]() { + REQUIRE(engine.levels.fairPrice.blob().dump() == "{\"price\":1234.55}"); + }); + REQUIRE_NOTHROW(engine.qp.fvModel = tribeca::FairValueModel::BBO); + vector randIds; + REQUIRE_NOTHROW(randIds.push_back(Random::uuid36Id())); + REQUIRE_NOTHROW(engine.orders.update({"", (Side)0, 0, 0, Tstamp, false, randIds.back(), "", Status::Working, 0})); + REQUIRE_NOTHROW(randIds.push_back(Random::uuid36Id())); + REQUIRE_NOTHROW(engine.orders.update({"", Side::Bid, 1234.52, 0.23456789, Tstamp, false, randIds.back()})); + REQUIRE_NOTHROW(engine.orders.update({"", (Side)0, 0, 0, Tstamp, false, randIds.back(), "", Status::Working, 0})); + REQUIRE_NOTHROW(randIds.push_back(Random::uuid36Id())); + REQUIRE_NOTHROW(engine.orders.update({"", Side::Bid, 1234.55, 0.01234567, Tstamp, false, randIds.back()})); + REQUIRE_NOTHROW(engine.orders.update({"", (Side)0, 0, 0, Tstamp, false, randIds.back(), "", Status::Working, 0})); + REQUIRE_NOTHROW(randIds.push_back(Random::uuid36Id())); + REQUIRE_NOTHROW(engine.orders.update({"", Side::Ask, 1234.69, 0.01234568, Tstamp, false, randIds.back()})); + REQUIRE_NOTHROW(engine.orders.update({"", (Side)0, 0, 0, Tstamp, false, randIds.back(), "", Status::Working, 0})); + REQUIRE_NOTHROW(engine.levels.read_from_gw({ + { {1234.50, 0.12345678}, {1234.55, 0.01234567} }, + { {1234.60, 1.23456789}, {1234.69, 0.11234569} } + })); + THEN("filters") { + REQUIRE(engine.levels.bids.size() == 1); + REQUIRE(engine.levels.bids[0].price == 1234.50); + REQUIRE(engine.levels.bids[0].size == 0.12345678); + REQUIRE(engine.levels.asks.size() == 2); + REQUIRE(engine.levels.asks[0].price == 1234.60); + REQUIRE(engine.levels.asks[0].size == 1.23456789); + REQUIRE(engine.levels.asks[1].price == 1234.69); + REQUIRE(engine.levels.asks[1].size == 0.10000001); + REQUIRE(engine.levels.unfiltered.bids.size() == 2); + REQUIRE(engine.levels.unfiltered.bids[0].price == 1234.50); + REQUIRE(engine.levels.unfiltered.bids[0].size == 0.12345678); + REQUIRE(engine.levels.unfiltered.bids[1].price == 1234.55); + REQUIRE(engine.levels.unfiltered.bids[1].size == 0.01234567); + REQUIRE(engine.levels.unfiltered.asks.size() == 2); + REQUIRE(engine.levels.unfiltered.asks[0].price == 1234.60); + REQUIRE(engine.levels.unfiltered.asks[0].size == 1.23456789); + REQUIRE(engine.levels.unfiltered.asks[1].price == 1234.69); + REQUIRE(engine.levels.unfiltered.asks[1].size == 0.11234569); + } + THEN("fair value") { + REQUIRE_NOTHROW(engine.levels.fairPrice.read = []() { + FAIL("broadcast() while filtering"); + }); + REQUIRE(engine.levels.ready()); + REQUIRE(engine.levels.fairValue == 1234.55); + } + THEN("fair value weight") { + REQUIRE_NOTHROW(engine.qp.fvModel = tribeca::FairValueModel::wBBO); + REQUIRE_NOTHROW(engine.levels.fairPrice.read = [&]() { + FAIL("broadcast() while filtering"); + }); + REQUIRE(engine.levels.ready()); + REQUIRE(engine.levels.fairValue == 1234.59); + } + THEN("fair value reversed weight") { + REQUIRE_NOTHROW(engine.qp.fvModel = tribeca::FairValueModel::rwBBO); + REQUIRE_NOTHROW(engine.levels.fairPrice.read = [&]() { + FAIL("broadcast() while filtering"); + }); + REQUIRE(engine.levels.ready()); + REQUIRE(engine.levels.fairValue == 1234.51); + } + WHEN("diff") { + REQUIRE(engine.levels.diff.hello().dump() == "[{" + "\"asks\":[{\"price\":1234.6,\"size\":1.23456789},{\"price\":1234.69,\"size\":0.11234569}]," + "\"bids\":[{\"price\":1234.5,\"size\":0.12345678},{\"price\":1234.55,\"size\":0.01234567}]" + "}]"); + REQUIRE_FALSE(engine.levels.diff.empty()); + THEN("broadcast") { + REQUIRE_NOTHROW(engine.qp.delayUI = 0); + struct timeval tv = {0, 370000}; + ::select(0, nullptr, nullptr, nullptr, &tv); + REQUIRE_NOTHROW(engine.levels.diff.read = [&]() { + REQUIRE(engine.levels.diff.blob().dump() == "{" + "\"asks\":[{\"price\":1234.69,\"size\":0.11234566}]," + "\"bids\":[{\"price\":1234.5},{\"price\":1234.4,\"size\":0.12345678}]," + "\"diff\":true" + "}"); + }); + REQUIRE_NOTHROW(engine.levels.fairPrice.read = [&]() { + REQUIRE(engine.levels.fairPrice.blob().dump() == "{\"price\":1234.5}"); + }); + REQUIRE_NOTHROW(engine.levels.read_from_gw({ + { {1234.40, 0.12345678}, {1234.55, 0.01234567} }, + { {1234.60, 1.23456789}, {1234.69, 0.11234566} } + })); + REQUIRE(engine.levels.diff.hello().dump() == "[{" + "\"asks\":[{\"price\":1234.6,\"size\":1.23456789},{\"price\":1234.69,\"size\":0.11234566}]," + "\"bids\":[{\"price\":1234.4,\"size\":0.12345678},{\"price\":1234.55,\"size\":0.01234567}]" + "}]"); + } + } + } + } + + GIVEN("RecentTrades") { + REQUIRE_NOTHROW(engine.wallet.safety.recentTrades.lastBuyPrice = 0); + REQUIRE_NOTHROW(engine.wallet.safety.recentTrades.lastSellPrice = 0); + REQUIRE_NOTHROW(engine.wallet.safety.recentTrades.buys = {}); + REQUIRE_NOTHROW(engine.wallet.safety.recentTrades.sells = {}); + REQUIRE_NOTHROW(engine.wallet.safety.recentTrades.sumBuys = 0); + REQUIRE_NOTHROW(engine.wallet.safety.recentTrades.sumSells = 0); + WHEN("defaults") { + THEN("empty") { + REQUIRE_FALSE(engine.wallet.safety.recentTrades.lastBuyPrice); + REQUIRE_FALSE(engine.wallet.safety.recentTrades.lastSellPrice); + REQUIRE_FALSE(engine.wallet.safety.recentTrades.buys.size()); + REQUIRE_FALSE(engine.wallet.safety.recentTrades.sells.size()); + REQUIRE_FALSE(engine.wallet.safety.recentTrades.sumBuys); + REQUIRE_FALSE(engine.wallet.safety.recentTrades.sumSells); + } + } + WHEN("assigned") { + Order order; + REQUIRE_NOTHROW(order.price = 1234.57); + REQUIRE_NOTHROW(order.qtyFilled = 0.01234566); + REQUIRE_NOTHROW(order.side = Side::Ask); + REQUIRE_NOTHROW(engine.wallet.safety.recentTrades.insert(order)); + REQUIRE_NOTHROW(order.price = 1234.58); + REQUIRE_NOTHROW(order.qtyFilled = 0.01234567); + REQUIRE_NOTHROW(engine.wallet.safety.recentTrades.insert(order)); + REQUIRE_NOTHROW(order.price = 1234.56); + REQUIRE_NOTHROW(order.qtyFilled = 0.12345678); + REQUIRE_NOTHROW(order.side = Side::Bid); + REQUIRE_NOTHROW(engine.wallet.safety.recentTrades.insert(order)); + REQUIRE_NOTHROW(order.price = 1234.50); + REQUIRE_NOTHROW(order.qtyFilled = 0.12345679); + REQUIRE_NOTHROW(engine.wallet.safety.recentTrades.insert(order)); + REQUIRE_NOTHROW(order.price = 1234.60); + REQUIRE_NOTHROW(order.qtyFilled = 0.12345678); + REQUIRE_NOTHROW(order.side = Side::Ask); + REQUIRE_NOTHROW(engine.wallet.safety.recentTrades.insert(order)); + THEN("values") { + REQUIRE(engine.wallet.safety.recentTrades.lastBuyPrice == 1234.50); + REQUIRE(engine.wallet.safety.recentTrades.lastSellPrice == 1234.60); + REQUIRE(engine.wallet.safety.recentTrades.buys.size() == 2); + REQUIRE(engine.wallet.safety.recentTrades.sells.size() == 3); + REQUIRE_FALSE(engine.wallet.safety.recentTrades.sumBuys); + REQUIRE_FALSE(engine.wallet.safety.recentTrades.sumSells); + WHEN("reset") { + THEN("skip") { + REQUIRE_NOTHROW(engine.wallet.safety.recentTrades.expire()); + REQUIRE(engine.wallet.safety.recentTrades.lastBuyPrice == 1234.50); + REQUIRE(engine.wallet.safety.recentTrades.lastSellPrice == 1234.60); + REQUIRE(engine.wallet.safety.recentTrades.buys.size() == 1); + REQUIRE_FALSE(engine.wallet.safety.recentTrades.sells.size()); + REQUIRE(engine.wallet.safety.recentTrades.sumBuys == 0.09876546); + REQUIRE_FALSE(engine.wallet.safety.recentTrades.sumSells); + } + THEN("expired") { + struct timeval tv = {1, 1000}; + ::select(0, nullptr, nullptr, nullptr, &tv); + REQUIRE_NOTHROW(engine.qp.tradeRateSeconds = 1); + REQUIRE_NOTHROW(engine.wallet.safety.recentTrades.expire()); + REQUIRE(engine.wallet.safety.recentTrades.lastBuyPrice == 1234.50); + REQUIRE(engine.wallet.safety.recentTrades.lastSellPrice == 1234.60); + REQUIRE_FALSE(engine.wallet.safety.recentTrades.buys.size()); + REQUIRE_FALSE(engine.wallet.safety.recentTrades.sells.size()); + REQUIRE_FALSE(engine.wallet.safety.recentTrades.sumBuys); + REQUIRE_FALSE(engine.wallet.safety.recentTrades.sumSells); + } + } + } + } + } + + GIVEN("Safety") { + REQUIRE_NOTHROW(engine.wallet.safety.read = [&]() { + INFO("read()"); + }); + + WHEN("calcSizes") { + REQUIRE_NOTHROW(engine.wallet.base = {1.0, 0}); + REQUIRE_NOTHROW(engine.wallet.quote = {1000.0, 0}); + REQUIRE_NOTHROW(engine.levels.fairValue = 500.0); + REQUIRE_NOTHROW(engine.wallet.base.value = 3.0); + REQUIRE_NOTHROW(engine.qp.percentageValues = true); + REQUIRE_NOTHROW(engine.qp.sellSizePercentage = 10.0); + REQUIRE_NOTHROW(engine.qp.buySizePercentage = 20.0); + REQUIRE_NOTHROW(engine.wallet.target.positionDivergence = 1.0); + + WHEN("raw values") { + REQUIRE_NOTHROW(engine.qp.percentageValues = false); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.01) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.02) == engine.wallet.safety.buySize); + } + + THEN("pct total value") { + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::Value); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.3) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.6) == engine.wallet.safety.buySize); + } + + THEN("pct side balance") { + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::Side); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.1) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.4) == engine.wallet.safety.buySize); + } + + THEN("with 0\% tbp") { + REQUIRE_NOTHROW(engine.wallet.target.targetBasePosition = 0.0); + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::TBPValue); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.3) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.0) == engine.wallet.safety.buySize); + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::TBPSide); + REQUIRE_NOTHROW(engine.qp.tradeSizeTBPExp = 1.0); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.1) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.0) == engine.wallet.safety.buySize); + REQUIRE_NOTHROW(engine.qp.tradeSizeTBPExp = 2.0); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.1/3) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.0) == engine.wallet.safety.buySize); + } + + THEN("with low tbp") { + REQUIRE_NOTHROW(engine.wallet.target.targetBasePosition = 0.5); + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::TBPValue); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.3) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.15) == engine.wallet.safety.buySize); + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::TBPSide); + REQUIRE_NOTHROW(engine.qp.tradeSizeTBPExp = 1.0); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.1) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.05) == engine.wallet.safety.buySize); + REQUIRE_NOTHROW(engine.qp.tradeSizeTBPExp = 2.0); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.1/3) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.0125/3) == engine.wallet.safety.buySize); + } + + THEN("with matched tbp") { + REQUIRE_NOTHROW(engine.wallet.target.targetBasePosition = 1.0); + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::TBPValue); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.3) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.4) == engine.wallet.safety.buySize); + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::TBPSide); + REQUIRE_NOTHROW(engine.qp.tradeSizeTBPExp = 1.0); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.1) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.2) == engine.wallet.safety.buySize); + REQUIRE_NOTHROW(engine.qp.tradeSizeTBPExp = 2.0); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.1/3) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.2/3) == engine.wallet.safety.buySize); + } + + THEN("with 50\% tbp") { + REQUIRE_NOTHROW(engine.wallet.target.targetBasePosition = 1.5); + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::TBPValue); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.3) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.6) == engine.wallet.safety.buySize); + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::TBPSide); + REQUIRE_NOTHROW(engine.qp.tradeSizeTBPExp = 1.0); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.06) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.36) == engine.wallet.safety.buySize); + REQUIRE_NOTHROW(engine.qp.tradeSizeTBPExp = 2.0); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.012) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.216) == engine.wallet.safety.buySize); + } + + THEN("with high tbp") { + REQUIRE_NOTHROW(engine.wallet.target.targetBasePosition = 2.0); + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::TBPValue); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.2) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.6) == engine.wallet.safety.buySize); + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::TBPSide); + REQUIRE_NOTHROW(engine.qp.tradeSizeTBPExp = 1.0); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.0) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.4) == engine.wallet.safety.buySize); + REQUIRE_NOTHROW(engine.qp.tradeSizeTBPExp = 2.0); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.0) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.8/3) == engine.wallet.safety.buySize); + } + + THEN("with 100\% tbp") { + REQUIRE_NOTHROW(engine.wallet.target.targetBasePosition = 3.0); + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::TBPValue); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.0) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.6) == engine.wallet.safety.buySize); + REQUIRE_NOTHROW(engine.qp.orderPctTotal = tribeca::OrderPctTotal::TBPSide); + REQUIRE_NOTHROW(engine.qp.tradeSizeTBPExp = 1.0); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.0) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.4) == engine.wallet.safety.buySize); + REQUIRE_NOTHROW(engine.qp.tradeSizeTBPExp = 2.0); + REQUIRE_NOTHROW(engine.wallet.safety.calc()); + REQUIRE(Approx(0.0) == engine.wallet.safety.sellSize); + REQUIRE(Approx(0.8/3) == engine.wallet.safety.buySize); + } + } + } + + GIVEN("Ewma") { + engine.levels.fairValue = 0; + WHEN("defaults") { + REQUIRE_FALSE(engine.levels.stats.ewma.mgEwmaM); + } + WHEN("assigned") { + vector fairHistory = { 268.05, 258.73, 239.82, 250.21, 224.49, 242.53, 248.25, 270.58, 252.77, 273.55, + 255.90, 226.10, 225.00, 263.12, 218.36, 254.73, 218.65, 252.40, 296.10, 222.20 }; + REQUIRE_NOTHROW(engine.levels.stats.ewma.fairValue96h.Backup::push = engine.levels.stats.ewma.Backup::push = [&]() { + INFO("push()"); + }); + for (const Price &it : fairHistory) { + REQUIRE_NOTHROW(engine.levels.fairValue = it); + REQUIRE_NOTHROW(engine.levels.stats.ewma.timer_60s(0)); + }; + REQUIRE_NOTHROW(engine.qp.mediumEwmaPeriods = 20); + REQUIRE_NOTHROW(engine.qp._diffEwma |= true << 0); + REQUIRE_NOTHROW(engine.qp._diffEwma |= true << 1); + REQUIRE_NOTHROW(engine.qp._diffEwma |= true << 2); + REQUIRE_NOTHROW(engine.qp._diffEwma |= true << 3); + REQUIRE_NOTHROW(engine.qp._diffEwma |= true << 4); + REQUIRE_NOTHROW(engine.qp._diffEwma |= true << 5); + REQUIRE_NOTHROW(K.clicked(&engine.qp)); + THEN("values") { + REQUIRE(engine.levels.stats.ewma.mgEwmaVL == Approx(266.1426832796)); + REQUIRE(engine.levels.stats.ewma.mgEwmaL == Approx(264.4045182289)); + REQUIRE(engine.levels.stats.ewma.mgEwmaM == Approx(261.3765619062)); + REQUIRE(engine.levels.stats.ewma.mgEwmaS == Approx(256.7706209412)); + REQUIRE(engine.levels.stats.ewma.mgEwmaXS == Approx(247.5567169778)); + REQUIRE(engine.levels.stats.ewma.mgEwmaU == Approx(245.5969655991)); + REQUIRE(engine.levels.stats.ewma.lifetime() == 24000000); + THEN("to json") { + REQUIRE(((json)engine.levels.stats.ewma).dump() == "{" + "\"ewmaExtraShort\":247.55671697778087," + "\"ewmaLong\":264.40451822891674," + "\"ewmaMedium\":261.3765619061748," + "\"ewmaQuote\":264.40451822891674," + "\"ewmaShort\":256.7706209412057," + "\"ewmaTrendDiff\":-0.7916373276580089," + "\"ewmaUltraShort\":245.59696559906004," + "\"ewmaVeryLong\":266.142683279562," + "\"ewmaWidth\":0.0" + "}"); + } + } + } + } + + GIVEN("Broker") { + REQUIRE_NOTHROW(engine.qp.mode = tribeca::QuotingMode::Top); + REQUIRE_NOTHROW(engine.qp.autoPositionMode = tribeca::AutoPositionMode::Manual); + REQUIRE_NOTHROW(engine.qp.aggressivePositionRebalancing = tribeca::APR::Off); + REQUIRE_NOTHROW(engine.qp.safety = tribeca::QuotingSafety::Off); + REQUIRE_NOTHROW(engine.qp.protectionEwmaQuotePrice = false); + REQUIRE_NOTHROW(engine.qp.widthPercentage = false); + REQUIRE_NOTHROW(engine.qp.percentageValues = false); + REQUIRE_NOTHROW(engine.qp.widthPing = 1); + REQUIRE_NOTHROW(engine.qp.bestWidth = true); + REQUIRE_NOTHROW(engine.qp.protectionEwmaWidthPing = false); + REQUIRE_NOTHROW(engine.qp.targetBasePosition = 1); + REQUIRE_NOTHROW(engine.qp.positionDivergence = 1); + REQUIRE_NOTHROW(engine.qp.read = engine.levels.diff.read = engine.levels.fairPrice.read = engine.wallet.read = engine.wallet.safety.read = engine.wallet.target.read = engine.broker.read = engine.broker.semaphore.read = [&]() { + INFO("read()"); + }); + REQUIRE_NOTHROW(engine.qp.Backup::push = engine.wallet.target.Backup::push = engine.wallet.profits.Backup::push = [&]() { + INFO("push()"); + }); + REQUIRE_NOTHROW(engine.broker.semaphore.read_from_gw( + Connectivity::Disconnected + )); + REQUIRE_NOTHROW(engine.levels.fairValue = 500); + REQUIRE_NOTHROW(engine.wallet.read_from_gw({1, 0, "BTC"})); + REQUIRE_NOTHROW(engine.wallet.read_from_gw({1000, 0, "EUR"})); + WHEN("assigned") { + for (Order *const it : engine.orders.open()) + engine.orders.purge(it); + vector randIds; + const Clock time = Tstamp; + REQUIRE_NOTHROW(randIds.push_back(Random::uuid36Id())); + REQUIRE_NOTHROW(engine.orders.update({"BTC-EUR", Side::Bid, 1234.50, 0.12345678, time-69, false, randIds.back()})); + REQUIRE_NOTHROW(engine.orders.update({"BTC-EUR", (Side)0, 0, 0, time, false, randIds.back(), "", Status::Working, 0})); + REQUIRE_NOTHROW(randIds.push_back(Random::uuid36Id())); + REQUIRE_NOTHROW(engine.orders.update({"BTC-EUR", Side::Bid, 1234.51, 0.12345679, time-69, false, randIds.back()})); + REQUIRE_NOTHROW(engine.orders.update({"BTC-EUR", (Side)0, 0, 0, time, false, randIds.back(), "", Status::Working, 0})); + REQUIRE_NOTHROW(randIds.push_back(Random::uuid36Id())); + REQUIRE_NOTHROW(engine.orders.update({"BTC-EUR", Side::Bid, 1234.52, 0.12345680, time-69, false, randIds.back()})); + REQUIRE_NOTHROW(engine.orders.update({"BTC-EUR", (Side)0, 0, 0, time, false, randIds.back(), "", Status::Working, 0})); + REQUIRE_NOTHROW(randIds.push_back(Random::uuid36Id())); + REQUIRE_NOTHROW(engine.orders.update({"BTC-EUR", Side::Ask, 1234.50, 0.12345678, time-69, false, randIds.back()})); + REQUIRE_NOTHROW(engine.orders.update({"BTC-EUR", (Side)0, 0, 0, time, false, randIds.back(), "", Status::Working, 0})); + REQUIRE_NOTHROW(randIds.push_back(Random::uuid36Id())); + REQUIRE_NOTHROW(engine.orders.update({"BTC-EUR", Side::Ask, 1234.51, 0.12345679, time-69, false, randIds.back()})); + REQUIRE_NOTHROW(engine.orders.update({"BTC-EUR", (Side)0, 0, 0, time, false, randIds.back(), "", Status::Working, 0})); + REQUIRE_NOTHROW(randIds.push_back(Random::uuid36Id())); + REQUIRE_NOTHROW(engine.orders.update({"BTC-EUR", Side::Ask, 1234.52, 0.12345680, time, false, randIds.back()})); + THEN("held amount") { + Order order; + engine.orders.last = ℴ + REQUIRE_NOTHROW(engine.orders.last->price = 1); + REQUIRE_NOTHROW(engine.orders.last->side = Side::Ask); + REQUIRE_NOTHROW(engine.wallet.calcFundsAfterOrder()); + REQUIRE_NOTHROW(engine.orders.last->side = Side::Bid); + REQUIRE_NOTHROW(engine.wallet.calcFundsAfterOrder()); + REQUIRE(engine.wallet.base.held == 0.37037037); + REQUIRE(engine.wallet.quote.held == Approx(457.22592546)); + } + THEN("to json") { + REQUIRE(string::npos == engine.orders.blob().dump().find("\"status\":0")); + REQUIRE(string::npos == engine.orders.blob().dump().find("\"status\":2")); + REQUIRE(string::npos != engine.orders.blob().dump().find("{\"exchangeId\":\"\",\"isPong\":false,\"latency\":69,\"orderId\":\"" + randIds[0] + "\",\"price\":1234.5,\"pricePrecision\":0.0,\"quantity\":0.12345678,\"quantityPrecision\":0.0,\"side\":0,\"status\":1,\"symbol\":\"BTC-EUR\",\"time\":" + to_string(time) + ",\"timeInForce\":0,\"type\":0}")); + REQUIRE(string::npos != engine.orders.blob().dump().find("{\"exchangeId\":\"\",\"isPong\":false,\"latency\":69,\"orderId\":\"" + randIds[1] + "\",\"price\":1234.51,\"pricePrecision\":0.0,\"quantity\":0.12345679,\"quantityPrecision\":0.0,\"side\":0,\"status\":1,\"symbol\":\"BTC-EUR\",\"time\":" + to_string(time) + ",\"timeInForce\":0,\"type\":0}")); + REQUIRE(string::npos != engine.orders.blob().dump().find("{\"exchangeId\":\"\",\"isPong\":false,\"latency\":69,\"orderId\":\"" + randIds[2] + "\",\"price\":1234.52,\"pricePrecision\":0.0,\"quantity\":0.1234568,\"quantityPrecision\":0.0,\"side\":0,\"status\":1,\"symbol\":\"BTC-EUR\",\"time\":" + to_string(time) + ",\"timeInForce\":0,\"type\":0}")); + REQUIRE(string::npos != engine.orders.blob().dump().find("{\"exchangeId\":\"\",\"isPong\":false,\"latency\":69,\"orderId\":\"" + randIds[3] + "\",\"price\":1234.5,\"pricePrecision\":0.0,\"quantity\":0.12345678,\"quantityPrecision\":0.0,\"side\":1,\"status\":1,\"symbol\":\"BTC-EUR\",\"time\":" + to_string(time) + ",\"timeInForce\":0,\"type\":0}")); + REQUIRE(string::npos != engine.orders.blob().dump().find("{\"exchangeId\":\"\",\"isPong\":false,\"latency\":69,\"orderId\":\"" + randIds[4] + "\",\"price\":1234.51,\"pricePrecision\":0.0,\"quantity\":0.12345679,\"quantityPrecision\":0.0,\"side\":1,\"status\":1,\"symbol\":\"BTC-EUR\",\"time\":" + to_string(time) + ",\"timeInForce\":0,\"type\":0}")); + } + } + WHEN("ready") { + REQUIRE_NOTHROW(engine.levels.read_from_gw({ + { }, + { } + })); + REQUIRE_NOTHROW(engine.broker.semaphore.click({ + {"agree", 0} + })); + REQUIRE_NOTHROW(engine.broker.quotes.bid.skip(QuoteState::UnknownReason)); + REQUIRE_NOTHROW(engine.broker.quotes.ask.skip(QuoteState::UnknownReason)); + REQUIRE(engine.broker.quotes.bid.empty()); + REQUIRE(engine.broker.quotes.ask.empty()); + REQUIRE_FALSE(engine.broker.ready()); + REQUIRE(engine.broker.quotes.bid.state == QuoteState::Disconnected); + REQUIRE(engine.broker.quotes.ask.state == QuoteState::Disconnected); + REQUIRE_NOTHROW(engine.orders.zombies.purge()); + REQUIRE_NOTHROW(engine.broker.semaphore.read_from_gw( + Connectivity::Connected + )); + REQUIRE(engine.broker.ready()); + REQUIRE_FALSE(engine.levels.ready()); + REQUIRE_NOTHROW(engine.levels.read_from_gw({ { + {699, 0.12345678}, + {698, 0.12345678}, + {696, 0.12345678} + }, { + {701, 0.12345678}, + {702, 0.12345678}, + {704, 0.12345678} + } })); + REQUIRE(engine.levels.fairValue == 700); + REQUIRE(engine.levels.ready()); + REQUIRE_NOTHROW(engine.wallet.read_from_gw({0, 0, "BTC"})); + REQUIRE_NOTHROW(engine.wallet.read_from_gw({0, 0, "EUR"})); + REQUIRE_FALSE(engine.wallet.ready()); + REQUIRE_NOTHROW(engine.wallet.read_from_gw({1, 0, "BTC"})); + REQUIRE_NOTHROW(engine.wallet.read_from_gw({1000, 0, "EUR"})); + REQUIRE_NOTHROW(engine.wallet.safety.timer_1s()); + REQUIRE(engine.wallet.ready()); + REQUIRE_NOTHROW(engine.broker.calcQuotes()); + REQUIRE(engine.broker.quotes.ask.empty()); + REQUIRE(engine.broker.quotes.bid.empty()); + THEN("agree") { + REQUIRE(engine.broker.ready()); + REQUIRE_NOTHROW(engine.qp.click(engine.qp)); + REQUIRE_NOTHROW(engine.broker.semaphore.click({ + {"agree", 1} + })); + WHEN("quoting") { + REQUIRE_NOTHROW(engine.broker.quotes.calcQuotes()); + REQUIRE_FALSE(engine.broker.quotes.bid.empty()); + REQUIRE_FALSE(engine.broker.quotes.ask.empty()); + THEN("to json") { + REQUIRE(((json)engine.broker.quotes.bid).dump() == "{" + "\"price\":699.01," + "\"size\":0.02" + "}"); + REQUIRE(((json)engine.broker.quotes.ask).dump() == "{" + "\"price\":700.99," + "\"size\":0.01" + "}"); + } + WHEN("widthPing=2") { + REQUIRE_NOTHROW(engine.qp.widthPing = 2); + REQUIRE_NOTHROW(engine.broker.quotes.calcQuotes()); + REQUIRE_FALSE(engine.broker.quotes.bid.empty()); + REQUIRE_FALSE(engine.broker.quotes.ask.empty()); + THEN("to json") { + REQUIRE(((json)engine.broker.quotes.bid).dump() == "{" + "\"price\":698.01," + "\"size\":0.02" + "}"); + REQUIRE(((json)engine.broker.quotes.ask).dump() == "{" + "\"price\":701.99," + "\"size\":0.01" + "}"); + } + } + WHEN("widthPing=3,bestWidth=false") { + REQUIRE_NOTHROW(engine.qp.bestWidth = false); + REQUIRE_NOTHROW(engine.qp.widthPing = 3); + REQUIRE_NOTHROW(engine.broker.quotes.calcQuotes()); + REQUIRE_FALSE(engine.broker.quotes.bid.empty()); + REQUIRE_FALSE(engine.broker.quotes.ask.empty()); + THEN("to json") { + REQUIRE(((json)engine.broker.quotes.bid).dump() == "{" + "\"price\":698.5," + "\"size\":0.02" + "}"); + REQUIRE(((json)engine.broker.quotes.ask).dump() == "{" + "\"price\":701.5," + "\"size\":0.01" + "}"); + } + } + WHEN("widthPing=3") { + REQUIRE_NOTHROW(engine.qp.widthPing = 3); + REQUIRE_NOTHROW(engine.broker.quotes.calcQuotes()); + REQUIRE_FALSE(engine.broker.quotes.bid.empty()); + REQUIRE_FALSE(engine.broker.quotes.ask.empty()); + THEN("to json") { + REQUIRE(((json)engine.broker.quotes.bid).dump() == "{" + "\"price\":698.01," + "\"size\":0.02" + "}"); + REQUIRE(((json)engine.broker.quotes.ask).dump() == "{" + "\"price\":701.99," + "\"size\":0.01" + "}"); + } + } + WHEN("widthPing=4") { + REQUIRE_NOTHROW(engine.qp.widthPing = 4); + REQUIRE_NOTHROW(engine.broker.quotes.calcQuotes()); + REQUIRE_FALSE(engine.broker.quotes.bid.empty()); + REQUIRE_FALSE(engine.broker.quotes.ask.empty()); + THEN("to json") { + REQUIRE(((json)engine.broker.quotes.bid).dump() == "{" + "\"price\":696.01," + "\"size\":0.02" + "}"); + REQUIRE(((json)engine.broker.quotes.ask).dump() == "{" + "\"price\":703.99," + "\"size\":0.01" + "}"); + } + } + } + } + } + } + + GIVEN("Issue #909 Corrupt Tradehistory Found") { + engine.qp.click(R"({"aggressivePositionRebalancing":1,"aprMultiplier":3.0,"audio":false,"autoPositionMode":0,"bestWidth":true,"bestWidthSize":0.0,"bullets":2,"buySize":0.02,"buySizeMax":false,"buySizePercentage":1.0,"cancelOrdersAuto":false,"cleanPongsAuto":0.0,"delayUI":3,"ewmaSensiblityPercentage":0.5,"extraShortEwmaPeriods":12,"fvModel":0,"lifetime":0,"localBalance":true,"longEwmaPeriods":200,"mediumEwmaPeriods":100,"mode":0,"percentageValues":true,"pingAt":0,"pongAt":1,"positionDivergence":0.9,"positionDivergenceMin":0.4,"positionDivergenceMode":0,"positionDivergencePercentage":50.0,"positionDivergencePercentageMin":10.0,"profitHourInterval":72.0,"protectionEwmaPeriods":5,"protectionEwmaQuotePrice":false,"protectionEwmaWidthPing":false,"quotingEwmaTrendProtection":false,"quotingEwmaTrendThreshold":2.0,"quotingStdevBollingerBands":false,"quotingStdevProtection":0,"quotingStdevProtectionFactor":1.0,"quotingStdevProtectionPeriods":1200,"range":0.5,"rangePercentage":5.0,"safety":3,"sellSize":0.01,"sellSizeMax":false,"sellSizePercentage":1.0,"shortEwmaPeriods":50,"sopSizeMultiplier":2.0,"sopTradesMultiplier":2.0,"sopWidthMultiplier":2.0,"superTrades":0,"targetBasePosition":1.0,"targetBasePositionPercentage":50.0,"tradeRateSeconds":3,"tradesPerMinute":1,"ultraShortEwmaPeriods":3,"veryLongEwmaPeriods":400,"widthPercentage":false,"widthPing":0.01,"widthPingPercentage":0.25,"widthPong":0.01,"widthPongPercentage":0.25})"_json); + REQUIRE_NOTHROW(engine.wallet.safety.trades.Backup::push = [&]() { + INFO("push()"); + }); + REQUIRE_NOTHROW(engine.wallet.safety.trades.read = [&]() { + INFO("read()"); + }); + auto parseTrade = [](string line) { + stringstream ss(line); + string _, pingpong, side; + Order order; + ss >> _ >> _ >> _ >> _ >> pingpong >> _ >> side >> order.qtyFilled >> _ >> _ >> _ >> order.price; + order.side = (side == "BUY" ? Side::Bid : Side::Ask); + order.isPong = (pingpong == "PONG"); + return order; + }; + vector loglines({ + parseTrade("03/30 07:10:34.800532 GW COINBASE PING TRADE BUY 0.03538069 BTC at price 141.31 EUR (value 4.99 EUR)."), + parseTrade("03/30 07:12:10.769009 GW COINBASE PONG TRADE SELL 0.03241380 BTC at price 141.40 EUR (value 4.58 EUR)."), + parseTrade("03/30 07:12:10.786990 GW COINBASE PONG TRADE SELL 0.00295204 BTC at price 141.40 EUR (value 0.41 EUR)."), + parseTrade("03/30 07:25:49.333540 GW COINBASE PING TRADE BUY 0.03528853 BTC at price 141.60 EUR (value 4.99 EUR)."), + parseTrade("03/30 07:25:50.787607 GW COINBASE PONG TRADE SELL 0.03528853 BTC at price 141.66 EUR (value 4.99 EUR)."), + parseTrade("03/30 07:38:07.369008 GW COINBASE PING TRADE BUY 0.03528867 BTC at price 141.66 EUR (value 4.99 EUR)."), + parseTrade("03/30 07:38:34.268582 GW COINBASE PONG TRADE SELL 0.03529119 BTC at price 141.68 EUR (value 5.00 EUR)."), + parseTrade("03/30 07:43:02.229028 GW COINBASE PING TRADE BUY 0.03528870 BTC at price 141.63 EUR (value 4.99 EUR)."), + parseTrade("03/30 07:45:22.735730 GW COINBASE PING TRADE SELL 0.03530102 BTC at price 141.55 EUR (value 4.99 EUR)."), + parseTrade("03/30 08:14:52.978466 GW COINBASE PING TRADE BUY 0.03512242 BTC at price 142.28 EUR (value 4.99 EUR)."), + parseTrade("03/30 08:15:13.002363 GW COINBASE PING TRADE BUY 0.03515685 BTC at price 142.22 EUR (value 5.00 EUR).") + }); + Amount expectedBaseDelta = 0; + Amount expectedQuoteDelta = 0; + Amount baseSign; + WHEN("cumulated cross pongs") { + for (const auto &order : loglines) { + baseSign = (order.side == Side::Bid) ? 1 : -1; + expectedBaseDelta += baseSign * order.qtyFilled; + expectedQuoteDelta -= baseSign * order.qtyFilled * order.price; + struct timeval tv = {0, 2000}; + ::select(0, nullptr, nullptr, nullptr, &tv); + engine.wallet.safety.trades.insert(order); + Amount actualBaseDelta = 0; + Amount actualQuoteDelta = 0; + Amount expectedDelta = 0; + Amount actualDelta = 0; + for (const auto &trade : engine.wallet.safety.trades) { + baseSign = (trade.side == Side::Bid) ? 1 : -1; + actualBaseDelta += baseSign * (trade.quantity - trade.Kqty); + Amount delta = baseSign * (trade.Kvalue - trade.value); + actualQuoteDelta += delta; + if (trade.delta) { + actualDelta += delta; + expectedDelta += trade.delta; + REQUIRE(trade.delta > 0); + } + } + REQUIRE(abs(actualBaseDelta - expectedBaseDelta) < 0.000000000001); + REQUIRE(abs(actualQuoteDelta - expectedQuoteDelta) < 0.000000000001); + REQUIRE(abs(actualDelta - expectedDelta) < 0.000000000001); + } + } + } +} diff --git a/src/common/messaging.ts b/src/common/messaging.ts deleted file mode 100644 index 18ba95e00..000000000 --- a/src/common/messaging.ts +++ /dev/null @@ -1,257 +0,0 @@ -/// -/// - -import Models = require("./models"); - -module Prefixes { - export var SUBSCRIBE = "u"; - export var SNAPSHOT = "n"; - export var MESSAGE = "m"; -} - -export interface IPublish { - publish : (msg : T) => void; - registerSnapshot : (generator : () => T[]) => IPublish; -} - -export class Publisher implements IPublish { - private _snapshot : () => T[] = null; - constructor(private topic : string, - private _io : SocketIO.Server, - snapshot : () => T[], - private _log : (...args: any[]) => void) { - this.registerSnapshot(snapshot || null); - - var onConnection = s => { - this._log("socket", s.id, "connected for Publisher", topic); - - s.on("disconnect", () => { - this._log("socket", s.id, "disconnected for Publisher", topic); - }); - - s.on(Prefixes.SUBSCRIBE + "-" + topic, () => { - if (this._snapshot !== null) { - var snapshot = this._snapshot(); - this._log("socket", s.id, "asking for snapshot on topic", topic); - s.emit(Prefixes.SNAPSHOT + "-" + topic, snapshot); - } - }); - }; - - this._io.on("connection", onConnection); - - Object.keys(this._io.sockets.connected).forEach(s => { - onConnection(this._io.sockets.connected[s]); - }); - } - - public publish = (msg : T) => this._io.emit(Prefixes.MESSAGE + "-" + this.topic, msg); - - public registerSnapshot = (generator : () => T[]) => { - if (this._snapshot === null) { - this._snapshot = generator; - } - else { - throw new Error("already registered snapshot generator for topic " + this.topic); - } - - return this; - } -} - -export class NullPublisher implements IPublish { - public publish = (msg : T) => {}; - public registerSnapshot = (generator : () => T[]) => this; -} - -export interface ISubscribe { - registerSubscriber : (incrementalHandler : (msg : T) => void, snapshotHandler : (msgs : T[]) => void) => ISubscribe; - registerDisconnectedHandler : (handler : () => void) => ISubscribe; - registerConnectHandler : (handler : () => void) => ISubscribe; - connected: boolean; - disconnect : () => void; -} - -export class Subscriber implements ISubscribe { - private _incrementalHandler : (msg : T) => void = null; - private _snapshotHandler : (msgs : T[]) => void = null; - private _disconnectHandler : () => void = null; - private _connectHandler : () => void = null; - private _socket : SocketIOClient.Socket; - - constructor(private topic : string, - io : SocketIOClient.Socket, - private _log : (...args: any[]) => void) { - this._socket = io; - - this._log("creating subscriber to", this.topic, "; connected?", this.connected); - - if (this.connected) - this.onConnect(); - - this._socket.on("connect", this.onConnect) - .on("disconnect", this.onDisconnect) - .on(Prefixes.MESSAGE + "-" + topic, this.onIncremental) - .on(Prefixes.SNAPSHOT + "-" + topic, this.onSnapshot); - } - - public get connected() : boolean { - return this._socket.connected; - } - - private onConnect = () => { - this._log("connect to", this.topic); - if (this._connectHandler !== null) { - this._connectHandler(); - } - - this._socket.emit(Prefixes.SUBSCRIBE + "-" + this.topic); - }; - - private onDisconnect = () => { - this._log("disconnected from", this.topic); - if (this._disconnectHandler !== null) - this._disconnectHandler(); - }; - - private onIncremental = (m : T) => { - if (this._incrementalHandler !== null) - this._incrementalHandler(m); - }; - - private onSnapshot = (msgs : T[]) => { - this._log("handling snapshot for", this.topic, "nMsgs:", msgs.length); - if (this._snapshotHandler !== null) - this._snapshotHandler(msgs); - }; - - public disconnect = () => { - this._log("forcing disconnection from ", this.topic); - this._socket.off("connect", this.onConnect); - this._socket.off("disconnect", this.onDisconnect); - this._socket.off(Prefixes.MESSAGE + "-" + this.topic, this.onIncremental); - this._socket.off(Prefixes.SNAPSHOT + "-" + this.topic, this.onSnapshot); - }; - - public registerSubscriber = (incrementalHandler : (msg : T) => void, snapshotHandler : (msgs : T[]) => void) => { - if (this._incrementalHandler === null) { - this._incrementalHandler = incrementalHandler; - } - else { - throw new Error("already registered incremental handler for topic " + this.topic); - } - - if (this._snapshotHandler === null) { - this._snapshotHandler = snapshotHandler; - } - else { - throw new Error("already registered snapshot handler for topic " + this.topic); - } - - return this; - }; - - public registerDisconnectedHandler = (handler : () => void) => { - if (this._disconnectHandler === null) { - this._disconnectHandler = handler; - } - else { - throw new Error("already registered disconnect handler for topic " + this.topic); - } - - return this; - }; - - public registerConnectHandler = (handler : () => void) => { - if (this._connectHandler === null) { - this._connectHandler = handler; - } - else { - throw new Error("already registered connect handler for topic " + this.topic); - } - - return this; - }; -} - -export interface IFire { - fire(msg : T) : void; -} - -export class Fire implements IFire { - private _socket : SocketIOClient.Socket; - - constructor(private topic : string, io : SocketIOClient.Socket, _log : (...args: any[]) => void) { - this._socket = io; - this._socket.on("connect", () => _log("Fire connected to", this.topic)) - .on("disconnect", () => _log("Fire disconnected to", this.topic)); - } - - public fire = (msg : T) : void => { - this._socket.emit(Prefixes.MESSAGE + "-" + this.topic, msg); - }; -} - -export interface IReceive { - registerReceiver(handler : (msg : T) => void) : void; -} - -export class NullReceiver implements IReceive { - registerReceiver = (handler : (msg : T) => void) => {}; -} - -export class Receiver implements IReceive { - private _handler : (msg : T) => void = null; - constructor(private topic : string, io : SocketIO.Server, - private _log : (...args: any[]) => void) { - var onConnection = (s : SocketIO.Socket) => { - this._log("socket", s.id, "connected for Receiver", topic); - s.on(Prefixes.MESSAGE + "-" + this.topic, msg => { - if (this._handler !== null) - this._handler(msg); - }); - s.on("error", e => { - _log("error in Receiver", e.stack, e.message); - }); - }; - - io.on("connection", onConnection); - Object.keys(io.sockets.connected).forEach(s => { - onConnection(io.sockets.connected[s]); - }); - } - - registerReceiver = (handler : (msg : T) => void) => { - if (this._handler === null) { - this._handler = handler; - } - else { - throw new Error("already registered receive handler for topic " + this.topic); - } - }; -} - -export class Topics { - static FairValue = "fv"; - static Quote = "q"; - static ActiveSubscription = "a"; - static ActiveChange = "ac"; - static MarketData = "md"; - static QuotingParametersChange = "qp-sub"; - static SafetySettings = "ss"; - static Product = "p"; - static OrderStatusReports = "osr"; - static ProductAdvertisement = "pa"; - static Position = "pos"; - static ExchangeConnectivity = "ec"; - static SubmitNewOrder = "sno"; - static CancelOrder = "cxl"; - static MarketTrade = "mt"; - static Trades = "t"; - static Message = "msg"; - static ExternalValuation = "ev"; - static QuoteStatus = "qs"; - static TargetBasePosition = "tbp"; - static TradeSafetyValue = "tsv"; - static CancelAllOrders = "cao"; -} diff --git a/src/common/models.ts b/src/common/models.ts deleted file mode 100644 index 5c583078b..000000000 --- a/src/common/models.ts +++ /dev/null @@ -1,396 +0,0 @@ -/// - -export interface ITimestamped { - time : moment.Moment; -} - -export class Timestamped implements ITimestamped { - constructor(public data: T, public time: moment.Moment) {} - - public toString() { - return "time=" + toUtcFormattedTime(this.time) + ";data=" + this.data; - } -} - -export class MarketSide { - constructor(public price: number, - public size: number) { } - - public toString() { - return "px=" + this.price + ";size=" + this.size; - } -} - -export class GatewayMarketTrade implements ITimestamped { - constructor(public price: number, - public size: number, - public time: moment.Moment, - public onStartup: boolean, - public make_side: Side) { } -} - -export function marketSideEquals(t: MarketSide, other: MarketSide, tol?: number) { - tol = tol || 1e-4; - if (other == null) return false; - return Math.abs(t.price - other.price) > tol && Math.abs(t.size - other.size) > tol; -} - -export class Market implements ITimestamped { - constructor(public bids: MarketSide[], - public asks: MarketSide[], - public time: moment.Moment) { } - - public toString() { - return "asks: [" + this.asks.join(";") + "] bids: [" + this.bids.join(";") + "]"; - } -} - -export class MarketTrade implements ITimestamped { - constructor(public exchange: Exchange, - public pair: CurrencyPair, - public price: number, - public size: number, - public time: moment.Moment, - public quote: TwoSidedQuote, - public bid: MarketSide, - public ask: MarketSide, - public make_side: Side) {} -} - -export enum GatewayType { MarketData, OrderEntry, Position } -export enum Currency { USD, BTC, LTC, EUR, GBP, CNY } -export enum ConnectivityStatus { Connected, Disconnected } -export enum Exchange { Null, HitBtc, OkCoin, AtlasAts, BtcChina, Coinbase, Bitfinex } -export enum Side { Bid, Ask, Unknown } -export enum OrderType { Limit, Market } -export enum TimeInForce { IOC, FOK, GTC } -export enum OrderStatus { New, Working, Complete, Cancelled, Rejected, Other } -export enum Liquidity { Make, Take } - -export enum MarketDataFlag { - Unknown = 0, - NoChange = 1, - First = 1 << 1, - PriceChanged = 1 << 2, - SizeChanged = 1 << 3, - PriceAndSizeChanged = 1 << 4 -} - -export interface Order { - side : Side; - quantity : number; - type : OrderType; - price : number; - timeInForce : TimeInForce; - exchange : Exchange; -} - -export class SubmitNewOrder implements Order { - constructor(public side: Side, - public quantity: number, - public type: OrderType, - public price: number, - public timeInForce: TimeInForce, - public exchange: Exchange, - public generatedTime: moment.Moment, - public preferPostOnly: boolean, - public msg?: string) { - this.msg = msg || null; - } -} - -export class CancelReplaceOrder { - constructor(public origOrderId: string, - public quantity: number, - public price: number, - public exchange: Exchange, - public generatedTime: moment.Moment) {} -} - -export class OrderCancel { - constructor(public origOrderId: string, - public exchange: Exchange, - public generatedTime: moment.Moment) {} -} - -export class BrokeredOrder implements Order { - constructor(public orderId: string, - public side: Side, - public quantity: number, - public type: OrderType, - public price: number, - public timeInForce: TimeInForce, - public exchange: Exchange, - public preferPostOnly: boolean) {} -} - -export class BrokeredReplace implements Order { - constructor(public orderId: string, - public origOrderId: string, - public side: Side, - public quantity: number, - public type: OrderType, - public price: number, - public timeInForce: TimeInForce, - public exchange: Exchange, - public exchangeId: string, - public preferPostOnly: boolean) {} -} - -export class BrokeredCancel { - constructor(public clientOrderId: string, - public requestId: string, - public side: Side, - public exchangeId: string) {} -} - -export class SentOrder { - constructor(public sentOrderClientId: string) {} -} - -export class OrderGatewayActionReport { - constructor(public sentTime: moment.Moment) {} -} - -export interface OrderStatusReport { - pair? : CurrencyPair; - side? : Side; - quantity? : number; - type? : OrderType; - price? : number; - timeInForce? : TimeInForce; - orderId? : string; - exchangeId? : string; - orderStatus? : OrderStatus; - rejectMessage? : string; - time? : moment.Moment; - lastQuantity? : number; - lastPrice? : number; - leavesQuantity? : number; - cumQuantity? : number; - averagePrice? : number; - liquidity? : Liquidity; - exchange? : Exchange; - computationalLatency? : number; - version? : number; - preferPostOnly?: boolean; - - partiallyFilled? : boolean; - pendingCancel? : boolean; - pendingReplace? : boolean; - cancelRejected? : boolean; -} - -export class OrderStatusReportImpl implements OrderStatusReport, ITimestamped { - constructor(public pair: CurrencyPair, - public side: Side, - public quantity: number, - public type: OrderType, - public price: number, - public timeInForce: TimeInForce, - public orderId: string, - public exchangeId: string, - public orderStatus: OrderStatus, - public rejectMessage: string, - public time: moment.Moment, - public lastQuantity: number, - public lastPrice: number, - public leavesQuantity: number, - public cumQuantity: number, - public averagePrice: number, - public liquidity: Liquidity, - public exchange: Exchange, - public computationalLatency: number, - public version: number, - public partiallyFilled: boolean, - public pendingCancel: boolean, - public pendingReplace: boolean, - public cancelRejected: boolean, - public preferPostOnly: boolean) {} - - public toString() { - var components: string[] = []; - - components.push("orderId=" + this.orderId); - components.push("time=" + this.time.format('M/d/YY h:mm:ss,SSS')); - if (typeof this.exchangeId !== "undefined") components.push("exchangeId=" + this.exchangeId); - components.push("pair=" + Currency[this.pair.base] + "/" + Currency[this.pair.quote]); - if (typeof this.exchange !== "undefined") components.push("exchange=" + Exchange[this.exchange]); - components.push("orderStatus=" + OrderStatus[this.orderStatus]); - if (this.partiallyFilled) components.push("partiallyFilled"); - if (this.pendingCancel) components.push("pendingCancel"); - if (this.pendingReplace) components.push("pendingReplace"); - if (this.cancelRejected) components.push("cancelRejected"); - components.push("side=" + Side[this.side]); - components.push("quantity=" + this.quantity); - components.push("price=" + this.price); - components.push("tif=" + TimeInForce[this.timeInForce]); - components.push("type=" + OrderType[this.type]); - components.push("version=" + this.version); - if (typeof this.rejectMessage !== "undefined") components.push(this.rejectMessage); - if (typeof this.computationalLatency !== "undefined") components.push("computationalLatency=" + this.computationalLatency); - if (typeof this.lastQuantity !== "undefined") components.push("lastQuantity=" + this.lastQuantity); - if (typeof this.lastPrice !== "undefined") components.push("lastPrice=" + this.lastPrice); - if (typeof this.leavesQuantity !== "undefined") components.push("leavesQuantity=" + this.leavesQuantity); - if (typeof this.cumQuantity !== "undefined") components.push("cumQuantity=" + this.cumQuantity); - if (typeof this.averagePrice !== "undefined") components.push("averagePrice=" + this.averagePrice); - if (typeof this.liquidity !== "undefined") components.push("liquidity=" + Liquidity[this.liquidity]); - - return components.join(";"); - } -} - -export class Trade implements ITimestamped { - constructor(public tradeId: string, - public time: moment.Moment, - public exchange: Exchange, - public pair: CurrencyPair, - public price: number, - public quantity: number, - public side: Side, - public value: number, - public liquidity: Liquidity, - public feeCharged: number) {} -} - -export class CurrencyPosition { - constructor(public amount: number, - public heldAmount: number, - public currency: Currency) {} - - public toString() { - return "currency=" + Currency[this.currency] + ";amount=" + this.amount; - } -} - -export class PositionReport { - constructor(public baseAmount: number, - public quoteAmount: number, - public baseHeldAmount: number, - public quoteHeldAmount: number, - public value: number, - public quoteValue: number, - public pair: CurrencyPair, - public exchange: Exchange, - public time: moment.Moment) {} -} - -export class OrderRequestFromUI { - constructor(public side: string, - public price: number, - public quantity: number, - public timeInForce: string, - public orderType: string) {} -} - -export interface ReplaceRequestFromUI { - price : number; - quantity : number; -} - -export class FairValue implements ITimestamped { - constructor(public price: number, public time: moment.Moment) {} -} - -export enum QuoteAction { New, Cancel } -export enum QuoteSent { First, Modify, UnsentDuplicate, Delete, UnsentDelete, UnableToSend } - -export class Quote { - constructor(public price: number, - public size: number) {} - - private static Tol = 1e-3; - public equals(other: Quote) { - return Math.abs(this.price - other.price) < Quote.Tol && Math.abs(this.size - other.size) < Quote.Tol; - } -} - -export class TwoSidedQuote implements ITimestamped { - constructor(public bid: Quote, public ask: Quote, public time: moment.Moment) {} -} - -export enum QuoteStatus { Live, Held } - -export class SerializedQuotesActive { - constructor(public active: boolean, public time: moment.Moment) {} -} - -export class TwoSidedQuoteStatus { - constructor(public bidStatus: QuoteStatus, public askStatus: QuoteStatus) {} -} - -export class CurrencyPair { - constructor(public base: Currency, public quote: Currency) {} - - public toString() { - return Currency[this.base] + "/" + Currency[this.quote]; - } -} - -export function currencyPairEqual(a: CurrencyPair, b: CurrencyPair): boolean { - return a.base === b.base && a.quote === b.quote; -} - -export enum QuotingMode { Top, Mid, Join, InverseJoin, InverseTop, PingPong } -export enum FairValueModel { BBO, wBBO } -export enum AutoPositionMode { Off, EwmaBasic } - -export class QuotingParameters { - constructor(public width: number, - public size: number, - public mode: QuotingMode, - public fvModel: FairValueModel, - public targetBasePosition: number, - public positionDivergence: number, - public ewmaProtection: boolean, - public autoPositionMode: AutoPositionMode, - public aggressivePositionRebalancing: boolean, - public tradesPerMinute: number, - public tradeRateSeconds: number, - public longEwma: number, - public shortEwma: number, - public quotingEwma: number, - public aprMultiplier: number, - public stepOverSize: number) {} -} - -export function toUtcFormattedTime(t: moment.Moment) { - return t.format('M/D/YY HH:mm:ss,SSS'); -} - -export function toShortTimeString(t: moment.Moment) { - return t.format('HH:mm:ss,SSS'); -} - -export class ExchangePairMessage { - constructor(public exchange: Exchange, public pair: CurrencyPair, public data: T) { } -} - -export class ProductAdvertisement { - constructor(public exchange: Exchange, public pair: CurrencyPair, public environment: string) { } -} - -export class Message implements ITimestamped { - constructor(public text: string, public time: moment.Moment) {} -} - -export class RegularFairValue { - constructor(public time: moment.Moment, public value: number) {} -} - -export class TradeSafety { - constructor(public buy: number, - public sell: number, - public combined: number, - public buyPing: number, - public sellPong: number, - public time: moment.Moment) {} -} - -export class TargetBasePositionValue { - constructor(public data: number, public time: moment.Moment) {} -} - -export class CancelAllOrdersRequest { - constructor() {} -} diff --git a/src/lib/Krypto.ninja-apis.h b/src/lib/Krypto.ninja-apis.h new file mode 100644 index 000000000..d80c09e34 --- /dev/null +++ b/src/lib/Krypto.ninja-apis.h @@ -0,0 +1,1412 @@ +//! \file +//! \brief Exchange API integrations. + +namespace ₿ { + enum class Connectivity: unsigned int { Disconnected, Connected }; + enum class Status: unsigned int { Waiting, Working, Terminated }; + enum class Side: unsigned int { Bid, Ask }; + enum class TimeInForce: unsigned int { GTC, IOC, FOK }; + enum class OrderType: unsigned int { Limit, Market }; + + struct Level { + Price price = 0; + Amount size = 0; + }; + static void to_json(json &j, const Level &k) { + j = { + {"price", k.price} + }; + if (k.size) j["size"] = k.size; + }; + struct Levels { + vector bids, + asks; + static Levels reduce(const size_t &max, Levels levels) { + if (max) { + if (levels.bids.size() > max) + levels.bids.erase(levels.bids.begin() + max, levels.bids.end()); + if (levels.asks.size() > max) + levels.asks.erase(levels.asks.begin() + max, levels.asks.end()); + } + return levels; + }; + static void update(const Side &side, const Price &price, const Amount &size, Levels *const levels) { + vector *const level = side == Side::Bid + ? &levels->bids + : &levels->asks; + auto it = find_if( + level->begin(), level->end(), + [&](const Level &it) { return price == it.price; } + ); + if (it == level->end()) { + if (size) + level->insert( + side == Side::Bid + ? find_if( + level->begin(), level->end(), + [&](const Level &it) { return price > it.price; } + ) + : find_if( + level->begin(), level->end(), + [&](const Level &it) { return price < it.price; } + ), + {price, size} + ); + } else if (size) + it->size = size; + else level->erase(it); + }; + }; + static void __attribute__ ((unused)) to_json(json &j, const Levels &k) { + j = { + {"bids", k.bids}, + {"asks", k.asks} + }; + }; + + struct Ticker { + string symbol = ""; + string base = ""; + string quote = ""; + Price price = 0, + bestask = 0, + bestbid = 0, + spread = 0; + double open = 0; + Amount volume = 0; + }; + static void __attribute__ ((unused)) to_json(json &j, const Ticker &k) { + j = { + { "symbol", k.symbol }, + { "base", k.base }, + { "quote", k.quote }, + { "price", k.price }, + {"bestask", k.bestask}, + {"bestbid", k.bestbid}, + { "spread", k.spread }, + { "open", k.open }, + { "volume", k.volume } + }; + }; + + struct Wallet { + Amount amount = 0, + held = 0; + string currency = ""; + Amount total = 0, + value = 0; + double profit = 0; + Wallet &operator=(const Wallet &raw) { + total = (amount = raw.amount) + + (held = raw.held); + currency = raw.currency; + return *this; + }; + }; + static void __attribute__ ((unused)) to_json(json &j, const Wallet &k) { + j = { + { "amount", k.amount }, + { "held", k.held }, + {"currency", k.currency}, + { "value", k.value }, + { "profit", k.profit } + }; + }; + + struct Trade { + Side side = (Side)0; + Price price = 0; + Amount quantity = 0; + Clock time = 0; + }; + static void __attribute__ ((unused)) to_json(json &j, const Trade &k) { + j = { + { "side", k.side }, + { "price", k.price }, + {"quantity", k.quantity}, + { "time", k.time } + }; + }; + + struct Order { + string symbol = ""; + Side side = (Side)0; + Price price = 0; + Amount quantity = 0; + Clock time = 0; + bool isPong = false; + string orderId = "", + exchangeId = ""; + Status status = (Status)0; + Amount qtyFilled = 0; + OrderType type = (OrderType)0; + TimeInForce timeInForce = (TimeInForce)0; + bool manual = false; + Clock latency = 0; + Price pricePrecision = 0; + Amount quantityPrecision = 0; + static Order *update(const Order &raw, Order *const order) { + if (order) { + if (Status::Working == ( order->status = raw.status + ) and !order->latency) order->latency = raw.time - order->time; + order->time = raw.time; + if (!raw.exchangeId.empty()) order->exchangeId = raw.exchangeId; + if (raw.price) order->price = raw.price; + if (raw.quantity) order->quantity = raw.quantity; + if (raw.qtyFilled) order->qtyFilled = raw.qtyFilled; + } + return order; + }; + static bool replace(const Price &price, const bool &isPong, Order *const order) { + if (!order + or order->exchangeId.empty() + ) return false; + order->price = price; + order->isPong = isPong; + order->time = Tstamp; + return true; + }; + static bool cancel(Order *const order) { + if (!order + or order->exchangeId.empty() + or order->status == Status::Waiting + ) return false; + order->status = Status::Waiting; + order->time = Tstamp; + return true; + }; + }; + static void __attribute__ ((unused)) to_json(json &j, const Order &k) { + j = { + { "orderId", k.orderId }, + { "exchangeId", k.exchangeId }, + { "symbol", k.symbol }, + { "side", k.side }, + { "quantity", k.quantity }, + { "type", k.type }, + { "isPong", k.isPong }, + { "price", k.price }, + { "timeInForce", k.timeInForce }, + { "status", k.status }, + { "time", k.time }, + { "latency", k.latency }, + { "pricePrecision", k.pricePrecision }, + {"quantityPrecision", k.quantityPrecision} + }; + }; + static void __attribute__ ((unused)) from_json(const json &j, Order &k) { + k.symbol = j.value("symbol", ""); + k.orderId = j.value("orderId", ""); + k.price = j.value("price", 0.0); + k.quantity = j.value("quantity", 0.0); + k.time = j.value("time", Tstamp); + k.side = j.value("side", k.side); + k.type = j.value("type", k.type); + k.timeInForce = j.value("timeInForce", k.timeInForce); + k.manual = j.value("manual", false); + }; + + class GwExchangeData { + public_friend: + using DataEvent = variant< + function, + function, + function, + function, + function, + function + >; + public: + curl_socket_t loopfd = 0; + struct { + Decimal funds, + price, + amount, + percent, + orderPrice, + orderAmount; + } decimal; + bool askForReplace = false; + bool askForBalance = false; + string (*randId)() = nullptr; + protected: + struct { + Loop::Async::Event connectivity; + Loop::Async::Event ticker; + Loop::Async::Event wallet; + Loop::Async::Event levels; + Loop::Async::Event orders; + Loop::Async::Event trades; + } async; + public: + virtual void ask_for_data(const unsigned int &tick) = 0; + virtual void wait_for_data(Loop *const loop) = 0; + void data(const DataEvent &ev) { + if (holds_alternative >(ev)) + async.connectivity.callback(get>(ev)); + else if (holds_alternative >(ev)) + async.ticker.callback(get>(ev)); + else if (holds_alternative >(ev)) + async.wallet.callback(get>(ev)); + else if (holds_alternative >(ev)) + async.levels.callback(get>(ev)); + else if (holds_alternative >(ev)) + async.orders.callback(get>(ev)); + else if (holds_alternative >(ev)) + async.trades.callback(get>(ev)); + }; + void place(const Order *const order) { + decimal.orderPrice.precision(order->pricePrecision); + decimal.orderAmount.precision(order->quantityPrecision); + place( + order->symbol, + order->orderId, + order->side, + decimal.orderPrice.str(order->price), + decimal.orderAmount.str(order->quantity), + order->type, + order->timeInForce + ); + }; + void replace(const Order *const order) { + replace( + order->exchangeId, + decimal.price.str(order->price) + ); + }; + void cancel(const Order *const order) { + cancel( + order->orderId, + order->exchangeId + ); + }; + void balance() { + askForBalance = false; + if (!async_wallet()) + async.wallet.ask_for(); + }; +//BO non-free Gw class member functions from lib build-*/lib/K-*.a (it just redefines all virtual gateway functions below)... +/**/ virtual void place(string, string, Side, string, string, OrderType, TimeInForce) = 0; // call async order data from exchange. +/**/ virtual void replace(string, string) {}; // call price async order data from exchange. +/**/ virtual void cancel(string, string) = 0; // call by uuid async order data from exchange. +/**/ virtual void cancel() = 0; // call by symbol async orders data from exchange. +/**/protected: +/**/ virtual bool async_wallet() { return false; }; // call async wallet data from exchange. +/**/ virtual vector sync_wallet() { return {}; }; // call sync wallet data from exchange. +//EO non-free Gw class member functions from lib build-*/lib/K-*.a (it just redefines all virtual gateway functions above)... + virtual void online(const Connectivity &connectivity) { + async.connectivity.try_write(connectivity); + if (!(bool)connectivity) + async.levels.try_write({}); + }; + void wait_for_never_async_data(Loop *const loop) { + async.wallet.wait_for(loop, [&]() { return sync_wallet(); }); + }; + void ask_for_never_async_data(const unsigned int &tick) { + if (async.wallet.waiting() and ( + askForBalance or !(tick % 15) + )) balance(); + }; + }; + + class GwExchange: public GwExchangeData { + public: + using Report = vector>; + string exchange, apikey, secret, apikeyid, + base, quote, symbol, + http, ws, + unlock; + Price tickPrice = 0; + Amount tickSize = 0, + minSize = 0, + minValue = 0, + makeFee = 0, + takeFee = 0; + int maxLevel = 0; + bool debug = false; + Connectivity adminAgreement = Connectivity::Disconnected; + unordered_map> precisions; + void handshakes(const bool &nocache) { + json reply; + const string cache = (K_HOME "/cache/handshakes") + + ('.' + exchange) + + '.' + "json"; + if (!nocache and !Files::mkdirs(cache)) + print("Error while writing into " + cache + ", please create the parent directory or change the permissions manually before try again."); + fstream file; + struct stat st; + if (!nocache + and access(cache.data(), R_OK) != -1 + and !stat(cache.data(), &st) + and Tstamp - 25200e+3 < st.st_mtime * 1e+3 + ) { + file.open(cache, fstream::in); + reply = json::parse(file); + } else + reply = handshakes(); + if (reply.is_array()) + for (auto &it : reply) + precisions[it.value("symbol", "")] = { + it.value("tickPrice", 0.0), + it.value("tickSize", 0.0) + }; + if (!nocache and !file.is_open() and Files::mkdirs(cache) + and !precisions.empty() + ) { + file.open(cache, fstream::out | fstream::trunc); + file << reply.dump(); + } + if (file.is_open()) file.close(); + }; + json handshake(const bool &nocache) { + json reply; + const string cache = (K_HOME "/cache/handshake") + + ('.' + exchange) + + '.' + base + + '.' + quote + + '.' + "json"; + if (!nocache and !Files::mkdirs(cache)) + print("Error while writing into " + cache + ", please create the parent directory or change the permissions manually before try again."); + fstream file; + struct stat st; + if (!nocache + and access(cache.data(), R_OK) != -1 + and !stat(cache.data(), &st) + and Tstamp - 25200e+3 < st.st_mtime * 1e+3 + ) { + file.open(cache, fstream::in); + reply = json::parse(file); + } else + reply = handshake(); + base = reply.value("base", base); + quote = reply.value("quote", quote); + symbol = reply.value("symbol", symbol); + tickPrice = reply.value("tickPrice", 0.0); + tickSize = reply.value("tickSize", 0.0); + minValue = reply.value("minValue", 0.0); + if (!minSize) minSize = reply.value("minSize", 0.0); + if (!makeFee) makeFee = reply.value("makeFee", 0.0); + if (!takeFee) takeFee = reply.value("takeFee", 0.0); + decimal.funds.precision(1e-8); + decimal.price.precision(tickPrice); + decimal.amount.precision(tickSize); + decimal.percent.precision(1e-2); + if (!nocache and !file.is_open() and Files::mkdirs(cache) + and tickPrice and tickSize and minSize + and !base.empty() and !quote.empty() + ) { + file.open(cache, fstream::out | fstream::trunc); + file << reply.dump(); + } + if (file.is_open()) file.close(); + return reply.value("reply", json::object()); + }; + void end() { + online(Connectivity::Disconnected); + disconnect(); + }; + void report(Report notes, const bool &nocache) { + for (const auto &it : (Report){ + {"tickers", to_string(precisions.size()) + " from " + exchange + " exchange" }, + {"symbols", base + "/" + quote + " (" + decimal.amount.str(tickSize) + + "/" + decimal.price.str(tickPrice) + ")" }, + {"minSize", decimal.amount.str(minSize) + " " + base + + (minValue ? " or " + decimal.price.str(minValue) + " " + quote : "")}, + {"makeFee", decimal.percent.str(makeFee * 1e+2) + "%" }, + {"takeFee", decimal.percent.str(takeFee * 1e+2) + "%" } + }) notes.push_back(it); + string note = "handshake:"; + for (const auto &it : notes) + if (!it.second.empty()) + note += ANSI_NEW_LINE "- " + it.first + ": " + it.second; + print((nocache ? "" : "cached ") + note); + }; + string latency(const function &fn) { + print("latency check", "start"); + const Clock Tstart = Tstamp; + fn(); + const Clock Tstop = Tstamp; + print("latency check", "stop"); + const unsigned int Tdiff = Tstop - Tstart; + print("HTTP read/write handshake took", to_string(Tdiff) + "ms of your time"); + string result = "This result is "; + if (Tdiff < 2e+2) result += "very good; most traders don't enjoy such speed!"; + else if (Tdiff < 5e+2) result += "good; most traders get the same result"; + else if (Tdiff < 7e+2) result += "a bit bad; most traders get better results"; + else if (Tdiff < 1e+3) result += "bad; consider moving to another server/network"; + else result += "very bad; move to another server/network"; + print(result); + return "--latency of 1 HTTP call done (consider to repeat a few times this check)"; + }; + string pairs() const { + string report; + pairs(report); + print("allows " + to_string(count(report.begin(), report.end(), '\n')) + " currency pairs"); + cout << report; + return "--list done (to find a symbol use grep)"; + }; + virtual string web(const string&, const string&) const = 0; + string web(const bool &orders = false) const { + return orders ? webOrders : web(base, quote); + }; + void disclaimer() const { + if (unlock.empty()) return; + print("was slowdown 121 seconds (--free-version argument was implicitly set):" + ANSI_NEW_LINE ANSI_NEW_LINE "Current apikey: " + apikey.substr(0, apikey.length() / 2) + + string(apikey.length() / 2, '#') + + ANSI_NEW_LINE ANSI_NEW_LINE "To unlock it anonymously and to collaborate with" + ANSI_NEW_LINE "the development, make an acceptable Pull Request" + ANSI_NEW_LINE "on github.. or send 0.00121000 BTC (or more) to:" + ANSI_NEW_LINE ANSI_NEW_LINE " " + unlock + + ANSI_NEW_LINE ANSI_NEW_LINE "Before restart, wait for zero (0) confirmations:" + ANSI_NEW_LINE ANSI_NEW_LINE "https://live.blockcypher.com/btc/address/" + unlock + + ANSI_NEW_LINE ANSI_NEW_LINE OBLIGATORY_analpaper_SOFTWARE_LICENSE + ANSI_NEW_LINE ANSI_NEW_LINE " Signed-off-by: Carles Tubio" + ANSI_NEW_LINE "see: github.com/ctubio/Krypto-trading-bot#unlock" + ANSI_NEW_LINE "or just use --free-version to hide this message" + ); + }; + function printer; + void print(const string &reason, const string &highlight = "") const { + if (printer) printer( + string(reason.find(">>>") != reason.find("<<<") + ? "DEBUG " + : "GW " + ) + exchange, + reason, + highlight + ); + }; + protected: + string webMarket, + webOrders; + virtual void disconnect() = 0; + virtual bool connected() const = 0; + virtual json handshakes() const = 0; + virtual json handshake() const = 0; + virtual void pairs(string&) const = 0; + virtual string nonce() const = 0; + void online(const Connectivity &connectivity) override { + print("network state changed to", string((bool)connectivity + ? "" + : "DIS" + ) + "CONNECTED"); + GwExchangeData::online(connectivity); + }; + }; + + class Gw: public GwExchange { + public: +//BO non-free Gw class member functions from lib build-*/lib/K-*.a (it just redefines all virtual gateway functions below). +/**/ static Gw* new_Gw(const string&); // may return too a nullptr instead of a child gateway class, if string is unknown. +//EO non-free Gw class member functions from lib build-*/lib/K-*.a (it just redefines all virtual gateway functions above). + }; + + class GwApiWs: public Gw, + public Curl::WebSocket { + private: + unsigned int countdown = 1; + bool subscription = false; + public: + void ask_for_data(const unsigned int &tick) override { + if (countdown and !--countdown) + connect(); + if (subscribed()) + ask_for_never_async_data(tick); + }; + void wait_for_data(Loop *const loop) override { + wait_for_never_async_data(loop); + }; + protected: +//BO non-free Gw class member functions from lib build-*/lib/K-*.a (it just redefines all virtual gateway functions below). +/**/ virtual string subscribe() = 0; // send subcription messages to remote server and return channel names. +/**/ virtual void consume(json) = 0; // read message one by one from remote server and call async observers. +//EO non-free Gw class member functions from lib build-*/lib/K-*.a (it just redefines all virtual gateway functions above). + bool connected() const override { + return WebSocket::connected(); + }; + virtual void connect() { + CURLcode rc; + if (CURLE_OK == (rc = WebSocket::connect(ws))) + WebSocket::start(GwExchangeData::loopfd, [&]() { + wait_for_async_data(); + }); + else reconnect(string("CURL connect Error: ") + curl_easy_strerror(rc)); + }; + void emit(const string &msg) { + CURLcode rc; + if (CURLE_OK != (rc = WebSocket::emit(msg, 0x01))) + print(string("CURL send Error: ") + curl_easy_strerror(rc)); + }; + void disconnect() override { + WebSocket::emit("", 0x08); + WebSocket::cleanup(); + }; + void reconnect(const string &reason) { + disconnect(); + countdown = 7; + print("WS " + reason + ", reconnecting in " + to_string(countdown) + "s."); + }; + bool subscribed() { + if (subscription != connected()) { + subscription = !subscription; + if (subscription) print("WS streaming [" + subscribe() + "]"); + else reconnect("Disconnected"); + online((Connectivity)subscription); + } + return subscription; + }; + bool accept_msg(const string &msg) { + const bool next = !msg.empty(); + if (next) { + if (json::accept(msg)) + consume(json::parse(msg)); + else print("WS Error: Unsupported data format"); + } + return next; + }; + private: + void wait_for_async_data() { + CURLcode rc; + if (CURLE_OK != (rc = WebSocket::send_recv())) + print(string("CURL recv Error: ") + curl_easy_strerror(rc)); + while (accept_msg(WebSocket::unframe())); + }; + }; + class GwApiWsWs: public GwApiWs, + public Curl::WebSocketTwin { + protected: + bool connected() const override { + return GwApiWs::connected() + and WebSocketTwin::connected(); + }; + void connect() override { + GwApiWs::connect(); + if (GwApiWs::connected()) { + CURLcode rc; + if (CURLE_OK == (rc = WebSocketTwin::connect(twin(ws)))) + WebSocketTwin::start(GwExchangeData::loopfd, [&]() { + wait_for_async_data(); + }); + else reconnect(string("CURL connect Error: ") + curl_easy_strerror(rc)); + } + }; + void disconnect() override { + WebSocketTwin::emit("", 0x08); + WebSocketTwin::cleanup(); + GwApiWs::disconnect(); + }; + void emit(const string &msg) { + GwApiWs::emit(msg); + }; + void beam(const string &msg) { + CURLcode rc; + if (CURLE_OK != (rc = WebSocketTwin::emit(msg, 0x01))) + print(string("CURL send Error: ") + curl_easy_strerror(rc)); + }; + private: + void wait_for_async_data() { + CURLcode rc; + if (CURLE_OK != (rc = WebSocketTwin::send_recv())) + print(string("CURL recv Error: ") + curl_easy_strerror(rc)); + while (accept_msg(WebSocketTwin::unframe())); + }; + }; + + class GwApiWsFix: public GwApiWs, + public Curl::FixSocket { + public: + GwApiWsFix(const string &t) + : FixSocket(t, apikey) + {}; + private: + string fix; + protected: + bool connected() const override { + return GwApiWs::connected() + and FixSocket::connected(); + }; +//BO non-free Gw class member functions from lib build-*/lib/K-*.a (it just redefines all virtual gateway functions below). +/**/ virtual string logon() = 0; // return logon message. +//EO non-free Gw class member functions from lib build-*/lib/K-*.a (it just redefines all virtual gateway functions above). + void connect() override { + GwApiWs::connect(); + if (GwApiWs::connected()) { + CURLcode rc; + if (CURLE_OK == (rc = FixSocket::connect(fix, logon()))) { + FixSocket::start(GwExchangeData::loopfd, [&]() { + wait_for_async_data(); + }); + print("FIX streaming [orders]"); + } else reconnect(string("CURL connect FIX Error: ") + curl_easy_strerror(rc)); + } + }; + void disconnect() override { + if (FixSocket::connected()) print("FIX Logout"); + FixSocket::emit("", "5"); + FixSocket::cleanup(); + GwApiWs::disconnect(); + }; + void beam(const string &msg, const string &type) { + CURLcode rc; + if (CURLE_OK != (rc = FixSocket::emit(msg, type))) + print(string("CURL send FIX Error: ") + curl_easy_strerror(rc)); + }; + private: + void wait_for_async_data() { + CURLcode rc; + if (CURLE_OK != (rc = FixSocket::send_recv())) + print(string("CURL recv FIX Error: ") + curl_easy_strerror(rc)); + while (accept_msg(FixSocket::unframe())); + }; + }; + + class GwBinance: public GwApiWs { + public: + GwBinance() + { + http = "https://api.binance.com"; + ws = "wss://stream.binance.com:9443/ws"; + randId = Random::uuid36Id; + webMarket = "https://www.binance.com/en/trade/"; + webOrders = "https://www.binance.com/en/my/orders/exchange"; + }; + string web(const string &base, const string "e) const override { + return webMarket + base + "_" + quote + "?layout=pro"; + }; + protected: + string nonce() const override { + return to_string(Tstamp) + "&recvWindow=21000"; + }; + json handshakes() const override { + json tickers = json::array(); + const json reply = Curl::Web::xfer(http + "/api/v3/exchangeInfo"); + if (reply.contains("symbols") and reply.at("symbols").is_array()) + for (auto &it : reply.at("symbols")) + if (it.contains("filters") and it.at("filters").is_array()) { + double tickPrice = 0, + tickSize = 0; + for (const json &it_ : it.at("filters")) { + if (it_.value("filterType", "") == "PRICE_FILTER") + tickPrice = stod(it_.value("tickSize", "0")); + else if (it_.value("filterType", "") == "LOT_SIZE") + tickSize = stod(it_.value("stepSize", "0")); + } + tickers.push_back({ + { "symbol", it.value("symbol", "")}, + {"tickPrice", tickPrice }, + { "tickSize", tickSize } + }); + } + return tickers; + }; + json handshake() const override { + json reply1 = Curl::Web::xfer(http + "/api/v3/exchangeInfo?symbol="+ base + quote); + if (reply1.contains("symbols") and reply1.at("symbols").is_array()) + for (const json &it : reply1.at("symbols")) + if (it.value("symbol", "") == base + quote) { + reply1 = it; + if (reply1.contains("filters") and reply1.at("filters").is_array()) + for (const json &it_ : reply1.at("filters")) { + if (it_.value("filterType", "") == "PRICE_FILTER") + reply1["tickPrice"] = stod(it_.value("tickSize", "0")); + else if (it_.value("filterType", "") == "NOTIONAL") + reply1["minValue"] = stod(it_.value("minNotional", "0")); + else if (it_.value("filterType", "") == "LOT_SIZE") { + reply1["tickSize"] = stod(it_.value("stepSize", "0")); + reply1["minSize"] = stod(it_.value("minQty", "0")); + } + } + break; + } + const json reply2 = fees(); + return { + { "base", base }, + { "quote", quote }, + { "symbol", reply1.value("symbol", "") }, + {"tickPrice", reply1.value("tickPrice", 0.0) }, + { "tickSize", reply1.value("tickSize", 0.0) }, + { "minSize", reply1.value("minSize", 0.0) }, + { "minValue", reply1.value("minValue", 0.0) }, + { "makeFee", stod(reply2.value("makerCommission", "0"))}, + { "takeFee", stod(reply2.value("takerCommission", "0"))}, + { "reply", {reply1, reply2} } + }; + }; + void pairs(string &report) const override { + const json reply = Curl::Web::xfer(http + "/api/v3/exchangeInfo"); + if (!reply.is_object() + or !reply.contains("symbols") + or !reply.at("symbols").is_array() + or reply.at("symbols").empty() + or !reply.at("symbols").at(0).is_object() + or !reply.at("symbols").at(0).contains("isSpotTradingAllowed") + ) print("Error while reading pairs: " + reply.dump()); + else for (const json &it : reply.at("symbols")) + if (it.value("isSpotTradingAllowed", false) + and it.value("status", "") == "TRADING" + ) report += it.value("baseAsset", "") + "/" + it.value("quoteAsset", "") + ANSI_NEW_LINE; + }; + json xfer(const string &url, const string &h1, const string &crud) const { + return Curl::Web::xfer(url, crud, "", { + "X-MBX-APIKEY: " + h1 + }); + }; + private: + json fees() const { + const string crud = "GET", + path = "/sapi/v1/asset/tradeFee?", + post = "symbol=" + base + quote + + "×tamp=" + nonce(), + sign = "&signature=" + Text::HMAC256(post, secret); + const json reply = xfer(http + path + post + sign, apikey, crud); + if (!reply.is_array() + or reply.empty() + or !reply.at(0).is_object() + or !reply.at(0).contains("symbol") + or !reply.at(0).at("symbol").is_string() + or reply.at(0).value("symbol", "") != base + quote + ) { + print("Error while reading fees: " + reply.dump()); + return json::object(); + } + return reply.at(0); + }; + }; + class GwBinanceUS: virtual public GwBinance { + public: + GwBinanceUS() + { + http = "https://api.binance.us"; + ws = "wss://stream.binance.us:9443/ws"; + webMarket = "https://www.binance.us/en/trade/"; + webOrders = "https://www.binance.us/en/orders"; + }; + }; + class GwBitmex: public GwApiWs { + public: + GwBitmex() + { + http = "https://www.bitmex.com/api/v1"; + ws = "wss://www.bitmex.com/realtime"; + randId = Random::uuid36Id; + askForReplace = true; + webMarket = "https://www.bitmex.com/app/trade/"; + webOrders = "https://www.bitmex.com/app/orderHistory"; + }; + string web(const string &base, const string "e) const override { + return webMarket + base + quote; + }; + protected: + string nonce() const override { + return to_string(Tstamp); + }; + json handshakes() const override { + json tickers = json::array(); + const json reply = Curl::Web::xfer(http + "/instrument"); + if (reply.is_array()) + for (auto &it : reply) + tickers.push_back({ + { "symbol", it.value("symbol", "") }, + {"tickPrice", it.value("tickSize", 0.0)}, + { "tickSize", it.value("lotSize", 0.0) } + }); + return tickers; + }; + json handshake() const override { + json reply = { + {"object", Curl::Web::xfer(http + "/instrument?symbol=" + base + "_" + quote)} + }; + if (reply.at("object").is_array() and !reply.at("object").empty()) + reply = reply.at("object").at(0); + return { + { "base", base }, + { "quote", quote }, + { "symbol", reply.value("symbol", 0.0) }, + {"tickPrice", reply.value("tickSize", 0.0) }, + { "tickSize", reply.value("lotSize", 0.0) }, + { "minSize", reply.value("lotSize", 0.0) }, + { "makeFee", reply.value("makerFee", 0.0) }, + { "takeFee", reply.value("takerFee", 0.0) }, + { "reply", reply } + }; + }; + void pairs(string &report) const override { + const json reply = Curl::Web::xfer(http + "/instrument?filter="+((json){ + {"typ", "IFXXXP"}, {"state", "Open"} + }).dump()); + if (!reply.is_array() + or reply.empty() + or !reply.at(0).is_object() + or !reply.at(0).contains("symbol") + ) print("Error while reading pairs: " + reply.dump()); + else for (const json &it : reply) + if (!it.value("isInverse", false)) + report += it.value("symbol", "") + ANSI_NEW_LINE; + }; + json xfer(const string &url, const string &h1, const string &h2, const string &h3, const string &post, const string &crud) const { + return Curl::Web::xfer(url, crud, post, { + "api-expires: " + h1, + "api-key: " + h2, + "api-signature: " + h3 + }); + }; + }; + class GwGateio: public GwApiWs { + public: + GwGateio() + { + http = "https://api.gateio.ws/api/v4"; + ws = "wss://api.gateio.ws/ws/v4/"; + randId = Random::int45Id; + webMarket = "https://www.gate.io/trade/"; + webOrders = "https://www.gate.io/myaccount/myorders"; + }; + string web(const string &base, const string "e) const override { + return webMarket + base + "_" + quote; + }; + protected: + string nonce() const override { + return to_string(Tstamp / 1e+3); + }; + json handshakes() const override { + json tickers = json::array(); + const json reply = Curl::Web::xfer(http + "/spot/currency_pairs"); + if (reply.is_array()) + for (auto &it : reply) + tickers.push_back({ + { "symbol", it.value("id", "") }, + {"tickPrice", pow(10, -it.value("precision", 0)) }, + { "tickSize", pow(10, -it.value("amount_precision", 0))} + }); + return tickers; + }; + json handshake() const override { + json reply = { + {"object", Curl::Web::xfer(http + "/spot/currency_pairs")} + }; + if (reply.at("object").is_array() and !reply.at("object").empty()) + for (const json &it : reply.at("object")) + if (it.value("id", "") == base + "_" + quote) { + reply = it; + break; + } + return { + { "base", base }, + { "quote", quote }, + { "symbol", reply.value("id", "") }, + {"tickPrice", pow(10, -reply.value("precision", 0)) }, + { "tickSize", pow(10, -reply.value("amount_precision", 0)) }, + { "minSize", stod(reply.value("min_base_amount", "0")) + ?: pow(10, -reply.value("amount_precision", 0))}, + { "minValue", stod(reply.value("min_quote_amount", "0")) + ?: pow(10, -reply.value("precision", 0)) }, + { "makeFee", stod(reply.value("fee", "0")) / 1e+2 }, + { "takeFee", stod(reply.value("fee", "0")) / 1e+2 }, + { "reply", reply } + }; + }; + void pairs(string &report) const override { + const json reply = Curl::Web::xfer(http + "/spot/currency_pairs"); + if (!reply.is_array() + or reply.empty() + or !reply.at(0).is_object() + or !reply.at(0).contains("trade_status") + ) print("Error while reading pairs: " + reply.dump()); + else for (const json &it : reply) + if (it.value("trade_status", "") == "tradable") + report += it.value("base", "") + "/" + it.value("quote", "") + ANSI_NEW_LINE; + }; + json xfer(const string &url, const string &h1, const string &h2, const string &h3, const string &post, const string &crud) const { + return Curl::Web::xfer(url, crud, post, { + "Content-Type: application/json", + "KEY: " + h1, + "Timestamp: " + h2, + "SIGN: " + h3 + }); + }; + }; + class GwHitBtc: public GwApiWsWs { + public: + GwHitBtc() + { + http = "https://api.hitbtc.com/api/2"; + ws = "wss://api.hitbtc.com/api/2/ws/public"; + randId = Random::uuid32Id; + webMarket = "https://hitbtc.com/exchange/"; + webOrders = "https://hitbtc.com/reports/orders"; + }; + string web(const string &base, const string "e) const override { + return webMarket + base + "-to-" + quote; + }; + protected: + string nonce() const override { + return randId() + randId(); + }; + json handshakes() const override { + json tickers = json::array(); + const json reply = Curl::Web::xfer(http + "/public/symbol"); + if (reply.is_array()) + for (auto &it : reply) + tickers.push_back({ + { "symbol", it.value("id", "") }, + {"tickPrice", stod(it.value("tickSize", "0")) }, + { "tickSize", stod(it.value("quantityIncrement", "0"))} + }); + return tickers; + }; + json handshake() const override { + const json reply = Curl::Web::xfer(http + "/public/symbol/" + base + quote); + return { + { "base", base == "USDT" ? "USD" : base }, + { "quote", quote == "USDT" ? "USD" : quote }, + { "symbol", reply.value("id", "") }, + {"tickPrice", stod(reply.value("tickSize", "0")) }, + { "tickSize", stod(reply.value("quantityIncrement", "0")) }, + { "minSize", stod(reply.value("quantityIncrement", "0")) }, + { "makeFee", stod(reply.value("provideLiquidityRate", "0"))}, + { "takeFee", stod(reply.value("takeLiquidityRate", "0")) }, + { "reply", reply } + }; + }; + void pairs(string &report) const override { + const json reply = Curl::Web::xfer(http + "/public/symbol"); + if (!reply.is_array() + or reply.empty() + or !reply.at(0).is_object() + or !reply.at(0).contains("baseCurrency") + or !reply.at(0).contains("quoteCurrency") + ) print("Error while reading pairs: " + reply.dump()); + else for (const json &it : reply) + report += it.value("baseCurrency", "") + "/" + it.value("quoteCurrency", "") + ANSI_NEW_LINE; + }; + string twin(const string &ws) const override { + return ws.substr(0, ws.length() - 6) + "trading"; + }; + json xfer(const string &url, const string &auth, const string &post) const { + return Curl::Web::xfer(url, "DELETE", post, {}, auth); + }; + }; + class GwBequant: virtual public GwHitBtc { + public: + GwBequant() + { + http = "https://api.bequant.io/api/2"; + ws = "wss://api.bequant.io/api/2/ws"; + webMarket = "https://bequant.io/exchange/"; + webOrders = "https://bequant.io/reports/orders"; + }; + }; + class GwCoinbase: public GwApiWsWs { + public: + GwCoinbase() + { + http = "https://api.coinbase.com/api/v3/brokerage"; + ws = "wss://advanced-trade-ws.coinbase.com"; + randId = Random::uuid36Id; + webMarket = "https://www.coinbase.com/advanced-trade/spot/"; + webOrders = "https://www.coinbase.com/orders/"; + }; + string web(const string &base, const string "e) const override { + return webMarket + base + "-" + quote; + }; + protected: +//BO non-free Gw class member functions from lib build-*/lib/K-*.a (it just redefines all virtual gateway functions below). +/**/ virtual string token(const string &crud = "", const string &url = "") const = 0; // return jwt token. +//EO non-free Gw class member functions from lib build-*/lib/K-*.a (it just redefines all virtual gateway functions above). + string nonce() const override { + return Random::char16Id(); + }; + json handshakes() const override { + json tickers = json::array(); + const json reply = xfer(http + "/products"); + if (reply.contains("products") + and reply.at("products").is_array() + ) for (auto &it : reply.at("products")) + tickers.push_back({ + { "symbol", it.value("product_id", "") }, + {"tickPrice", stod(it.value("quote_increment", "0"))}, + { "tickSize", stod(it.value("base_increment", "0")) } + }); + return tickers; + }; + json handshake() const override { + const json reply1 = xfer(http + "/products/" + base + "-" + quote); + const json reply2 = fees(); + return { + { "base", base }, + { "quote", quote }, + { "symbol", reply1.value("product_id", "") }, + {"tickPrice", stod(reply1.value("quote_increment", "0"))}, + { "tickSize", stod(reply1.value("base_increment", "0")) }, + { "minSize", stod(reply1.value("base_min_size", "0")) }, + { "makeFee", stod(reply2.value("maker_fee_rate", "0")) }, + { "takeFee", stod(reply2.value("taker_fee_rate", "0")) }, + { "reply", {reply1, reply2} } + }; + }; + void pairs(string &report) const override { + const json reply = xfer(http + "/products"); + if (!reply.is_object() + or reply.empty() + or !reply.contains("products") + or !reply.at("products").is_array() + or reply.at("products").empty() + or !reply.at("products").at(0).is_object() + or !reply.at("products").at(0).contains("base_currency_id") + or !reply.at("products").at(0).contains("quote_currency_id") + ) print("Error while reading pairs: " + reply.dump()); + else for (const json &it : reply.at("products")) + if (!it.value("trading_disabled", true) and it.value("status", "") == "online") + report += it.value("base_currency_id", "") + "/" + it.value("quote_currency_id", "") + ANSI_NEW_LINE; + }; + string twin(const string &ws) const override { + return string(ws).insert(ws.find("ws.") + 2, "-user"); + }; + json xfer(const string &url, const string &post = "", const string &crud = "GET") const { + return Curl::Web::xfer(url, crud, post, { + "Content-Type: application/json", + "Authorization: Bearer " + token(crud, url) + }); + }; + private: + json fees() const { + const json reply = xfer(http + "/transaction_summary?product_type=SPOT"); + if (!reply.is_object() + or !reply.contains("fee_tier") + ) { + print("Error while reading fees: " + reply.dump()); + return json::object(); + } + return reply.at("fee_tier"); + }; + }; + class GwBitfinex: public GwApiWs { + protected: + string trading = "exchange"; + public: + GwBitfinex() + { + http = "https://api-pub.bitfinex.com/v2"; + ws = "wss://api.bitfinex.com/ws/2"; + randId = Random::int45Id; + askForReplace = true; + webMarket = "https://www.bitfinex.com/trading/"; + webOrders = "https://www.bitfinex.com/reports/orders"; + }; + string web(const string &base, const string "e) const override { + return webMarket + base + quote; + }; + protected: + string nonce() const override { + return to_string(Tstamp * 1e+3); + }; + json handshakes() const override { + json tickers = json::array(); + const json reply = Curl::Web::xfer(http + "/tickers"); + if (reply.is_array()) + for (auto &it : reply) + tickers.push_back({ + { "symbol", it.at(0).get() }, + {"tickPrice", pow(10, fmax((int)log10( + it.at(6).get() + ), -4) -4) }, + { "tickSize", 1e-8 } + }); + return tickers; + }; + json handshake() const override { + json reply1 = { + {"object", Curl::Web::xfer(http + "/ticker/t" + base + quote)} + }; + if (reply1.at("object").is_array() + and reply1.at("object").size() > 6 + and reply1.at("object").at(6).is_number() + ) reply1["tickPrice"] = pow(10, fmax((int)log10( + reply1.at("object").at(6).get() + ), -4) -4); + json reply2 = { + {"object", Curl::Web::xfer(http + "/conf/pub:info:pair")} + }; + if (reply2.at("object").is_array() and !reply2.at("object").empty()) + for (const json &it : reply2.at("object").at(0)) { + if (it.at(0).is_string() + and it.at(0).get() == base + quote + and it.at(1).is_array() + and it.at(1).size() > 3 + and it.at(1).at(3).is_string() + ) { + reply2 = { + {"object", it} + }; + reply2["minSize"] = stod(reply2.at("object").at(1).at(3).get()); + break; + } + } + return { + { "base", base }, + { "quote", quote }, + { "symbol", base + quote }, + {"tickPrice", reply1.value("tickPrice", 0.0)}, + { "tickSize", 1e-8 }, + { "minSize", reply2.value("minSize", 0.0) }, + { "reply", {reply1, reply2} } + }; + }; + void pairs(string &report) const override { + const json reply = Curl::Web::xfer(http + "/conf/pub:list:pair:" + trading); + if (!reply.is_array() + or reply.empty() + or !reply.at(0).is_array() + or reply.at(0).empty() + or !reply.at(0).at(0).is_string() + ) print("Error while reading pairs: " + reply.dump()); + else for (const json &it : reply.at(0)) + if (it.get().find(":") != string::npos) + report += it.get().substr(0, it.get().find(":")) + "/" + + it.get().substr(it.get().find(":") + 1) + ANSI_NEW_LINE; + else + report += it.get().substr(0, 3) + "/" + + it.get().substr(3) + ANSI_NEW_LINE; + }; + json xfer(const string &url, const string &post, const string &h1, const string &h2, const string &h3) const { + return Curl::Web::xfer(url, "GET", post, { + "Content-Type: application/json", + "bfx-apikey: " + h1, + "bfx-nonce: " + h2, + "bfx-signature: " + h3 + }); + }; + }; + class GwEthfinex: virtual public GwBitfinex { + public: + GwEthfinex() + { + http = "https://api.ethfinex.com/v1"; + ws = "wss://api.ethfinex.com/ws/2"; + webMarket = "https://www.ethfinex.com/trading/"; + webOrders = "https://www.ethfinex.com/reports/orders"; + }; + }; + class GwKuCoin: public GwApiWs { + public: + GwKuCoin() + { + http = "https://api.kucoin.com"; + ws = "wss://push-private.kucoin.com/endpoint"; + randId = Random::uuid36Id; + webMarket = "https://trade.kucoin.com/"; + webOrders = "https://www.kucoin.com/order/trade"; + }; + string web(const string &base, const string "e) const override { + return webMarket + base + "-" + quote; + }; + protected: + string nonce() const override { + return to_string(Tstamp); + }; + json handshakes() const override { + json tickers = json::array(); + const json reply = Curl::Web::xfer(http + "/api/v1/symbols"); + if (reply.contains("data") and reply.at("data").is_array()) + for (auto &it : reply.at("data")) + tickers.push_back({ + { "symbol", it.value("symbol", "") }, + {"tickPrice", stod(it.value("priceIncrement", "0"))}, + { "tickSize", stod(it.value("baseIncrement", "0")) } + }); + return tickers; + }; + json handshake() const override { + json reply1 = Curl::Web::xfer(http + "/api/v1/symbols"); + if (reply1.contains("data") and reply1.at("data").is_array()) + for (const json &it : reply1.at("data")) + if (it.value("symbol", "") == base + "-" + quote) { + reply1 = it; + break; + } + const json reply2 = fees(); + return { + { "base", base }, + { "quote", quote }, + { "symbol", reply1.value("symbol", "") }, + {"tickPrice", stod(reply1.value("priceIncrement", "0"))}, + { "tickSize", stod(reply1.value("baseIncrement", "0")) }, + { "minSize", stod(reply1.value("baseMinSize", "0")) }, + { "makeFee", stod(reply2.value("makerFeeRate", "0")) }, + { "takeFee", stod(reply2.value("takerFeeRate", "0")) }, + { "reply", {reply1, reply2} } + }; + }; + void pairs(string &report) const override { + const json reply = Curl::Web::xfer(http + "/api/v1/symbols"); + if (!reply.is_object() + or !reply.contains("data") + or !reply.at("data").is_array() + or reply.at("data").empty() + or !reply.at("data").at(0).is_object() + or !reply.at("data").at(0).contains("enableTrading") + ) print("Error while reading pairs: " + reply.dump()); + else for (const json &it : reply.at("data")) + if (it.value("enableTrading", false)) + report += it.value("baseCurrency", "") + "/" + it.value("quoteCurrency", "") + ANSI_NEW_LINE; + }; + json xfer(const string &url, const string &h1, const string &h2, const string &h3, const string &h4, const string &crud, const string &post = "") const { + return Curl::Web::xfer(url, crud, post, { + "Content-Type: application/json", + "KC-API-KEY: " + h1, + "KC-API-SIGN: " + h2, + "KC-API-PASSPHRASE: " + h3, + "KC-API-TIMESTAMP: " + h4, + "KC-API-KEY-VERSION: 2" + }); + }; + private: + json fees() const { + const string crud = "GET", + path = "/api/v1/base-fee", + time = nonce(), + hash = time + crud + path, + sign = Text::B64(Text::HMAC256(hash, secret, true)), + code = Text::B64(Text::HMAC256(apikeyid, secret, true)); + const json reply = xfer(http + path, apikey, sign, code, time, crud); + if (!reply.contains("code") + or !reply.at("code").is_string() + or reply.value("code", "") != "200000" + or !reply.contains("data") + or !reply.at("data").is_object() + ) { + print("Error while reading fees: " + reply.dump()); + return json::object(); + } + return reply.at("data"); + }; + }; + class GwKraken: public GwApiWsWs { + public: + GwKraken() + { + http = "https://api.kraken.com"; + ws = "wss://ws.kraken.com"; + randId = Random::int32Id; + webMarket = "https://trade.kraken.com/charts/KRAKEN:"; + webOrders = "https://www.kraken.com/u/trade"; + }; + string web(const string &base, const string "e) const override { + return webMarket + base + "-" + quote; + }; + protected: + string nonce() const override { + return to_string(Tstamp); + }; + json handshakes() const override { + json tickers = json::array(); + const json reply = Curl::Web::xfer(http + "/0/public/AssetPairs"); + if (reply.contains("result")) + for (auto &it : reply.at("result")) + tickers.push_back({ + { "symbol", it.value("wsname", "") }, + {"tickPrice", pow(10, -it.value("pair_decimals", 0))}, + { "tickSize", pow(10, -it.value("lot_decimals", 0)) } + }); + return tickers; + }; + json handshake() const override { + json reply = Curl::Web::xfer(http + "/0/public/AssetPairs?pair=" + base + quote); + if (reply.contains("result")) + for (const json &it : reply.at("result")) + if (it.contains("pair_decimals")) { + reply = it; + break; + } + return { + { "base", base }, + { "quote", quote }, + { "symbol", reply.value("wsname", "") }, + {"tickPrice", pow(10, -reply.value("pair_decimals", 0))}, + { "tickSize", pow(10, -reply.value("lot_decimals", 0)) }, + { "minSize", pow(10, -reply.value("lot_decimals", 0)) }, + { "reply", reply } + }; + }; + void pairs(string &report) const override { + const json reply = Curl::Web::xfer(http + "/0/public/AssetPairs"); + if (!reply.is_object() + or !reply.contains("result") + or !reply.at("result").is_object() + ) print("Error while reading pairs: " + reply.dump()); + else for (const json &it : reply.at("result")) + if (it.contains("wsname")) + report += it.value("wsname", "") + ANSI_NEW_LINE; + }; + string twin(const string &ws) const override { + return string(ws).insert(ws.find("ws.") + 2, "-auth"); + }; + json xfer(const string &url, const string &h1, const string &h2, const string &post) const { + return Curl::Web::xfer(url, "GET", post, { + "API-Key: " + h1, + "API-Sign: " + h2 + }); + }; + }; + class GwPoloniex: public GwApiWs { + public: + GwPoloniex() + { + http = "https://api.poloniex.com"; + ws = "wss://ws.poloniex.com"; + randId = Random::int45Id; + webMarket = "https://poloniex.com/trade/"; + webOrders = "https://poloniex.com/activity/spot/open-orders"; + }; + string web(const string &base, const string "e) const override { + return webMarket + quote + "_" + base; + }; + protected: + string nonce() const override { + return to_string(Tstamp); + }; + json handshakes() const override { + json tickers = json::array(); + const json reply = Curl::Web::xfer(http + "/markets"); + if (reply.is_array()) + for (auto &it : reply) + tickers.push_back({ + { "symbol", it.at("symbolTradeLimit").value("symbol", "") }, + {"tickPrice", pow(10, -it.at("symbolTradeLimit").value("priceScale", 0)) }, + { "tickSize", pow(10, -it.at("symbolTradeLimit").value("quantityScale", 0))} + }); + return tickers; + }; + json handshake() const override { + json reply = Curl::Web::xfer(http + "/markets/" + base + "_" + quote); + if (reply.is_array() and !reply.empty() and reply.at(0).value("state", "") == "NORMAL" and reply.at(0).at("symbolTradeLimit").is_object()) + reply = reply.at(0).at("symbolTradeLimit"); + return { + { "base", base }, + { "quote", quote }, + { "symbol", reply.value("symbol", "") }, + {"tickPrice", pow(10, -reply.value("priceScale", 0)) }, + { "tickSize", pow(10, -reply.value("quantityScale", 0))}, + { "minSize", stod(reply.value("minQuantity", "0")) }, + { "reply", reply } + }; + }; + void pairs(string &report) const override { + const json reply = Curl::Web::xfer(http + "/markets"); + if (!reply.is_array()) + print("Error while reading pairs: " + reply.dump()); + else for (auto &it : reply) + if (it.value("state", "") == "NORMAL") + report += it.value("displayName", "") + ANSI_NEW_LINE; + }; + json xfer(const string &url, const string &post, const string &h1, const string &h2, const string &h3) const { + return Curl::Web::xfer(url, "GET", post, { + "Content-Type: application/json", + "key: " + h1, + "signature: " + h2, + "signTimestamp: " + h3 + }); + }; + }; +} diff --git a/src/lib/Krypto.ninja-bots.h b/src/lib/Krypto.ninja-bots.h new file mode 100644 index 000000000..a9d74ac6a --- /dev/null +++ b/src/lib/Krypto.ninja-bots.h @@ -0,0 +1,1668 @@ +//! \file +//! \brief Minimal user application framework. + +namespace ₿ { + static string epilogue, epitaph; + + //! \brief Call all endingFn once and print a last log msg. + //! \param[in] reason Allows any (colorful?) string. + static void exit(const string &reason = "") { + epilogue = reason + string(!(reason.empty() or reason.back() == '.'), '.'); + raise(SIGBREAK); + }; + + //! \brief Call all endingFn once and print a last error log msg. + //! \param[in] prefix Allows any string, if possible with a length of 2. + //! \param[in] reason Allows any (colorful?) string. + static void error(const string &prefix, const string &reason) { + exit(prefix + ANSI_PUKE_RED + " Errrror: " + ANSI_HIGH_RED + reason); + }; + + class Rollout { + public: + Rollout() { + static once_flag rollout; + call_once(rollout, version); + }; + protected: + static string changelog() { + string mods; + const json diff = +#ifndef NDEBUG + json::object(); +#else + Curl::Web::xfer("https://api.github.com/repos/ctubio/" + "Krypto-trading-bot/compare/" K_HEAD "...HEAD"); +#endif + if (diff.value("ahead_by", 0) + and diff.contains("commits") + and diff.at("commits").is_array() + ) for (const json &it : diff.at("commits")) + mods += it.value("/commit/author/date"_json_pointer, "").substr(0, 10) + " " + + it.value("/commit/author/date"_json_pointer, "").substr(11, 8) + + " (" + it.value("sha", "").substr(0, 7) + ") " + + it.value("/commit/message"_json_pointer, "").substr(0, + it.value("/commit/message"_json_pointer, "").find("\n\n") + 1 + ); + return mods; + }; + private: + static void version() { + clog << ANSI_HIGH_GREEN << K_SOURCE " " K_BUILD + << ANSI_PUKE_GREEN << " (build on " K_CHOST " at " K_STAMP ")" +#ifndef NDEBUG + << ANSI_HIGH_GREEN << " with DEBUG MODE enabled" + << ANSI_PUKE_GREEN +#endif + << '.' << ANSI_RESET ANSI_NEW_LINE; + }; + }; + + static vector> endingFn; + + static volatile sig_atomic_t sigscr = SIGWINCH; + + class Ending: public Rollout { + public_friend: + using QuitEvent = function; + public: + Ending() { + ::signal(SIGPIPE, SIG_IGN); + ::signal(SIGINT, [](const int) { + clog << ANSI_NEW_LINE; + raise(SIGBREAK); + }); + ::signal(SIGBREAK, die); + ::signal(SIGTERM, err); + ::signal(SIGABRT, wtf); + ::signal(SIGSEGV, wtf); + ::signal(SIGUSR1, wtf); + ::signal(SIGWINCH, meh); + }; + void ending(const QuitEvent &fn) { + endingFn.push_back(fn); + }; + private: + static void meh(const int sig) { + sigscr = sig; + }; + static void halt(const int code) { + vector> happyEndingFn; + endingFn.swap(happyEndingFn); + for (const auto &it : happyEndingFn) it(); + colorful = true; + clog << ANSI_HIGH_GREEN << 'K' + << ANSI_PUKE_GREEN << " exit code " + << ANSI_HIGH_GREEN << code + << ANSI_PUKE_GREEN << '.' + << ANSI_RESET ANSI_NEW_LINE; + EXIT(code); + }; + static void die(const int) { + if (epilogue.empty()) + epilogue = "Excellent decision! " + + Curl::Web::xfer("https://api.chucknorris.io/jokes/random?category=dev") + .value("value", "let's plant a tree instead.."); + halt( + epilogue.find("Errrror") == string::npos + ? EXIT_SUCCESS + : EXIT_FAILURE + ); + }; + static void err(const int) { + if (epilogue.empty()) epilogue = "Unknown exit reason, no joke."; + halt(EXIT_FAILURE); + }; + static void wtf(const int sig) { + epilogue = ANSI_HIGH_CYAN + "Errrror: " + strsignal(sig) + ' '; + const string mods = changelog(); + if (mods.empty()) { + epilogue += "(Three-Headed Monkey found):" ANSI_NEW_LINE + epitaph + + "- binbuild: " K_SOURCE " " K_CHOST ANSI_NEW_LINE + + "- numpatch: " K_BUILD ANSI_NEW_LINE + "- lastbeat: " + to_string(Tspent) + ANSI_NEW_LINE +#ifndef _WIN32 + + "- tracelog: " ANSI_NEW_LINE; + void *k[69]; + size_t jumps = backtrace(k, sizeof(k) / sizeof(void*)); + char **trace = backtrace_symbols(k, jumps); + for (; + jumps --> 0; + epilogue += " " + to_string(jumps) + ": " + string(trace[jumps]) + ANSI_NEW_LINE + ); + free(trace) +#endif + ; + epilogue += ANSI_NEW_LINE + + ANSI_HIGH_RED + "Yikes!" + ANSI_PUKE_RED + + ANSI_NEW_LINE "please copy and paste the error above into a new github issue (noworry for duplicates)." + ANSI_NEW_LINE "If you agree, go to https://github.com/ctubio/Krypto-trading-bot/issues/new" + ANSI_NEW_LINE; + } else + epilogue += string("(deprecated K version found).") + ANSI_NEW_LINE ANSI_NEW_LINE + + ANSI_HIGH_YELLOW + "Hint!" + ANSI_PUKE_YELLOW + + ANSI_NEW_LINE "please upgrade to the latest commit; the encountered error may be already fixed at:" + ANSI_NEW_LINE + mods + + ANSI_NEW_LINE "If you agree, consider to run \"make upgrade\" prior further executions." + ANSI_NEW_LINE; + halt(EXIT_FAILURE); + }; + }; + + class Terminal { + public: + struct { + string (*terminal)() = nullptr; + mutable unsigned int width = 80, + height = 0; + } display; + protected: + bool gobeep = false; +#ifndef _WIN32 + struct termios original = {}; +#endif + private: + mutable Clock warned = 0; + mutable vector clogs; + mutable unsigned int frame = 0; + const vector spinner = { "∙∙∙", "∙∙●", "∙●∙", "●∙∙" }; + public: + void resize() const { + sigscr = 0; +#ifdef _WIN32 + CONSOLE_SCREEN_BUFFER_INFO ws; + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &ws); + display.width = ws.srWindow.Right - ws.srWindow.Left + 1; + display.height = ws.srWindow.Bottom - ws.srWindow.Top + 1; +#else + struct winsize ws; + ioctl(1, TIOCGWINSZ, &ws); + display.width = ws.ws_col; + display.height = ws.ws_row; +#endif + }; + void repaint(const bool &spin = false) const { + if (display.height) { + if (spin) frame = (frame + 1) % 4; + if (sigscr) resize(); + clog << ANSI_HIDE_CURSOR + << ANSI_TOP_RIGHT + << display.terminal() + << ANSI_RESET + << ANSI_SHOW_CURSOR; + } + }; + void log(const string &prefix, const string &reason, const string &highlight = "") const { + const string space = highlight.empty() + ? "" + : string(reason.back() != '=', ' '); + puke_rainbow(stamp() + + prefix + ' ' + ANSI_PUKE_WHITE + + reason + space + ANSI_HIGH_YELLOW + + highlight + ); + }; + void warn(const string &prefix, const string &reason, const Clock &ratelimit = 0) const { + if (ratelimit) { + const Clock now = Tstamp; + if (warned + ratelimit > now) return; + warned = now; + } + puke_rainbow(stamp() + + prefix + ANSI_PUKE_RED + + " Warrrrning: " + lines(reason) + ); + }; + void beep() const { + if (gobeep) + clog << ANSI_BELL; + }; + string spin() { + return ANSI_HIGH_YELLOW + + spinner.at(frame) + + ANSI_PUKE_WHITE; + }; + string logs(const unsigned int &rows, const string &prefix) { + string lines; + const unsigned int empty_rows = fmax(0, display.height - rows - 1); + if (clogs.size() > empty_rows) + clogs.erase(clogs.begin(), clogs.end() - empty_rows); + else while (clogs.size() < empty_rows) + clogs.insert(clogs.begin(), " "); + for (auto &it : clogs) { + if (it.length() < 6 or it.substr(it.length() - 6) != ANSI_END_LINE) + it.append(ANSI_END_LINE) + .insert(0, ANSI_PUKE_WHITE + prefix); + lines += it; + } + return lines + "│ " + ANSI_END_LINE; + }; + protected: + bool windowed() { + if (display.terminal) { +#ifndef _WIN32 + struct termios t; + tcgetattr(1, &t); + original = t; + cfmakeraw(&t); + tcsetattr(1, TCSANOW, &t); +#endif + clog << ANSI_ALTERNATIVE + ANSI_CURSOR; + resize(); + return true; + } + return false; + }; + void with_goodbye() { + if (display.height) { + display = {}; +#ifndef _WIN32 + tcsetattr(1, TCSANOW, &original); +#endif + clog << ANSI_ORIGINAL; + } + clog << stamp() + << epilogue + << (epilogue.empty() ? "" : ANSI_NEW_LINE); + }; + private: + string lines(string reason) const { + size_t n = 0; + while ((n = reason.find(ANSI_NEW_LINE, n + 2)) != string::npos) + reason.insert(n + 2, ANSI_HIGH_RED); + return ANSI_HIGH_RED + reason; + }; + void puke_rainbow(const string &rain) const { + string puke = rain + ANSI_PUKE_WHITE + + '.' + ANSI_RESET + + ANSI_NEW_LINE; +#ifdef NDEBUG + if (!display.height) + clog << puke; +#endif + if (display.terminal) { + size_t n = 0; + while ((n = puke.find(ANSI_NEW_LINE)) != string::npos) { + clogs.emplace_back(puke.begin(), puke.begin() + n); + puke.erase(0, n + 2); + } + repaint(); + } + }; + string stamp() const { + chrono::system_clock::time_point clock = chrono::system_clock::now(); + chrono::system_clock::duration t = clock.time_since_epoch(); + t -= chrono::duration_cast(t); + auto milliseconds = chrono::duration_cast(t); + t -= milliseconds; + auto microseconds = chrono::duration_cast(t); + stringstream microtime; + microtime << setfill('0') << '.' + << setw(3) << milliseconds.count() + << setw(3) << microseconds.count(); + time_t tt = chrono::system_clock::to_time_t(clock); + char datetime[15]; + strftime(datetime, sizeof(datetime), "%m/%d %H:%M:%S", localtime(&tt)); + return ANSI_HIGH_GREEN + + datetime + ANSI_PUKE_GREEN + + microtime.str() + ANSI_HIGH_WHITE + + ' '; + }; + }; + + class Option: public Terminal { + private_friend: + struct Argument { + const string name; + const string defined_value; + const char *default_value; + const string help; + }; + protected: + using MutableUserArguments = unordered_map>; + struct { + vector options; + function fn = nullptr; + } arguments; + private: + MutableUserArguments args; + public: + template const T arg(const string &name) const { +#ifndef NDEBUG + if (!args.contains(name)) return T(); +#endif + return get(args.at(name)); + }; + protected: + void optional_setup(int argc, char** argv, const bool &proactive, const bool &blackhole, const bool &unmounted) { + args["autobot"] = + args["headless"] = unmounted; + args["naked"] = !display.terminal; + vector long_options = { + {"INFORMATION", "", nullptr, ""}, + {"help", "h", nullptr, "print this help and quit"}, + {"version", "v", nullptr, "print current build version and quit"}, + {"latency", "1", nullptr, "print current HTTP latency (not from WS) and quit"}, + {"list", "1", nullptr, "print current available currency pairs and quit"}, + {"CREDENTIALS", "", nullptr, ""}, + {"exchange", "NAME", "", "set exchange NAME for trading, mandatory"}, + {"currency", "PAIR", "", "set currency PAIR for trading, use format ISO 4217-A3" + ANSI_NEW_LINE "with '/' separator, like 'BTC/EUR', mandatory"}, + {"apikey", "WORD", "", "set (never share!) WORD as api key name for trading, mandatory"}, + {"secret", "WORD", "", "set (never share!) WORD as api secret for trading, mandatory"}, + {"apikeyid", "WORD", "", "set (never share!) WORD as api key id for trading"}, + {"ENDPOINTS", "", nullptr, ""}, + {"http", "URL", "", "set URL of alernative HTTPS api endpoint for trading"}, + {"wss", "URL", "", "set URL of alernative WSS api endpoint for trading"}, + {"NETWORK", "", nullptr, ""}, + {"nocache", "1", nullptr, "do not cache handshakes 7 hours at " K_HOME "/cache"}, + {"interface", "IP", "", "set IP to bind as outgoing network interface"}, + {"ipv6", "1", nullptr, "use IPv6 when possible"} + }; + if (!arg("headless")) for (const Argument &it : (vector){ + {"ADMIN", "", nullptr, ""}, + {"headless", "1", nullptr, "do not listen for UI connections," + ANSI_NEW_LINE "all other UI related arguments will be ignored"}, + {"without-ssl", "1", nullptr, "do not use HTTPS for UI connections (use HTTP only)"}, + {"whitelist", "IP", "", "set IP or csv of IPs to allow UI connections," + ANSI_NEW_LINE "alien IPs will get a zip-bomb instead"}, + {"client-limit", "NUMBER", "7", "set NUMBER of maximum concurrent UI connections"}, + {"port", "NUMBER", "3000", "set NUMBER of an open port to listen for UI connections" + ANSI_NEW_LINE "default NUMBER is '3000'"}, + {"user", "WORD", "", "set allowed WORD as username for UI basic authentication"}, + {"pass", "WORD", "", "set allowed WORD as password for UI basic authentication"}, + {"ssl-crt", "FILE", "", "set FILE to custom SSL .crt file for HTTPS UI connections" + ANSI_NEW_LINE "(see www.akadia.com/services/ssh_test_certificate.html)"}, + {"ssl-key", "FILE", "", "set FILE to custom SSL .key file for HTTPS UI connections" + ANSI_NEW_LINE "(the passphrase MUST be removed from the .key file!)"}, + {"matryoshka", "URL", "https://example.com/", "set Matryoshka link URL of the next UI"}, + {"ignore-sun", "2", nullptr, "do not switch UI to light theme on daylight"}, + {"ignore-moon", "1", nullptr, "do not switch UI to dark theme on moonlight"} + }) long_options.push_back(it); + for (const Argument &it : (vector){ + {"title", "NAME", K_SOURCE, "set NAME to allow admins to identify different bots"}, + {"BOT", "", nullptr, ""} + }) long_options.push_back(it); + if (!arg("autobot")) long_options.push_back( + {"autobot", "1", nullptr, "automatically start trading on boot"} + ); + for (const Argument &it : arguments.options) + long_options.push_back(it); + arguments.options.clear(); + for (const Argument &it : (vector){ + {"heartbeat", "1", nullptr, "print detailed output about most important data"}, + {"debug-orders", "1", nullptr, "print detailed output about order states"}, + {"debug-quotes", "1", nullptr, "print detailed output about quoting engine"}, + {"debug-secret", "1", nullptr, "print (never share!) secret exchange messages"}, + {"debug", "1", nullptr, "print detailed output about all the (previous) things!"}, + {"colors", "1", nullptr, "print highlighted output"}, + }) long_options.push_back(it); + if (!arg("naked")) long_options.push_back( + {"naked", "1", nullptr, "do not display CLI, print output to stdout instead"} + ); + if (proactive) long_options.push_back( + {"beep", "1", nullptr, "make computer go beep on filled orders"} + ); + if (!blackhole) long_options.push_back( + {"database", "FILE", "", "set alternative PATH to database filename," + ANSI_NEW_LINE "default PATH is '" K_HOME "/db/K-*.db'," + ANSI_NEW_LINE "or use ':memory:' (see sqlite.org/inmemorydb.html)"} + ); + long_options.push_back( + {"free-version", "1", nullptr, "slowdown market levels 121 seconds"} + ); + vector io_options(find_if( + long_options.begin(), long_options.end(), + [&](const Argument &it) { + return "heartbeat" == it.name.substr(0, 9); + } + ), long_options.end()); + long_options = vector( + long_options.begin(), + long_options.end() - io_options.size() + ); + long_options.push_back( + {"market-limit", "NUMBER", "321", "set NUMBER of maximum market levels saved in memory," + ANSI_NEW_LINE "default NUMBER is '321' and the minimum is '10'"} + ); + if (proactive) long_options.push_back( + {"lifetime", "NUMBER", "0", "set NUMBER of minimum milliseconds to keep orders open," + ANSI_NEW_LINE "otherwise open orders can be replaced anytime required"} + ); + long_options.push_back( + {"I/O", "", nullptr, ""} + ); + for (const Argument &it : io_options) + long_options.push_back(it); + int index = ANY_NUM; + vector