├── .dockerignore ├── .env.development ├── .env.production ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.MD ├── backend ├── __init__.py ├── cli.py ├── config │ ├── __init__.py │ ├── docker │ │ ├── dashboard.dockerfile │ │ ├── database.dockerfile │ │ ├── entrypoint_listener.sh │ │ ├── entrypoint_webserver.sh │ │ ├── listener.dockerfile │ │ ├── listener_req.txt │ │ ├── mysqld.cnf │ │ └── nginx.conf │ ├── pynance-core.txt │ └── system.py ├── jinja.py ├── models │ ├── bot.py │ ├── config.py │ ├── keys.py │ ├── orders.py │ ├── status.py │ └── system.py ├── utils │ ├── auth.py │ └── trading │ │ ├── __init__.py │ │ ├── future.py │ │ └── spot.py └── views │ ├── __init__.py │ ├── api │ ├── backup.py │ ├── coinmarketcal.py │ ├── history.py │ ├── keys.py │ ├── klines.py │ ├── logic.py │ ├── prices.py │ ├── system.py │ ├── trades.py │ └── wallet.py │ └── homepage │ └── __init__.py ├── docker-compose.yaml ├── docs ├── _config.yml └── index.md ├── frontend ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── public │ ├── favicon.ico │ ├── img │ │ └── icons │ │ │ ├── android-chrome-192x192.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── mstile-150x150.png │ │ │ └── safari-pinned-tab.svg │ ├── index.html │ └── robots.txt ├── src │ ├── App.vue │ ├── assets │ │ ├── flags │ │ │ ├── american.png │ │ │ ├── dutch.png │ │ │ ├── filipino.png │ │ │ ├── french.png │ │ │ └── unknown.png │ │ ├── future_orders.png │ │ ├── ico.png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── prices.png │ │ ├── stat_1.png │ │ ├── stat_2.png │ │ └── stonks │ │ │ ├── 1.png │ │ │ ├── 10.png │ │ │ ├── 100.png │ │ │ ├── 20.png │ │ │ ├── 25.png │ │ │ ├── 40.png │ │ │ ├── 50.png │ │ │ ├── 60.png │ │ │ ├── 80.png │ │ │ ├── 90.png │ │ │ └── terror.gif │ ├── components │ │ ├── Backup.vue │ │ ├── CoinMarketCal.vue │ │ ├── CurrentPrices.vue │ │ ├── FirstTimeSetup.vue │ │ ├── FutureConfig.vue │ │ ├── HelpComponents │ │ │ └── Menu.vue │ │ ├── Knightrider.vue │ │ ├── Login.vue │ │ ├── SpotConfig.vue │ │ ├── discord.vue │ │ └── walletInfo.vue │ ├── layout │ │ ├── DrawerMenu.vue │ │ └── SystemBar.vue │ ├── main.ts │ ├── plugins │ │ ├── bootstrap-vue.js │ │ ├── gcharts.js │ │ └── vuetify.ts │ ├── registerServiceWorker.ts │ ├── router │ │ └── index.ts │ ├── shims-tsx.d.ts │ ├── shims-vue.d.ts │ ├── shims-vuetify.d.ts │ ├── store │ │ ├── backup.ts │ │ ├── bot.ts │ │ ├── futures.ts │ │ ├── index.ts │ │ ├── keys.ts │ │ ├── prices.ts │ │ ├── system.ts │ │ └── trades.ts │ └── views │ │ ├── Authenticate.vue │ │ ├── Config.vue │ │ ├── Guide.vue │ │ ├── News.vue │ │ ├── Statistics.vue │ │ ├── Trades.vue │ │ └── Wallet.vue ├── tsconfig.json └── vue.config.js ├── listener.py ├── requirements.txt ├── reset_docker.bat └── webserver.py /.dockerignore: -------------------------------------------------------------------------------- 1 | # These are some examples of commonly ignored file patterns. 2 | # You should customize this list as applicable to your project. 3 | # Learn more about .gitignore: 4 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 5 | 6 | # Node artifact files 7 | node_modules/ 8 | **/node_modules/ 9 | dist/ 10 | 11 | # Compiled Java class files 12 | *.class 13 | 14 | # Compiled Python bytecode 15 | *.py[cod] 16 | 17 | # Log files 18 | *.log 19 | 20 | # Package files 21 | *.jar 22 | 23 | # Maven 24 | target/ 25 | dist/ 26 | 27 | # JetBrains IDE 28 | .idea/ 29 | 30 | # Unit test reports 31 | TEST*.xml 32 | 33 | # Generated by MacOS 34 | .DS_Store 35 | 36 | # Generated by Windows 37 | Thumbs.db 38 | 39 | # Applications 40 | *.app 41 | *.exe 42 | *.war 43 | 44 | # Large media files 45 | *.mp4 46 | *.tiff 47 | *.avi 48 | *.flv 49 | *.mov 50 | *.wmv 51 | 52 | # Logs 53 | logs 54 | *.log 55 | npm-debug.log* 56 | yarn-debug.log* 57 | yarn-error.log* 58 | lerna-debug.log* 59 | 60 | # Diagnostic reports (https://nodejs.org/api/report.html) 61 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 62 | 63 | # Runtime data 64 | pids 65 | *.pid 66 | *.seed 67 | *.pid.lock 68 | 69 | # Directory for instrumented libs generated by jscoverage/JSCover 70 | lib-cov 71 | 72 | # Coverage directory used by tools like istanbul 73 | coverage 74 | *.lcov 75 | 76 | # nyc test coverage 77 | .nyc_output 78 | 79 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 80 | .grunt 81 | 82 | # Bower dependency directory (https://bower.io/) 83 | bower_components 84 | 85 | # node-waf configuration 86 | .lock-wscript 87 | 88 | # Compiled binary addons (https://nodejs.org/api/addons.html) 89 | build/Release 90 | 91 | # Dependency directories 92 | node_modules/ 93 | jspm_packages/ 94 | 95 | # TypeScript v1 declaration files 96 | typings/ 97 | 98 | # TypeScript cache 99 | *.tsbuildinfo 100 | 101 | # Optional npm cache directory 102 | .npm 103 | 104 | # Optional eslint cache 105 | .eslintcache 106 | 107 | # Microbundle cache 108 | .rpt2_cache/ 109 | .rts2_cache_cjs/ 110 | .rts2_cache_es/ 111 | .rts2_cache_umd/ 112 | 113 | # Optional REPL history 114 | .node_repl_history 115 | 116 | # Output of 'npm pack' 117 | *.tgz 118 | 119 | # Yarn Integrity file 120 | .yarn-integrity 121 | 122 | # dotenv environment variables file 123 | .env 124 | .env.test 125 | 126 | # parcel-bundler cache (https://parceljs.org/) 127 | .cache 128 | 129 | # Next.js build output 130 | .next 131 | 132 | # Nuxt.js build / generate output 133 | .nuxt 134 | dist 135 | 136 | # Gatsby files 137 | .cache/ 138 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 139 | # https://nextjs.org/blog/next-9-1#public-directory-support 140 | # public 141 | 142 | # vuepress build output 143 | .vuepress/dist 144 | 145 | # Serverless directories 146 | .serverless/ 147 | 148 | # FuseBox cache 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | .dynamodb/ 153 | 154 | # TernJS port file 155 | .tern-port 156 | 157 | # Byte-compiled / optimized / DLL files 158 | __pycache__/ 159 | *.py[cod] 160 | *$py.class 161 | 162 | # C extensions 163 | *.so 164 | 165 | # Distribution / packaging 166 | .Python 167 | build/ 168 | develop-eggs/ 169 | dist/ 170 | downloads/ 171 | eggs/ 172 | .eggs/ 173 | lib/ 174 | lib64/ 175 | parts/ 176 | sdist/ 177 | var/ 178 | wheels/ 179 | pip-wheel-metadata/ 180 | share/python-wheels/ 181 | *.egg-info/ 182 | .installed.cfg 183 | *.egg 184 | MANIFEST 185 | 186 | # PyInstaller 187 | # Usually these files are written by a python script from a template 188 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 189 | *.manifest 190 | *.spec 191 | 192 | # Installer logs 193 | pip-log.txt 194 | pip-delete-this-directory.txt 195 | 196 | # Unit test / coverage reports 197 | htmlcov/ 198 | .tox/ 199 | .nox/ 200 | .coverage 201 | .coverage.* 202 | .cache 203 | nosetests.xml 204 | coverage.xml 205 | *.cover 206 | *.py,cover 207 | .hypothesis/ 208 | .pytest_cache/ 209 | 210 | # Translations 211 | *.mo 212 | *.pot 213 | 214 | # Django stuff: 215 | *.log 216 | local_settings.py 217 | db.sqlite3 218 | db.sqlite3-journal 219 | 220 | # Flask stuff: 221 | instance/ 222 | .webassets-cache 223 | 224 | # Scrapy stuff: 225 | .scrapy 226 | 227 | # Sphinx documentation 228 | docs/_build/ 229 | 230 | # PyBuilder 231 | target/ 232 | 233 | # Jupyter Notebook 234 | .ipynb_checkpoints 235 | 236 | # IPython 237 | profile_default/ 238 | ipython_config.py 239 | 240 | # pyenv 241 | .python-version 242 | 243 | # pipenv 244 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 245 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 246 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 247 | # install all needed dependencies. 248 | #Pipfile.lock 249 | 250 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 251 | __pypackages__/ 252 | 253 | # Celery stuff 254 | celerybeat-schedule 255 | celerybeat.pid 256 | 257 | # SageMath parsed files 258 | *.sage.py 259 | 260 | # Environments 261 | .env 262 | .venv 263 | env/ 264 | venv/ 265 | ENV/ 266 | env.bak/ 267 | venv.bak/ 268 | 269 | # Spyder project settings 270 | .spyderproject 271 | .spyproject 272 | 273 | # Rope project settings 274 | .ropeproject 275 | 276 | # mkdocs documentation 277 | /site 278 | 279 | # mypy 280 | .mypy_cache/ 281 | .dmypy.json 282 | dmypy.json 283 | 284 | # Pyre type checker 285 | .pyre/ 286 | migrations/ 287 | 288 | # Visual Studio Code 289 | .vscode 290 | 291 | # Project builds 292 | backend/templates/vue 293 | backend/static/vue 294 | 295 | package-lock.json 296 | 297 | .git 298 | 299 | .tar.gz 300 | 301 | .whl 302 | 303 | ./reset_docker.bat 304 | 305 | frontend/node_modules 306 | dist/ 307 | .github 308 | .git 309 | 310 | ./migrations -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | // This file represents the Development environment 2 | ENVIRONMENT=Development 3 | // If you find yourself stuck, Take the .env.development file as an example. 4 | // Never share these settings, keep in mind that PyNance NEVER asks you any information besides 5 | // what is needed to actually run PyNance. This is everything we need. 6 | 7 | // MODIFY TO YOUR LIKINGS 8 | // // Your Binance credentials 9 | BINANCE_API_KEY=oepK24J3sKucEaTHd9EuHI9FfgHp8r7jOAxwmM1rwKDsOpn5XJgHrTUqazb5isca 10 | BINANCE_API_SECRET=SSFSWtBcI9ew5UnOMH4I6JiCujijmEVdA8b0EIHbXTN6z5ZVvjGI7lk3fJSk8PDD 11 | 12 | // // Your prefered language, choose out of: [en] 13 | LOCALE=en 14 | 15 | // Advanced settings 16 | // // Change your database Password. This password needs to match in the docker-compose.yaml file. 17 | // // Don't change anything else other than the password 18 | SQLALCHEMY_DATABASE_URI=mysql+pymysql://user:admin@127.0.0.1:3306/pynance3 19 | // // Turn False to get less logs (if you watch them that is) 20 | SQLALCHEMY_TRACK_MODIFICATIONS=True 21 | 22 | // The next configuration has to be unmodified in order to keep docekr happy, my advice is 23 | // Do not touch. 24 | DEBUG=False 25 | FALLBACK_LOCALE=en 26 | SERVER_BACKEND=http://pn_dashboard:5000 -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | // This file represents the Development environment 2 | ENVIRONMENT=Production 3 | // If you find yourself stuck, Take the .env.development file as an example. 4 | // Never share these settings, keep in mind that PyNance NEVER asks you any information besides 5 | // what is needed to actually run PyNance. This is everything we need. 6 | 7 | // MODIFY TO YOUR LIKINGS 8 | // // Your Binance credentials 9 | BINANCE_API_KEY=oepK24J3sKucEaTHd9EuHI9FfgHp8r7jOAxwmM1rwKDsOpn5XJgHrTUqazb5isca 10 | BINANCE_API_SECRET=SSFSWtBcI9ew5UnOMH4I6JiCujijmEVdA8b0EIHbXTN6z5ZVvjGI7lk3fJSk8PDD 11 | 12 | // // Your prefered language, choose out of: [en] 13 | LOCALE=en 14 | 15 | // Advanced settings 16 | // // Change your database Password. This password needs to match in the docker-compose.yaml file. 17 | // // Don't change anything else other than the password 18 | SQLALCHEMY_DATABASE_URI=mysql+pymysql://root:PyNanceV3@pn_database:3306/PYNANCE 19 | // // Turn False to get less logs (if you watch them that is) 20 | SQLALCHEMY_TRACK_MODIFICATIONS=True 21 | 22 | // The next configuration has to be unmodified in order to keep docekr happy, my advice is 23 | // Do not touch. 24 | DEBUG=False 25 | FALLBACK_LOCALE=en 26 | SERVER_BACKEND=http://pn_dashboard:5000 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # These are some examples of commonly ignored file patterns. 2 | # You should customize this list as applicable to your project. 3 | # Learn more about .gitignore: 4 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 5 | 6 | # Node artifact files 7 | node_modules/ 8 | dist/ 9 | 10 | # Compiled Java class files 11 | *.class 12 | 13 | # Compiled Python bytecode 14 | *.py[cod] 15 | 16 | # Log files 17 | *.log 18 | 19 | # Package files 20 | *.jar 21 | 22 | # Maven 23 | target/ 24 | dist/ 25 | 26 | # JetBrains IDE 27 | .idea/ 28 | 29 | # Unit test reports 30 | TEST*.xml 31 | 32 | # Generated by MacOS 33 | .DS_Store 34 | 35 | # Generated by Windows 36 | Thumbs.db 37 | 38 | # Applications 39 | *.app 40 | *.exe 41 | *.war 42 | 43 | # Large media files 44 | *.mp4 45 | *.tiff 46 | *.avi 47 | *.flv 48 | *.mov 49 | *.wmv 50 | 51 | # Logs 52 | logs 53 | *.log 54 | npm-debug.log* 55 | yarn-debug.log* 56 | yarn-error.log* 57 | lerna-debug.log* 58 | 59 | # Diagnostic reports (https://nodejs.org/api/report.html) 60 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 61 | 62 | # Runtime data 63 | pids 64 | *.pid 65 | *.seed 66 | *.pid.lock 67 | 68 | # Directory for instrumented libs generated by jscoverage/JSCover 69 | lib-cov 70 | 71 | # Coverage directory used by tools like istanbul 72 | coverage 73 | *.lcov 74 | 75 | # nyc test coverage 76 | .nyc_output 77 | 78 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 79 | .grunt 80 | 81 | # Bower dependency directory (https://bower.io/) 82 | bower_components 83 | 84 | # node-waf configuration 85 | .lock-wscript 86 | 87 | # Compiled binary addons (https://nodejs.org/api/addons.html) 88 | build/Release 89 | 90 | # Dependency directories 91 | node_modules/ 92 | jspm_packages/ 93 | 94 | # TypeScript v1 declaration files 95 | typings/ 96 | 97 | # TypeScript cache 98 | *.tsbuildinfo 99 | 100 | # Optional npm cache directory 101 | .npm 102 | 103 | # Optional eslint cache 104 | .eslintcache 105 | 106 | # Microbundle cache 107 | .rpt2_cache/ 108 | .rts2_cache_cjs/ 109 | .rts2_cache_es/ 110 | .rts2_cache_umd/ 111 | 112 | # Optional REPL history 113 | .node_repl_history 114 | 115 | # Output of 'npm pack' 116 | *.tgz 117 | 118 | # Yarn Integrity file 119 | .yarn-integrity 120 | 121 | # dotenv environment variables file 122 | .env 123 | .env.test 124 | 125 | # parcel-bundler cache (https://parceljs.org/) 126 | .cache 127 | 128 | # Next.js build output 129 | .next 130 | 131 | # Nuxt.js build / generate output 132 | .nuxt 133 | dist 134 | 135 | # Gatsby files 136 | .cache/ 137 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 138 | # https://nextjs.org/blog/next-9-1#public-directory-support 139 | # public 140 | 141 | # vuepress build output 142 | .vuepress/dist 143 | 144 | # Serverless directories 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | .fusebox/ 149 | 150 | # DynamoDB Local files 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | .tern-port 155 | 156 | # Byte-compiled / optimized / DLL files 157 | __pycache__/ 158 | *.py[cod] 159 | *$py.class 160 | 161 | # C extensions 162 | *.so 163 | 164 | # Distribution / packaging 165 | .Python 166 | build/ 167 | develop-eggs/ 168 | dist/ 169 | downloads/ 170 | eggs/ 171 | .eggs/ 172 | lib/ 173 | lib64/ 174 | parts/ 175 | sdist/ 176 | var/ 177 | wheels/ 178 | pip-wheel-metadata/ 179 | share/python-wheels/ 180 | *.egg-info/ 181 | .installed.cfg 182 | *.egg 183 | MANIFEST 184 | 185 | # PyInstaller 186 | # Usually these files are written by a python script from a template 187 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 188 | *.manifest 189 | *.spec 190 | 191 | # Installer logs 192 | pip-log.txt 193 | pip-delete-this-directory.txt 194 | 195 | # Unit test / coverage reports 196 | htmlcov/ 197 | .tox/ 198 | .nox/ 199 | .coverage 200 | .coverage.* 201 | .cache 202 | nosetests.xml 203 | coverage.xml 204 | *.cover 205 | *.py,cover 206 | .hypothesis/ 207 | .pytest_cache/ 208 | 209 | # Translations 210 | *.mo 211 | *.pot 212 | 213 | # Django stuff: 214 | *.log 215 | local_settings.py 216 | db.sqlite3 217 | db.sqlite3-journal 218 | 219 | # Flask stuff: 220 | instance/ 221 | .webassets-cache 222 | 223 | # Scrapy stuff: 224 | .scrapy 225 | 226 | # Sphinx documentation 227 | docs/_build/ 228 | 229 | # PyBuilder 230 | target/ 231 | 232 | # Jupyter Notebook 233 | .ipynb_checkpoints 234 | 235 | # IPython 236 | profile_default/ 237 | ipython_config.py 238 | 239 | # pyenv 240 | .python-version 241 | 242 | # pipenv 243 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 244 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 245 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 246 | # install all needed dependencies. 247 | #Pipfile.lock 248 | 249 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 250 | __pypackages__/ 251 | 252 | # Celery stuff 253 | celerybeat-schedule 254 | celerybeat.pid 255 | 256 | # SageMath parsed files 257 | *.sage.py 258 | 259 | # Environments 260 | .env 261 | .venv 262 | env/ 263 | venv/ 264 | ENV/ 265 | env.bak/ 266 | venv.bak/ 267 | 268 | # Spyder project settings 269 | .spyderproject 270 | .spyproject 271 | 272 | # Rope project settings 273 | .ropeproject 274 | 275 | # mkdocs documentation 276 | /site 277 | 278 | # mypy 279 | .mypy_cache/ 280 | .dmypy.json 281 | dmypy.json 282 | 283 | # Pyre type checker 284 | .pyre/ 285 | migrations/ 286 | 287 | # Visual Studio Code 288 | .vscode 289 | 290 | # Project builds 291 | backend/templates/vue 292 | backend/static/vue 293 | 294 | package-lock.json 295 | 296 | !frontend/.env 297 | 298 | .env.production -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: bash 3 | 4 | services: 5 | - docker 6 | 7 | before_install: 8 | - sudo apt-get update 9 | - sudo apt-get install apt-transport-https ca-certificates curl gnupg lsb-release 10 | - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg 11 | - echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 12 | - sudo apt-get update 13 | - sudo apt-get install docker-ce docker-ce-cli containerd.io 14 | - sudo curl -L "https://github.com/docker/compose/releases/download/1.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 15 | - sudo chmod +x /usr/local/bin/docker-compose 16 | - docker --version 17 | - docker-compose --version 18 | 19 | script: 20 | - docker-compose up --build -d 21 | - docker ps -a 22 | - docker images -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License 2 | Version 2.0 3 | 1. Definitions 4 | 1.1. “Contributor” 5 | means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 6 | 7 | 1.2. “Contributor Version” 8 | means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution. 9 | 10 | 1.3. “Contribution” 11 | means Covered Software of a particular Contributor. 12 | 13 | 1.4. “Covered Software” 14 | means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 15 | 16 | 1.5. “Incompatible With Secondary Licenses” 17 | means 18 | 19 | that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or 20 | 21 | that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 22 | 23 | 1.6. “Executable Form” 24 | means any form of the work other than Source Code Form. 25 | 26 | 1.7. “Larger Work” 27 | means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 28 | 29 | 1.8. “License” 30 | means this document. 31 | 32 | 1.9. “Licensable” 33 | means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 34 | 35 | 1.10. “Modifications” 36 | means any of the following: 37 | 38 | any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or 39 | 40 | any new file in Source Code Form that contains any Covered Software. 41 | 42 | 1.11. “Patent Claims” of a Contributor 43 | means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 44 | 45 | 1.12. “Secondary License” 46 | means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 47 | 48 | 1.13. “Source Code Form” 49 | means the form of the work preferred for making modifications. 50 | 51 | 1.14. “You” (or “Your”) 52 | means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 53 | 54 | 2. License Grants and Conditions 55 | 2.1. Grants 56 | Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: 57 | 58 | under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and 59 | 60 | under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 61 | 62 | 2.2. Effective Date 63 | The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 64 | 65 | 2.3. Limitations on Grant Scope 66 | The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: 67 | 68 | for any code that a Contributor has removed from Covered Software; or 69 | 70 | for infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or 71 | 72 | under Patent Claims infringed by Covered Software in the absence of its Contributions. 73 | 74 | This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 75 | 76 | 2.4. Subsequent Licenses 77 | No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 78 | 79 | 2.5. Representation 80 | Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 81 | 82 | 2.6. Fair Use 83 | This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 84 | 85 | 2.7. Conditions 86 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 87 | 88 | 3. Responsibilities 89 | 3.1. Distribution of Source Form 90 | All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form. 91 | 92 | 3.2. Distribution of Executable Form 93 | If You distribute Covered Software in Executable Form then: 94 | 95 | such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and 96 | 97 | You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License. 98 | 99 | 3.3. Distribution of a Larger Work 100 | You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 101 | 102 | 3.4. Notices 103 | You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 104 | 105 | 3.5. Application of Additional Terms 106 | You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 107 | 108 | 4. Inability to Comply Due to Statute or Regulation 109 | If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 110 | 111 | 5. Termination 112 | 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 113 | 114 | 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 115 | 116 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 117 | 118 | 6. Disclaimer of Warranty 119 | Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 120 | 121 | 7. Limitation of Liability 122 | Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 123 | 124 | 8. Litigation 125 | Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims. 126 | 127 | 9. Miscellaneous 128 | This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 129 | 130 | 10. Versions of the License 131 | 10.1. New Versions 132 | Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 133 | 134 | 10.2. Effect of New Versions 135 | You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 136 | 137 | 10.3. Modified Versions 138 | If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 139 | 140 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 141 | If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. 142 | 143 | Exhibit A - Source Code Form License Notice 144 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. 145 | 146 | If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. 147 | 148 | You may add additional accurate notices of copyright ownership. 149 | 150 | Exhibit B - “Incompatible With Secondary Licenses” Notice 151 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from flask_cors import CORS 4 | from flask_migrate import Migrate 5 | from flask_sqlalchemy import SQLAlchemy 6 | 7 | from backend.config import Config 8 | from backend.views import ViewManager 9 | from backend.jinja import CustomFunctions 10 | 11 | from pynance import PyNance 12 | import pathlib 13 | 14 | db = SQLAlchemy() 15 | cors = CORS() 16 | migrate = Migrate() 17 | pynance = PyNance(flask_app=True) 18 | 19 | class Webserver(Flask): 20 | def __init__(self): 21 | public_folder = str( 22 | pathlib.PurePosixPath( 23 | pathlib.Path(__file__).resolve().parent.parent, 24 | 'frontend', 25 | 'dist' 26 | ) 27 | ) 28 | if not pathlib.Path(public_folder).exists(): raise Exception( 29 | 'Frontend needs to be compiled first\n\n' \ 30 | 'cd webserver/frontend && npm run watch\n' \ 31 | 'cd webserver/frontend && npm run build' 32 | ) 33 | 34 | Flask.__init__(self, __name__, 35 | template_folder=public_folder, 36 | static_folder=public_folder, 37 | static_url_path='' 38 | ) 39 | self.config.from_object(Config()) 40 | 41 | if len(self.config['BINANCE_API_KEY']) <= 10: raise Exception('Binance API KEY invalid') 42 | if len(self.config['BINANCE_API_SECRET']) <= 10: raise Exception('Binance API SECRET invalid') 43 | 44 | self._setup_cors() 45 | self._setup_views() 46 | self._setup_database() 47 | self._setup_plugins() 48 | self._setup_jinja() 49 | self._is_populated = False 50 | 51 | def _setup_cors(self): 52 | cors.init_app(self, resources={r'/*': {'origins': ['*', 'https://developers.coinmarketcal.com/']}}) 53 | 54 | def _setup_views(self): 55 | viewmanager = ViewManager(self) 56 | viewmanager.register() 57 | 58 | def _setup_database(self): 59 | db.init_app(self) 60 | migrate.init_app(self, db) 61 | 62 | from backend.models.keys import KeysModel 63 | from backend.models.system import SystemModel 64 | from backend.models.bot import BotModel 65 | from backend.models.config import ConfigModel 66 | from backend.models.status import StatusModel 67 | from backend.models.orders import OrdersModel 68 | 69 | def _setup_plugins(self): 70 | pynance.init_app(self) 71 | 72 | def _setup_jinja(self): 73 | """ 74 | Updates jinja with custom python commands. Each new command will be callable 75 | in the jinja templates. 76 | """ 77 | self.jinja_env.globals.update( 78 | secret_key=CustomFunctions.secret_key, 79 | server_backend=CustomFunctions.server_backend 80 | ) 81 | 82 | def __call__(self, environ, start_response): 83 | if not self._is_populated: 84 | with self.app_context(): 85 | from backend.models.system import SystemModel 86 | system = SystemModel.query.first() 87 | if system is None: 88 | system = SystemModel(version=self.config['VERSION']) 89 | db.session.add(system) 90 | db.session.commit() 91 | 92 | from backend.models.config import ConfigModel 93 | config = ConfigModel.query.first() 94 | if config is None: 95 | config = ConfigModel() 96 | db.session.add(config) 97 | db.session.commit() 98 | 99 | from backend.models.status import StatusModel 100 | status = StatusModel.query.first() 101 | if status is None: 102 | status = StatusModel() 103 | db.session.add(status) 104 | db.session.commit() 105 | 106 | from backend.models.bot import BotModel 107 | bot = BotModel.query.first() 108 | if bot is None: 109 | db.session.add(BotModel(config_id=config.id, status_id=status.id)) 110 | db.session.commit() 111 | 112 | if system is not None and \ 113 | config is not None and \ 114 | status is not None and \ 115 | bot is not None: 116 | self._is_populated = True 117 | return super().__call__(environ, start_response) -------------------------------------------------------------------------------- /backend/cli.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | import pathlib 3 | 4 | 5 | def register(app): 6 | @app.cli.group() 7 | def password(): 8 | """ 9 | Tooling for passwords 10 | """ 11 | pass 12 | 13 | @password.command() 14 | def reset(): 15 | """ 16 | Reset master password 17 | """ 18 | from backend.models.system import SystemModel 19 | from backend import db 20 | system = SystemModel.query.first() 21 | if system is not None: 22 | system.authentication = False 23 | system.tos=False 24 | system.password = '' 25 | system.token = '' 26 | db.session.add(system) 27 | db.session.commit() 28 | print('[PyNance] Master password has been reset') 29 | else: print('[PyNance] Could not reset master password') 30 | -------------------------------------------------------------------------------- /backend/config/__init__.py: -------------------------------------------------------------------------------- 1 | from backend.config.system import System 2 | 3 | class Config(System): 4 | """ 5 | The base of the configuration can be found in this class. 6 | The more complex the application, the more settings to fiddle with. 7 | 8 | Note 9 | ---- 10 | System should super at the end of the init file 11 | """ 12 | def __init__(self) -> None: 13 | self.DEBUG = False 14 | self.VERSION = "3.1.0" 15 | self.PROJECT_NAME = "PyNance - Webinterface" 16 | self.TESTING = True if self.DEBUG else False 17 | 18 | self.ENVIRONMENT = None 19 | self.SERVER_BACKEND = None 20 | self.LOCALE = None 21 | self.FALLBACK_LOCALE = None 22 | 23 | self.SQLALCHEMY_DATABASE_URI = None 24 | self.SQLALCHEMY_TRACK_MODIFICATIONS = None 25 | 26 | self.BINANCE_API_KEY = None 27 | self.BINANCE_API_SECRET = None 28 | 29 | # Load system configuration 30 | System.__init__(self) 31 | -------------------------------------------------------------------------------- /backend/config/docker/dashboard.dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.6-slim as DashboardImage 2 | 3 | 4 | # Set the default workdir 5 | WORKDIR /workspaceFolder 6 | # Map files directly to docker container 7 | ADD . . 8 | # Update system 9 | RUN apt-get update && apt-get upgrade -y 10 | # Install dependencies 11 | RUN apt-get install git -y && apt-get install curl -y 12 | ## PYTHON 13 | RUN pip install -r requirements.txt 14 | # Install gunicorn 15 | RUN pip install gunicorn==20.0.4 16 | ## NPM 17 | # Install npm / node 18 | RUN apt-get install -y nodejs npm 19 | # update version 20 | RUN npm i npm@latest -g 21 | # Install requirements 22 | RUN cd frontend && npm install 23 | # Build frontend 24 | RUN cd frontend && npm run build 25 | 26 | # Setup server environment 27 | ENV PYTHONUNBUFFERED=1 28 | RUN chmod +x ./backend/config/docker/entrypoint_webserver.sh 29 | 30 | ENTRYPOINT ["./backend/config/docker/entrypoint_webserver.sh" ] 31 | EXPOSE 5000 32 | -------------------------------------------------------------------------------- /backend/config/docker/database.dockerfile: -------------------------------------------------------------------------------- 1 | FROM mysql:8.0.23 as DATABASEIMAGE 2 | ENV MYSQL_ROOT_HOST=% 3 | -------------------------------------------------------------------------------- /backend/config/docker/entrypoint_listener.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | while ! curl -o - pn_database:3306; do sleep 1; done 5 | 6 | { 7 | python listener.py 8 | } -------------------------------------------------------------------------------- /backend/config/docker/entrypoint_webserver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export FLASK_APP=webserver.py 4 | 5 | while ! curl -o - pn_database:3306; do sleep 1; done 6 | 7 | { 8 | flask db init 9 | } || { 10 | echo 'Database already initialized' 11 | } 12 | flask db migrate 13 | flask db upgrade 14 | 15 | # flask run 16 | gunicorn --bind 0.0.0.0:5000 --error-logfile - --access-logfile - --capture-output --worker-tmp-dir /dev/shm --log-level=debug --workers=1 --timeout 200 'webserver:create_server()' 17 | -------------------------------------------------------------------------------- /backend/config/docker/listener.dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.6-slim as ListenerImage 2 | 3 | # Set the default workdir 4 | WORKDIR /workspaceFolder 5 | # Map files directly to docker container 6 | ADD ./listener.py . 7 | ADD ./.env.production . 8 | ADD ./.env.development . 9 | ADD ./backend ./backend 10 | ADD ./backend/config/docker/entrypoint_listener.sh . 11 | # Update system 12 | RUN apt-get update && apt-get upgrade -y 13 | # Install dependencies 14 | RUN apt-get install git -y && apt-get install curl -y 15 | ## PYTHON 16 | RUN pip install -r ./backend/config/docker/listener_req.txt 17 | 18 | # Setup server environment 19 | ENV PYTHONUNBUFFERED=1 20 | RUN chmod +x ./entrypoint_listener.sh 21 | 22 | ENTRYPOINT ["./entrypoint_listener.sh" ] -------------------------------------------------------------------------------- /backend/config/docker/listener_req.txt: -------------------------------------------------------------------------------- 1 | Flask==1.1.2 2 | Flask-Cors==3.0.10 3 | Flask-Migrate==2.7.0 4 | Flask-SQLAlchemy==2.4.4 5 | PyMySQL==1.0.2 6 | requests==2.25.1 7 | -r ../pynance-core.txt -------------------------------------------------------------------------------- /backend/config/docker/mysqld.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | pid-file = /var/run/mysqld/mysqld.pid 3 | socket = /var/run/mysqld/mysqld.sock 4 | # Where the database files are stored inside the container 5 | datadir = /var/lib/mysql 6 | 7 | # My application special configuration 8 | max_allowed_packet = 32M 9 | sql-mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION' 10 | 11 | # Accept connections from any IP address 12 | bind-address = 0.0.0.0 -------------------------------------------------------------------------------- /backend/config/docker/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { worker_connections 1024; } 4 | 5 | http { 6 | 7 | sendfile on; 8 | 9 | upstream webinterface { 10 | server pn_dashboard:5000 max_fails=3 fail_timeout=15s; 11 | } 12 | 13 | server { 14 | listen 5000; 15 | 16 | location / { 17 | proxy_pass http://webinterface; 18 | proxy_set_header Host $http_host; 19 | proxy_set_header X-Real-IP $remote_addr; 20 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 21 | proxy_set_header X-Forwarded-Host $server_name; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /backend/config/pynance-core.txt: -------------------------------------------------------------------------------- 1 | PyNance @ git+https://github.com/0x78f1935/PyNance.git@7fd3c3000b679d03fa0e7187fd03dd45eacba85d -------------------------------------------------------------------------------- /backend/config/system.py: -------------------------------------------------------------------------------- 1 | from pynance.core.exceptions import BinanceException 2 | from uuid import uuid4 3 | import pathlib 4 | 5 | class System(object): 6 | """ 7 | The system configuration is ment to be constant and should not be changed if not necessary. 8 | If you still would like to overwrite values consider using the production or development configuration file instead. 9 | """ 10 | def __init__(self) -> None: 11 | self.MAINTAINER = "0x78f1935" 12 | self.GITHUB = "https://github.com/0x78f1935/PyNance-Webinterface" 13 | self.TWITTER = "https://twitter.com/UnicodeError" 14 | 15 | self.SECRET_KEY = str(uuid4()).replace('-', '') 16 | 17 | # When debug is False, load production environment 18 | if not self.DEBUG: filename = '.env.production' 19 | # When debug is True, load development environment 20 | else: filename = '.env.development' 21 | 22 | config_loc = pathlib.PurePosixPath(pathlib.Path(__file__).resolve().parent.parent.parent, filename) 23 | with open(config_loc, 'r') as config_file: 24 | data = self._clean_config_data([i for i in config_file.readlines()]) 25 | 26 | self._load_config(data) 27 | 28 | def _clean_config_data(self, data): 29 | results = {} 30 | for item in data: 31 | if not item.startswith('//'): 32 | if item.endswith('\n'): 33 | item = item.strip() 34 | attributes = item.split('=') 35 | key = attributes.pop(0) 36 | value = '='.join(attributes) 37 | if key != '' and value != '': 38 | results[key] = value 39 | return results 40 | 41 | def _load_config(self, data): 42 | for key, value in data.items(): 43 | if key != '' and value != '': 44 | try: 45 | getattr(self, key) 46 | setattr(self, key, value) 47 | except AttributeError: raise BinanceException("Invalid configuration provided") 48 | -------------------------------------------------------------------------------- /backend/jinja.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | from datetime import datetime 3 | 4 | class CustomFunctions(object): 5 | @staticmethod 6 | def secret_key(): 7 | return current_app.config['SECRET_KEY'] 8 | 9 | @staticmethod 10 | def server_backend(): 11 | return current_app.config['SERVER_BACKEND'] -------------------------------------------------------------------------------- /backend/models/bot.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from backend import db 3 | from werkzeug.security import generate_password_hash, check_password_hash 4 | from sqlalchemy import inspect 5 | from uuid import uuid4 6 | 7 | 8 | class BotModel(db.Model): 9 | """BotModel keeps track of the bots status""" 10 | __tablename__ = "Bot" 11 | id = db.Column(db.Integer, primary_key=True) 12 | updated = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 13 | 14 | config_id = db.Column(db.Integer, db.ForeignKey('Config.id')) 15 | config = db.relationship("ConfigModel", back_populates="bot") 16 | 17 | status_id = db.Column(db.Integer, db.ForeignKey('Status.id')) 18 | status = db.relationship("StatusModel", back_populates="bot") 19 | 20 | orders = db.relationship("OrdersModel") 21 | 22 | graph_type = db.Column(db.Text, default='5m') 23 | graph_interval = db.Column(db.Integer, default=30) 24 | online = db.Column(db.Boolean, default=False, onupdate=False) 25 | 26 | def update_data(self, data: dict): 27 | """"Just throw in a json object, each key that can be mapped will be updated" 28 | 29 | Args: 30 | data (dict): The data to update with 31 | """ 32 | for key, value in data.items(): 33 | try: 34 | getattr(self, key) 35 | setattr(self, key, value) 36 | except AttributeError: pass 37 | db.session.commit() 38 | 39 | def to_dict(self, blacklist:list=[]): 40 | """Transforms a row object into a dictionary object 41 | 42 | Args: 43 | blacklist ([list]): [Columns you don't want to include in the dict] 44 | """ 45 | return {c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs if c.key not in blacklist} 46 | 47 | def chat(self, message: str): 48 | """Updates the statusbar in the frontend 49 | 50 | Args: 51 | message (str): [The message which you would like to make appear] 52 | """ 53 | self.status.message = message 54 | db.session.commit() 55 | 56 | def update_target(self, symbol: str): 57 | """Updates the current selected symbol in the statusbar table 58 | """ 59 | self.status.target = symbol 60 | db.session.commit() 61 | 62 | def finished_order(self): 63 | """Increment the status model with +1 64 | """ 65 | self.status.total_orders += 1 66 | db.session.commit() 67 | 68 | def update_average(self, average: float): 69 | """Keeps track of the current selected symbol average 70 | 71 | Args: 72 | average (float): [The average of the selected symbol] 73 | """ 74 | self.status.average = average 75 | db.session.commit() 76 | 77 | def get_order(self, symbol: str): 78 | """Gets the order which we want to trade, if not existing we create one 79 | 80 | Args: 81 | symbol (str): [The symbol which we would like to trade] 82 | 83 | Returns: 84 | [OrderModel]: [representing order] 85 | """ 86 | if self.config.sandbox: orders = [i for i in self.orders if i.symbol == symbol and i.active and i.spot == self.config.spot and i.sandbox == True] 87 | else: orders = [i for i in self.orders if i.symbol == symbol and i.active and i.spot == self.config.spot] 88 | if orders: order = orders.pop(0) 89 | else: 90 | from backend.models.orders import OrdersModel 91 | order = OrdersModel( 92 | bot_id=self.id, 93 | symbol=symbol, 94 | spot=self.config.spot, 95 | sandbox=self.config.sandbox, 96 | status="PROCESSING" 97 | ) 98 | db.session.add(order) 99 | db.session.commit() 100 | return order -------------------------------------------------------------------------------- /backend/models/config.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from backend import db 3 | from werkzeug.security import generate_password_hash, check_password_hash 4 | from sqlalchemy import inspect 5 | from sqlalchemy.ext.mutable import MutableList 6 | from sqlalchemy import PickleType 7 | from uuid import uuid4 8 | 9 | 10 | class ConfigModel(db.Model): 11 | """ConfigModel keeps track of the trade configuration set in /trades""" 12 | __tablename__ = "Config" 13 | id = db.Column(db.Integer, primary_key=True) 14 | updated = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 15 | 16 | bot = db.relationship("BotModel", back_populates="config") 17 | 18 | symbols = db.Column(MutableList.as_mutable(PickleType), default=[]) 19 | timeframe = db.Column(db.Text, default="1h") 20 | candle_interval = db.Column(db.Integer, default=100) 21 | wallet_amount = db.Column(db.Float, default=99) 22 | below_average = db.Column(db.Float, default=5) 23 | profit_margin = db.Column(db.Float, default=35) 24 | profit_as = db.Column(db.Text, default="USDT") 25 | spot = db.Column(db.Boolean, default=True) 26 | sandbox = db.Column(db.Boolean, default=True) 27 | 28 | # Futures 29 | expected_leverage = db.Column(db.Float, default=5) 30 | volume_timeframe = db.Column(db.Text, default="5m") 31 | total_volume = db.Column(db.Integer, default=30) 32 | margin_type = db.Column(db.Text, default="ISOLATED") 33 | in_green = db.Column(db.Float, default=0.2) 34 | in_red = db.Column(db.Float, default=2) 35 | take_profit = db.Column(db.Float, default=2) 36 | 37 | allow_multiple_orders = db.Column(db.Boolean, default=False) 38 | use_average = db.Column(db.Boolean, default=False) 39 | 40 | def update_data(self, data: dict): 41 | """"Just throw in a json object, each key that can be mapped will be updated" 42 | 43 | Args: 44 | data (dict): The data to update with 45 | """ 46 | for key, value in data.items(): 47 | try: 48 | getattr(self, key) 49 | setattr(self, key, value) 50 | except AttributeError: pass 51 | db.session.commit() 52 | 53 | def to_dict(self, blacklist:list=[]): 54 | """Transforms a row object into a dictionary object 55 | 56 | Args: 57 | blacklist ([list]): [Columns you don't want to include in the dict] 58 | """ 59 | return {c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs if c.key not in blacklist} 60 | 61 | def remove_sandbox(self): 62 | from backend.models.orders import OrdersModel 63 | sandbox_items = OrdersModel.query.filter(OrdersModel.sandbox == True).delete() 64 | db.session.commit() -------------------------------------------------------------------------------- /backend/models/keys.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from backend import db 3 | from werkzeug.security import generate_password_hash, check_password_hash 4 | from sqlalchemy import inspect 5 | from uuid import uuid4 6 | 7 | 8 | class KeysModel(db.Model): 9 | """KeysModel keeps track of API keys which can extend features within PyNance""" 10 | __tablename__ = "Keys" 11 | id = db.Column(db.Integer, primary_key=True) 12 | created = db.Column(db.DateTime, default=datetime.utcnow) 13 | 14 | key = db.Column(db.Text, nullable=False) 15 | value = db.Column(db.Text, nullable=False) 16 | 17 | def update_data(self, data: dict): 18 | """"Just throw in a json object, each key that can be mapped will be updated" 19 | 20 | Args: 21 | data (dict): The data to update with 22 | """ 23 | for key, value in data.items(): 24 | try: 25 | getattr(self, key) 26 | setattr(self, key, value) 27 | except AttributeError: pass 28 | db.session.commit() 29 | 30 | def to_dict(self, blacklist:list=[]): 31 | """Transforms a row object into a dictionary object 32 | 33 | Args: 34 | blacklist ([list]): [Columns you don't want to include in the dict] 35 | """ 36 | return {c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs if c.key not in blacklist} -------------------------------------------------------------------------------- /backend/models/orders.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from backend import db 3 | from werkzeug.security import generate_password_hash, check_password_hash 4 | from sqlalchemy import inspect 5 | from uuid import uuid4 6 | 7 | 8 | class OrdersModel(db.Model): 9 | """KeysModel keeps track of API keys which can extend features within PyNance""" 10 | __tablename__ = "Orders" 11 | id = db.Column(db.Integer, primary_key=True) 12 | created = db.Column(db.DateTime, default=datetime.utcnow) 13 | updated = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 14 | 15 | bot_id = db.Column(db.Integer, db.ForeignKey('Bot.id')) 16 | 17 | symbol = db.Column(db.Text, nullable=False) 18 | brought_price = db.Column(db.Float, default=0.0) 19 | quantity = db.Column(db.Float, default=0.0) 20 | sold_for = db.Column(db.Float, default=0.0) 21 | stop_loss = db.Column(db.Float, default=0.0) 22 | profit_target = db.Column(db.Float, default=0.0) 23 | buying = db.Column(db.Boolean, default=True) 24 | status = db.Column(db.Text, default="UNKNOWN") 25 | spot = db.Column(db.Boolean, default=True) 26 | sandbox = db.Column(db.Boolean, default=False) 27 | active = db.Column(db.Boolean, default=True) 28 | 29 | order_id = db.Column(db.BigInteger) 30 | client_order_id = db.Column(db.Text) 31 | 32 | def update_data(self, data: dict): 33 | """"Just throw in a json object, each key that can be mapped will be updated" 34 | 35 | Args: 36 | data (dict): The data to update with 37 | """ 38 | for key, value in data.items(): 39 | try: 40 | getattr(self, key) 41 | setattr(self, key, value) 42 | except AttributeError: pass 43 | db.session.commit() 44 | 45 | def to_dict(self, blacklist:list=[]): 46 | """Transforms a row object into a dictionary object 47 | 48 | Args: 49 | blacklist ([list]): [Columns you don't want to include in the dict] 50 | """ 51 | return {c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs if c.key not in blacklist} 52 | 53 | def set_active(self, value: bool): 54 | self.active = value 55 | db.session.commit() -------------------------------------------------------------------------------- /backend/models/status.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from backend import db 3 | from werkzeug.security import generate_password_hash, check_password_hash 4 | from sqlalchemy import inspect 5 | from sqlalchemy.ext.mutable import MutableList 6 | from sqlalchemy import PickleType 7 | from uuid import uuid4 8 | 9 | 10 | class StatusModel(db.Model): 11 | """StatusModel keeps track of the statusbar""" 12 | __tablename__ = "Status" 13 | id = db.Column(db.Integer, primary_key=True) 14 | updated = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 15 | 16 | bot = db.relationship("BotModel", back_populates="status") 17 | 18 | target = db.Column(db.Text, default="NO TARGET") 19 | message = db.Column(db.Text, default="Offline") 20 | total_orders = db.Column(db.Integer, default=0) 21 | average = db.Column(db.Float, default=0.0) 22 | 23 | def update_data(self, data: dict): 24 | """"Just throw in a json object, each key that can be mapped will be updated" 25 | 26 | Args: 27 | data (dict): The data to update with 28 | """ 29 | for key, value in data.items(): 30 | try: 31 | getattr(self, key) 32 | setattr(self, key, value) 33 | except AttributeError: pass 34 | db.session.commit() 35 | 36 | def to_dict(self, blacklist:list=[]): 37 | """Transforms a row object into a dictionary object 38 | 39 | Args: 40 | blacklist ([list]): [Columns you don't want to include in the dict] 41 | """ 42 | return {c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs if c.key not in blacklist} -------------------------------------------------------------------------------- /backend/models/system.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from backend import db 3 | from werkzeug.security import generate_password_hash, check_password_hash 4 | from sqlalchemy import inspect 5 | from uuid import uuid4 6 | 7 | 8 | class SystemModel(db.Model): 9 | """System model for PyNance contains variables that will be loaded when the dashbord 10 | is visited for the first time. The content is created when the flask server starts. 11 | The content however gets only created if it doesn't exists already. 12 | """ 13 | __tablename__ = "System" 14 | id = db.Column(db.Integer, primary_key=True) 15 | updated = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) 16 | version = db.Column(db.Text, default="x.x.x") 17 | language = db.Column(db.Text, default="en") 18 | tos = db.Column(db.Boolean, default=False) 19 | authentication = db.Column(db.Boolean, default=False) 20 | password = db.Column(db.Text, default='') 21 | token = db.Column(db.Text, default='') 22 | 23 | def set_password(self, password): 24 | self.password = generate_password_hash(password) 25 | self.token = str(uuid4()).replace('-', '') 26 | db.session.commit() 27 | 28 | def check_password(self, password): 29 | return check_password_hash(self.password, password) 30 | 31 | def update_data(self, data: dict): 32 | """"Just throw in a json object, each key that can be mapped will be updated" 33 | 34 | Args: 35 | data (dict): The data to update with 36 | """ 37 | for key, value in data.items(): 38 | try: 39 | getattr(self, key) 40 | setattr(self, key, value) 41 | except AttributeError: pass 42 | db.session.commit() 43 | 44 | def to_dict(self, blacklist:list=[]): 45 | """Transforms a row object into a dictionary object 46 | 47 | Args: 48 | blacklist ([list]): [Columns you don't want to include in the dict] 49 | """ 50 | return {c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs if c.key not in blacklist} -------------------------------------------------------------------------------- /backend/utils/auth.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from flask import request, jsonify, current_app 3 | 4 | 5 | def login_required(f): 6 | @wraps(f) 7 | def decorated_function(*args, **kwargs): 8 | from backend.models.system import SystemModel 9 | system = SystemModel.query.first() 10 | 11 | if system.authentication: 12 | if 'token' in [i.lower() for i in request.headers.keys()] and request.headers['token'] != '': 13 | if request.headers['token'] != system.token: 14 | if request.headers['token'] != current_app.config['SECRET_KEY']: 15 | return jsonify({'authenticated': False}), 400 16 | else: 17 | return jsonify({'authenticated': False}), 400 18 | return f(*args, **kwargs) 19 | return decorated_function 20 | -------------------------------------------------------------------------------- /backend/utils/trading/__init__.py: -------------------------------------------------------------------------------- 1 | from backend import pynance 2 | from datetime import datetime 3 | 4 | 5 | class Trading(object): 6 | def __init__(self): 7 | self.stopwatch = datetime.now() 8 | 9 | from backend.models.bot import BotModel 10 | self.bot = BotModel.query.first() 11 | self.order = None 12 | self.precision = 0 13 | self.required_amount = 0 14 | self.symbol = '' 15 | self.base_asset = '' 16 | self.quote_asset = '' 17 | self.base_balance_free = 0 18 | self.quote_balance_free = 0 19 | self.position = '' 20 | 21 | def fetch_results(self): 22 | return { 23 | 'online': self.bot.online, 24 | 'execution_time': str(datetime.now()-self.stopwatch), 25 | } 26 | 27 | @property 28 | def can_trade(self): 29 | if not self.bot.online: 30 | self.bot.chat("CURRENTLY OFFLINE") 31 | return False 32 | 33 | binance_status = pynance.system.maintenance() 34 | if binance_status.json['status'] != 0: 35 | self.bot.chat("BINANCE IS CURRENTLY UNAVAILABLE") 36 | return False 37 | 38 | if len(self.bot.config.symbols) <= 0: 39 | self.bot.chat("NO SYMBOL CONFIGURED TO TRADE WITH") 40 | return False 41 | return True 42 | 43 | def _prepare(self, symbol): 44 | self.symbol = symbol 45 | self.bot.update_target(symbol) 46 | self.bot.chat(f"CURRENT SYMBOL SELECTED - {symbol}") 47 | 48 | # Creates an order in the database if this is not an existing order, otherwise returns the existing order 49 | self.order = self.bot.get_order(symbol) 50 | 51 | @property 52 | def base_balance(self): 53 | """self.prepare needs to run before this method can be called 54 | """ 55 | return self.base_balance_free 56 | 57 | @property 58 | def quote_balance(self): 59 | """self.prepare needs to run before this method can be called 60 | """ 61 | return self.quote_balance_free 62 | 63 | @property 64 | def current_price(self): 65 | """self.prepare needs to run before this method can be called 66 | """ 67 | if self.order.spot: return float(round(float(pynance.assets.symbols(self.symbol).json['price']), self.precision)) 68 | else: return float(round(float(pynance.futures.assets.symbols(self.symbol).json['price']), self.precision)) 69 | 70 | from backend.utils.trading.spot import Spot 71 | from backend.utils.trading.future import Futures -------------------------------------------------------------------------------- /backend/utils/trading/spot.py: -------------------------------------------------------------------------------- 1 | from backend.utils.trading import Trading 2 | from backend import pynance 3 | import math 4 | 5 | 6 | class Spot(Trading): 7 | def __init__(self): 8 | Trading.__init__(self) 9 | 10 | @property 11 | def get_active_symbols(self): 12 | target_symbols = [] 13 | for item in self.bot.orders: 14 | if item.symbol in self.bot.config.symbols and item.active and item.spot: 15 | target_symbols.append(item.symbol) 16 | return list(set([i for i in self.bot.config.symbols] + target_symbols)) 17 | 18 | @property 19 | def average(self): 20 | """self.prepare needs to run before this method can be called 21 | 22 | Returns: 23 | Average based on configuration parameters 24 | """ 25 | # Calculate the lowest average based on the total amount of candles and the timeframe 26 | average = pynance.assets.average(self.symbol, self.bot.config.timeframe, self.bot.config.candle_interval) 27 | # If supression is on, calculate the average - x% of the average 28 | if self.bot.config.below_average > 0: 29 | average = float(average - float(float(average/100) * self.bot.config.below_average)) 30 | return average 31 | 32 | def prepare(self, symbol): 33 | self._prepare(symbol) 34 | # Fetch exchange information 35 | asset_exchange_info = pynance.assets.exchange_info(symbol).json['symbols'].pop(0) 36 | # Calculate the precision size of the symbol 37 | stepSize = [ i for i in asset_exchange_info['filters'] if i['filterType'] == 'LOT_SIZE'][0]['stepSize'] 38 | self.precision = int(round(-math.log(float(stepSize), 10), 0)) 39 | if self.precision == 0: self.precision = 8 40 | # Check the amount needed in order to place a minimum order 41 | self.required_amount = float(round(float([ i for i in asset_exchange_info['filters'] if i['filterType'] == 'MIN_NOTIONAL'][0]['minNotional']), self.precision)) 42 | # Get the asset names 43 | self.base_asset = asset_exchange_info['baseAsset'] 44 | self.quote_asset = asset_exchange_info['quoteAsset'] 45 | 46 | # Check for each asset selected what we have available in the wallet 47 | balance = pynance.wallet.balance() 48 | bb = [i for i in balance.json if i['coin'] == self.base_asset] 49 | bq = [i for i in balance.json if i['coin'] == self.quote_asset] 50 | if bb and bq: 51 | self.base_balance_free = float(round(float(bb[0]['free']), self.precision)) 52 | self.quote_balance_free = float(round(float(bq[0]['free']), self.precision)) 53 | else: 54 | self.bot.chat(f"SYMBOL {symbol} SEEMS TO BE BROKEN ON BINANCE SIDE") 55 | return False 56 | return True 57 | 58 | def start(self): 59 | """self.prepare needs to run before this method can be called 60 | """ 61 | if self.order.buying: 62 | self.bot.update_average(self.average) 63 | if self.quote_balance < self.required_amount: self.bot.chat(f"NOT ENOUGH {self.quote_asset} TO BUY {self.base_asset} - {self.quote_balance} {self.quote_asset} AVAILABLE NEED MINIMUM OF {self.required_amount} {self.quote_asset}") 64 | else: 65 | if self.current_price <= self.average: # HAS TO BE <= TODO change to correct comparator 66 | # Calculate the quantity we can buy based on the total amount free balance available * the total % allowed for trading 67 | quantity = float(float(float(float(self.quote_balance / self.current_price) / 100) * float(self.bot.config.wallet_amount))) 68 | self.bot.chat(f"{self.base_asset} REACHED TARGET PRICE - BUYING {quantity} {self.base_asset}") 69 | if self.bot.config.sandbox: 70 | brought_price = float(round(float(self.current_price)* quantity, self.precision)) 71 | self.order.update_data({ 72 | 'brought_price': brought_price, 73 | 'quantity': quantity, 74 | 'buying': False 75 | }) 76 | self.bot.chat(f"BROUGHT IN SANBOXMODE ({float(round(float(self.order.quantity), self.precision))}) {self.base_asset} FOR AN AMAZING ({float(round(float(brought_price), self.precision))}) {self.quote_asset}") 77 | else: 78 | buy_order = pynance.orders.create(self.symbol, float(round(float(quantity - float(quantity/100)), self.precision)), order_id='PyNanceV3-Order-Buy') 79 | if buy_order is not None: 80 | data = buy_order.json['fills'][0] 81 | brought_price = float(round(float(data['price'])* quantity, self.precision)) 82 | self.order.update_data({ 83 | 'brought_price': brought_price, 84 | 'quantity': quantity, 85 | 'buying': False 86 | }) 87 | self.bot.chat(f"BROUGHT ({float(round(float(self.order.quantity), self.precision))}) {self.base_asset} FOR AN AMAZING ({float(round(float(brought_price), self.precision))}) {self.quote_asset}") 88 | else: self.bot.chat(f"UNABLE TO PLACE A BUY ORDER FOR ({float(round(float(quantity), self.precision))}) {self.base_asset}") 89 | else: self.bot.chat(f"CURRENT {self.base_asset} NOT AT BUY TARGET OF {self.average} - SKIPPING BUYING {self.base_asset}") 90 | else: # Trying to sell 91 | # This check is so if the enduser keeps trading manualy the bot keeps going. It simply 92 | # closes the open order where there is no quantity for and starts over with buying if possible. 93 | if self.base_balance < self.order.quantity and not self.bot.config.sandbox: 94 | self.bot.chat(f"NOT ENOUGH {self.base_asset} TO SELL - {self.base_balance} {self.base_asset} AVAILABLE NEED MINIMUM OF {self.order.quantity} {self.base_asset} DISABLING ORDER") 95 | self.order.update_data({'active': False, 'status': 'EXPIRED'}) 96 | else: 97 | brought_price = self.order.brought_price 98 | minimal_profit = self.bot.config.profit_margin 99 | asset_fees = pynance.assets.fees(self.symbol) # makerCommission | takerCommission 100 | fee_percentage = float(asset_fees.json[0]['makerCommission']) 101 | ask_price = brought_price + float(brought_price / 100 * fee_percentage) + float(brought_price / 100 * minimal_profit) 102 | ask_price_a_coin = float(ask_price / self.order.quantity) 103 | self.bot.update_average(ask_price_a_coin) 104 | if self.current_price >= ask_price_a_coin: 105 | self.bot.chat(f"{self.base_asset} REACHED TARGET PRICE - SELLING {self.order.quantity} {self.base_asset}") 106 | if self.bot.config.sandbox: 107 | sold_price = float(round(float(self.current_price) * self.order.quantity, self.precision)) 108 | self.order.update_data({ 109 | 'active': False, 110 | 'sold_for': sold_price, 111 | 'status': 'PROFIT' 112 | }) 113 | self.bot.chat(f"SOLD IN SANDBOX ({float(round(float(self.order.quantity), self.precision))}) {self.base_asset} FOR AN AMAZING ({float(round(float(sold_price), self.precision))}) {self.quote_asset}") 114 | else: 115 | sell_order = pynance.orders.create(self.symbol, float(round(float(self.order.quantity), self.precision)), buy=False, order_id='PyNanceV3-Order-Sell') 116 | if sell_order is not None: 117 | sold_price = float(round(float(self.current_price) * self.order.quantity, self.precision)) 118 | self.order.update_data({ 119 | 'active': False, 120 | 'sold_for': sold_price, 121 | 'status': 'PROFIT' 122 | }) 123 | self.bot.chat(f"SOLD ({float(round(float(self.order.quantity), self.precision))}) {self.base_asset} FOR AN AMAZING ({float(round(float(sold_price), self.precision))}) {self.quote_asset}") 124 | else: self.bot.chat(f"UNABLE TO PLACE A SELL ORDER FOR ({float(round(float(self.order.quantity), self.precision))}) {self.base_asset}") 125 | else: self.bot.chat(f"CURRENT {self.base_asset} NOT AT SELL TARGET OF {ask_price_a_coin} - SKIPPING SELLING {self.base_asset}") 126 | -------------------------------------------------------------------------------- /backend/views/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import redirect, url_for, request, current_app 2 | 3 | class ViewManager(object): 4 | """ 5 | This class is a representation of any router 'class'. 6 | It just registers all views listed in the self.register method. 7 | """ 8 | def __init__(self, server) -> None: 9 | super().__init__() 10 | self.server = server 11 | self.prefix = "/api" 12 | self.version = "/v1" 13 | 14 | @server.errorhandler(404) 15 | def catch_all(path): 16 | """ 17 | Redirects each non existing endpoint to the main page. 18 | When `API` is included in the url we ignore this redirect. 19 | """ 20 | if 'api' not in request.base_url: 21 | return redirect(url_for('HomePageView:get')) 22 | 23 | def register(self) -> None: 24 | """ 25 | This method register the views to the server object. 26 | """ 27 | from backend.views.homepage import HomePageView 28 | HomePageView.register(self.server, route_base='/') 29 | 30 | from backend.views.api.system import SystemApiView 31 | SystemApiView.register(self.server, route_base=f'{self.prefix+self.version}/system') 32 | 33 | from backend.views.api.keys import KeysApiView 34 | KeysApiView.register(self.server, route_base=f'{self.prefix+self.version}/keys') 35 | 36 | from backend.views.api.coinmarketcal import CoinMarketApiView 37 | CoinMarketApiView.register(self.server, route_base=f'{self.prefix+self.version}/coinmarketcal') 38 | 39 | from backend.views.api.wallet import WalletApiView 40 | WalletApiView.register(self.server, route_base=f'{self.prefix+self.version}/wallet') 41 | 42 | from backend.views.api.trades import TradesApiView 43 | TradesApiView.register(self.server, route_base=f'{self.prefix+self.version}/trades') 44 | 45 | from backend.views.api.klines import KlinesApiView 46 | KlinesApiView.register(self.server, route_base=f'{self.prefix+self.version}/klines') 47 | 48 | from backend.views.api.history import HistoryApiView 49 | HistoryApiView.register(self.server, route_base=f'{self.prefix+self.version}/history') 50 | 51 | from backend.views.api.logic import LogicApiView 52 | LogicApiView.register(self.server, route_base=f'{self.prefix+self.version}/logic') 53 | 54 | from backend.views.api.backup import BackupAPIView 55 | BackupAPIView.register(self.server, route_base=f'{self.prefix+self.version}/backup') 56 | 57 | from backend.views.api.prices import PricesApiView 58 | PricesApiView.register(self.server, route_base=f'{self.prefix+self.version}/prices') 59 | -------------------------------------------------------------------------------- /backend/views/api/backup.py: -------------------------------------------------------------------------------- 1 | from flask_classful import FlaskView, route 2 | from flask import jsonify, request, Response 3 | 4 | from sqlalchemy.ext.serializer import loads, dumps 5 | 6 | from backend.utils.auth import login_required 7 | from backend import db, pynance 8 | 9 | import zipfile 10 | import io 11 | 12 | class BackupAPIView(FlaskView): 13 | 14 | decorators = [ login_required ] 15 | 16 | def get(self): 17 | from backend.models.bot import BotModel 18 | bot = BotModel.query.first() 19 | packed = [ 20 | ('bot.txt', io.BytesIO(dumps(bot)),), 21 | ('status.txt', io.BytesIO(dumps(bot.status)),), 22 | ('config.txt', io.BytesIO(dumps(bot.config)),), 23 | ('orders.txt', io.BytesIO(dumps(bot.orders)),), 24 | ] 25 | 26 | if True if request.args.get('pwd', 'false') == 'true' else False: 27 | from backend.models.keys import KeysModel 28 | keys = KeysModel.query.all() 29 | packed.append(('keys.txt', io.BytesIO(dumps(keys)))) 30 | from backend.models.system import SystemModel 31 | system = SystemModel.query.first() 32 | packed.append(('system.txt', io.BytesIO(dumps(system)))) 33 | 34 | zip_buffer = io.BytesIO() 35 | with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file: 36 | for file_name, data in packed: 37 | zip_file.writestr(file_name, data.getvalue()) 38 | 39 | return Response( 40 | zip_buffer.getvalue(), 41 | mimetype='application/zip', 42 | headers={'Content-Disposition': 'attachment;filename=pynance_backup.txt'} 43 | ) 44 | 45 | def post(self): 46 | backup = None 47 | for filename in request.files: 48 | if filename == 'backup': 49 | try: 50 | backup = request.files[filename] 51 | if backup: break 52 | except Exception as e: 53 | return jsonify({'succes': False, 'msg': 'Unable to read .pynance file'}), 200 54 | 55 | if backup is None: return jsonify({'succes': False, 'msg': 'Your backup seems to be empty'}), 200 56 | 57 | to_merge = [] 58 | from backend.models.bot import BotModel 59 | with zipfile.ZipFile(backup) as zf: 60 | for backup_file in zf.filelist: 61 | with zf.open(backup_file.filename) as f: 62 | data = f.read() 63 | 64 | from backend.models.status import StatusModel 65 | from backend.models.config import ConfigModel 66 | from backend.models.orders import OrdersModel 67 | from backend.models.system import SystemModel 68 | from backend.models.keys import KeysModel 69 | if 'bot' in backup_file.filename: to_merge.append(loads(data, BotModel.metadata, db.session)) 70 | elif 'status' in backup_file.filename: to_merge.append(loads(data, StatusModel.metadata, db.session)) 71 | elif 'config' in backup_file.filename: to_merge.append(loads(data, ConfigModel.metadata, db.session)) 72 | elif 'system' in backup_file.filename: to_merge.append(loads(data, SystemModel.metadata, db.session)) 73 | elif 'orders' in backup_file.filename: 74 | for item in loads(data): 75 | to_merge.append(loads(dumps(item), OrdersModel.metadata, db.session)) 76 | elif 'keys' in backup_file.filename: 77 | for item in loads(data): 78 | to_merge.append(loads(dumps(item), KeysModel.metadata, db.session)) 79 | 80 | for item in to_merge: 81 | db.session.merge(item) 82 | db.session.commit() 83 | bot = BotModel.query.first() 84 | bot.update_data({'online': False}) 85 | 86 | return jsonify({'succes': True, 'msg': 'Backup has been restored!'}), 200 87 | -------------------------------------------------------------------------------- /backend/views/api/coinmarketcal.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, jsonify, current_app, request 2 | from flask_classful import FlaskView, route 3 | 4 | from backend.utils.auth import login_required 5 | from backend import db, pynance 6 | 7 | import requests 8 | 9 | class CoinMarketApiView(FlaskView): 10 | 11 | decorators = [ login_required ] 12 | 13 | @route('/check', methods=['GET']) 14 | def check(self): 15 | url = "https://developers.coinmarketcal.com/v1/events" 16 | querystring = {"max":"5"} 17 | payload = "" 18 | headers = { 19 | 'x-api-key': request.args['api-key'], 20 | 'Accept-Encoding': "deflate, gzip", 21 | 'Accept': "application/json" 22 | } 23 | response = requests.request("GET", url, data=payload, headers=headers, params=querystring) 24 | if response.status_code == 200: 25 | from backend.models.keys import KeysModel 26 | model = KeysModel.query.filter(KeysModel.value=="coinmarketcal").first() 27 | if model is None: 28 | model = KeysModel(value="coinmarketcal", key=request.args['api-key']) 29 | else: 30 | model.key = request.args['api-key'] 31 | db.session.add(model) 32 | db.session.commit() 33 | return jsonify({'error': False}), 200 34 | else: return jsonify({'error': True}), 400 35 | 36 | @route('/events', methods=['POST']) 37 | def events(self): 38 | from backend.models.keys import KeysModel 39 | model = KeysModel.query.filter(KeysModel.value == 'coinmarketcal').first() 40 | if model is not None: 41 | url = "https://developers.coinmarketcal.com/v1/events" 42 | querystring = { 43 | "max": request.json['max'], 44 | "page": request.json['page'], 45 | "sortBy": request.json['sortBy'], 46 | "showOnly": request.json['showOnly'] 47 | } 48 | payload = "" 49 | headers = { 50 | 'x-api-key': model.key, 51 | 'Accept-Encoding': "deflate, gzip", 52 | 'Accept': "application/json" 53 | } 54 | response = requests.request("GET", url, data=payload, headers=headers, params=querystring) 55 | data = response.json() 56 | return jsonify({ 57 | 'pagecount': data['_metadata']['page_count'], 58 | 'total_items': data['_metadata']['total_count'], 59 | 'events': [ 60 | { 61 | 'title': i['title']['en'] if 'title' in i.keys() and 'en' in i['title'].keys() else '', 62 | 'coins': i['coins'] if 'coins' in i.keys() else [], 63 | 'date_event': i['date_event'] if 'date_event' in i.keys() else '', 64 | 'can_occur_before': i['can_occur_before'] if 'can_occur_before' in i.keys() else False, 65 | 'created': i['created_date'] if 'created_date' in i.keys() else '', 66 | 'categories': [y['name'] for y in i['categories']] if 'categories' in i.keys() else [], 67 | 'proof': i['proof'] if 'proof' in i.keys() else '', 68 | 'source': i['source'] if 'source' in i.keys() else '', 69 | 70 | } for i in data['body'] 71 | ] 72 | }), 200 73 | 74 | -------------------------------------------------------------------------------- /backend/views/api/history.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, jsonify, current_app, request 2 | from flask_classful import FlaskView, route 3 | 4 | from backend.utils.auth import login_required 5 | from backend import db, pynance 6 | 7 | 8 | class HistoryApiView(FlaskView): 9 | 10 | decorators = [ login_required ] 11 | 12 | @route('/orders', methods=['GET']) 13 | def get(self): 14 | from backend.models.bot import BotModel 15 | bot = BotModel.query.first() 16 | if bot.config.spot: 17 | return jsonify(list(sorted([i.to_dict(['bot_id', 'id', 'order_id', 'stop_loss', 'profit_target']) for i in bot.orders if i.spot == bot.config.spot], key=lambda x: x['updated']))[::-1]) 18 | else: 19 | return jsonify(list(sorted([i.to_dict(['bot_id', 'id', 'buying', 'order_id', 'sold_for']) for i in bot.orders if i.spot == bot.config.spot], key=lambda x: x['updated']))[::-1]) 20 | -------------------------------------------------------------------------------- /backend/views/api/keys.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, jsonify, current_app, request 2 | from flask_classful import FlaskView, route 3 | 4 | from backend.utils.auth import login_required 5 | from backend import db, pynance 6 | 7 | 8 | class KeysApiView(FlaskView): 9 | 10 | decorators = [ login_required ] 11 | 12 | def get(self): 13 | from backend.models.keys import KeysModel 14 | model = KeysModel.query.all() 15 | return jsonify([{i.value: i.key} for i in model]) 16 | -------------------------------------------------------------------------------- /backend/views/api/klines.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, jsonify, current_app, request 2 | from flask_classful import FlaskView, route 3 | from sqlalchemy import and_ 4 | from backend.utils.auth import login_required 5 | from backend import db, pynance 6 | 7 | 8 | class KlinesApiView(FlaskView): 9 | 10 | decorators = [ login_required ] 11 | 12 | def get(self): 13 | from backend.models.bot import BotModel 14 | from backend.models.orders import OrdersModel 15 | bot = BotModel.query.first() 16 | 17 | order = OrdersModel.query.filter(and_( 18 | OrdersModel.symbol==bot.status.target, 19 | OrdersModel.spot==bot.config.spot, 20 | OrdersModel.sandbox==bot.config.sandbox, 21 | OrdersModel.active==True, 22 | )).first() 23 | 24 | response_data = { 25 | 'target_type': 'WAITING FOR ONLINE STATUS', 26 | 'trade_type': 'NONE', 27 | 'target_type_status': 'NOT RUNNING', 28 | } 29 | kline_headers = ["Open time", "Open", "High", "Low", "Close", "Volume", "Close time", "Quote asset volume", "Number of trades", "Taker buy base asset volume", "Taker buy quote asset volume", "Ignore"] 30 | if order is None: response_data['order'] = False 31 | else: 32 | response_data['order'] = True 33 | response_data['symbol'] = order.symbol 34 | response_data['price_target'] = bot.status.average 35 | if order.spot: 36 | response_data['trade_type'] = 'SPOT' 37 | response_data['target_type'] = 'BUYING' if order.buying else 'SELLING' 38 | klines_data = list(sorted( 39 | pynance.assets.klines(order.symbol, timeframe=bot.graph_type, total_candles=bot.graph_interval), 40 | key=lambda x: x[0] 41 | )) 42 | klines_data.insert(0, kline_headers) 43 | response_data['klines'] = klines_data 44 | else: 45 | response_data['trade_type'] = 'FUTURES' 46 | response_data['target_type'] = 'PLACED' if order.stop_loss > 0 else 'PROCESSING' 47 | response_data['stop_loss'] = order.stop_loss 48 | response_data['take_profit'] = order.profit_target 49 | response_data['position'] = 'LONG' if order.buying else 'SHORT' 50 | klines_data = list(sorted( 51 | pynance.futures.assets.klines(order.symbol, timeframe=bot.graph_type, total_candles=bot.graph_interval), 52 | key=lambda x: x[0] 53 | )) 54 | klines_data.insert(0, kline_headers) 55 | response_data['klines'] = klines_data 56 | 57 | return jsonify(response_data) 58 | 59 | @route('/graph', methods=['GET', 'POST']) 60 | def set_graph(self): 61 | """Saves the graph view on the statistic page 62 | """ 63 | from backend.models.bot import BotModel 64 | bot = BotModel.query.first() 65 | if request.method == 'POST': 66 | data = request.json 67 | bot.update_data({'online': bot.online, 'graph_type': data['graph-type'], 'graph_interval': data['graph-interval']}) 68 | return jsonify({'graph-type': bot.graph_type, 'graph-interval': bot.graph_interval}), 200 69 | -------------------------------------------------------------------------------- /backend/views/api/logic.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, jsonify, current_app, request 2 | from flask_classful import FlaskView, route 3 | 4 | from backend.utils.auth import login_required 5 | from backend.utils.trading import Spot, Futures 6 | from backend import db, pynance 7 | 8 | from datetime import datetime 9 | import math, time 10 | 11 | 12 | class LogicApiView(FlaskView): 13 | 14 | # decorators = [ login_required ] # TODO enable 15 | 16 | def get(self): 17 | """This method is the brain of the bot, places orders etc 18 | 19 | Returns: 20 | [dict]: { 21 | online: bool, indicates if the bot is online, 22 | execution_time: datetime, the total time it took to execute this method 23 | } 24 | """ 25 | from backend.models.bot import BotModel 26 | bot = BotModel.query.first() 27 | 28 | if bot.config.spot: 29 | trade = Spot() 30 | else: 31 | trade = Futures() 32 | 33 | if trade.can_trade: 34 | for symbol in trade.get_active_symbols: 35 | time.sleep(1) 36 | print('\n\n') 37 | print('-'*50) 38 | print(symbol) 39 | print('-'*50) 40 | print('\n\n') 41 | can_still_trade = trade.prepare(symbol) 42 | if can_still_trade: 43 | trade.start() 44 | 45 | return jsonify(trade.fetch_results()) 46 | 47 | @route('toggle', methods=['POST', 'GET']) 48 | def toggle(self): 49 | """Toggles the bot offline and online 50 | """ 51 | from backend.models.bot import BotModel 52 | bot = BotModel.query.first() 53 | if request.method == 'POST': 54 | option = request.json['online'] 55 | bot.update_data({'online': option}) 56 | return jsonify({'online': bot.online}) 57 | 58 | @route('systembar', methods=['GET']) 59 | def systembar(self): 60 | """Gets called often to retrieve the status of the bot 61 | """ 62 | from backend.models.bot import BotModel 63 | bot = BotModel.query.first() 64 | return jsonify(bot.status.to_dict(['id'])) 65 | -------------------------------------------------------------------------------- /backend/views/api/prices.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, jsonify, current_app, request 2 | from flask_classful import FlaskView, route 3 | 4 | from backend.utils.auth import login_required 5 | from backend import db, pynance 6 | 7 | class PricesApiView(FlaskView): 8 | 9 | decorators = [ login_required ] 10 | 11 | def get(self): 12 | """Returns prices of all symbols available""" 13 | symbol = None 14 | if request.args.keys() and 'symbol' in request.args.keys(): 15 | symbol = request.args['symbol'] 16 | if symbol is None: data = pynance.assets.symbols() 17 | else: data = pynance.assets.symbols(symbol.upper()) 18 | if type(data.json) != list: data = [data] 19 | return jsonify(data.json), 200 -------------------------------------------------------------------------------- /backend/views/api/system.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, jsonify, current_app, request 2 | from flask_classful import FlaskView, route 3 | 4 | from backend.utils.auth import login_required 5 | from backend import db, pynance 6 | 7 | class SystemApiView(FlaskView): 8 | 9 | decorators = [ ] 10 | 11 | def get(self): 12 | """Returns system details 13 | 14 | Returns: 15 | { 16 | "authentication": false, 17 | "language": "en", 18 | "token": "", 19 | "version": "3.0.0" 20 | } 21 | """ 22 | from backend.models.system import SystemModel 23 | from backend.models.bot import BotModel 24 | system = SystemModel.query.first() 25 | bot = BotModel.query.first() 26 | return jsonify(system.to_dict(['id', 'updated', 'password'])), 200 27 | 28 | @route('/create', methods=['POST']) 29 | def create(self): 30 | from backend.models.system import SystemModel 31 | system = SystemModel.query.first() 32 | if request.headers['sk'] == current_app.config['SECRET_KEY']: 33 | system.set_password(request.json['pwd']) 34 | system.update_data({'authentication': True, 'tos': True}) 35 | return jsonify(system.to_dict(['id', 'updated', 'password'])), 200 36 | 37 | @login_required 38 | def post(self): 39 | from backend.models.system import SystemModel 40 | system = SystemModel.query.first() 41 | if 'pwd' in request.json.keys(): 42 | pwd = request.json['pwd'] 43 | if system.check_password(pwd): 44 | return jsonify({current_app.config['SECRET_KEY']: True}), 200 45 | return jsonify({'succes': False}), 200 46 | -------------------------------------------------------------------------------- /backend/views/api/trades.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, jsonify, current_app, request 2 | from flask_classful import FlaskView, route 3 | 4 | from backend.utils.auth import login_required 5 | from backend import db, pynance 6 | 7 | class TradesApiView(FlaskView): 8 | 9 | decorators = [ login_required ] 10 | 11 | def get(self): 12 | """Loads the current configuration and loads it into the store in the frontend. 13 | The user can change the values and save the changes to confirm the changes. 14 | 15 | Returns: 16 | [dict]: { 17 | 'assets': [A list of available assets from Binance], 18 | 'symbols': [The selected symbols to trade], 19 | 'symbols-choices': [A list of available symbols from Binance], 20 | 'timeframe-choices': [A list of available graph time frames], 21 | 'timeframe': Returns a string representing the selected timeframe, 22 | 'candle-interval': Returns an integer which represents the selected candle-interval, 23 | 'wallet-amount': Returns an integer which is the total amount of % available free coins to use when placing buy orders, 24 | 'below-average': Returns an integer which pushes the lowest average x% lower, 25 | 'profit-margin': Returns an integer which represent the profit % used for sell orders, 26 | 'profit-as': Is used to calculate all profit as the selected asset 27 | } 28 | """ 29 | symbols = [i['symbol'] for i in pynance.assets.symbols().json] 30 | try: 31 | assets = [i['coin'] for i in pynance.wallet.balance().json] 32 | except TypeError: 33 | return jsonify({'status': 'Invalid Binance API key provided'}), 200 34 | from backend.models.config import ConfigModel 35 | config = ConfigModel.query.first() 36 | return jsonify({ 37 | 'sandbox': config.sandbox, 38 | 'assets': assets, 39 | 'symbols': config.symbols, 40 | 'symbols-choices': symbols, 41 | 'timeframe-choices': ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w', '1M'], 42 | 'timeframe': config.timeframe, 43 | 'candle-interval': config.candle_interval, 44 | 'wallet-amount': config.wallet_amount, 45 | 'below-average': config.below_average, 46 | 'profit-margin': config.profit_margin, 47 | 'profit-as': config.profit_as, 48 | 'spot': config.spot, 49 | 'in-green': config.in_green, 50 | 'in-red': config.in_red, 51 | 'use-average': config.use_average, 52 | 'expected-leverage': config.expected_leverage, 53 | 'volume-timeframe': config.volume_timeframe, 54 | 'total-volume': config.total_volume, 55 | 'margin-type': config.margin_type, 56 | 'allow-multiple-orders': config.allow_multiple_orders, 57 | 'take_profit': config.take_profit 58 | }), 200 59 | 60 | def post(self): 61 | """Saves the users trade configuration which is used while trading 62 | """ 63 | from backend.models.bot import BotModel 64 | bot = BotModel.query.first() 65 | bot.update_data({'online': False}) 66 | data = request.json 67 | from backend.models.config import ConfigModel 68 | config = ConfigModel.query.first() 69 | config.update_data({**data}) 70 | [i.set_active(False) for i in bot.orders if i.symbol not in config.symbols] 71 | [i.set_active(True) for i in bot.orders if i.symbol in config.symbols and i.sold_for == 0] 72 | if not config.sandbox: config.remove_sandbox() 73 | return jsonify({}), 200 74 | -------------------------------------------------------------------------------- /backend/views/api/wallet.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, jsonify, current_app, request 2 | from flask_classful import FlaskView, route 3 | 4 | from backend.utils.auth import login_required 5 | from backend import db, pynance 6 | 7 | class WalletApiView(FlaskView): 8 | 9 | decorators = [ login_required ] 10 | 11 | def get(self): 12 | """Returns wallet balance""" 13 | from backend.models.bot import BotModel 14 | bot = BotModel.query.first() 15 | if bot.config.spot: data = pynance.wallet.balance() 16 | else: data = pynance.futures.wallet.balance() 17 | return jsonify({ 18 | 'status': data.response_info['status_code'], 19 | 'wallet': data.json 20 | }), 200 21 | -------------------------------------------------------------------------------- /backend/views/homepage/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_classful import FlaskView, route 2 | from flask import render_template 3 | 4 | from backend import db, pynance 5 | 6 | class HomePageView(FlaskView): 7 | 8 | decorators = [ ] 9 | 10 | def get(self): 11 | return render_template('index.html') -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | pn_database: 4 | build: 5 | context: . 6 | dockerfile: ./backend/config/docker/database.dockerfile 7 | restart: always 8 | command: --default-authentication-plugin=mysql_native_password 9 | container_name: pn_database 10 | hostname: pn_database 11 | environment: 12 | MYSQL_ROOT_PASSWORD: "PyNanceV3" 13 | MYSQL_DATABASE: "PYNANCE" 14 | volumes: 15 | - ./backend/config/docker/mysqld.cnf:/etc/mysql/mysql.conf.d/mysqld.cnf 16 | networks: 17 | - pynance 18 | 19 | pn_dashboard: 20 | container_name: pn_dashboard 21 | hostname: pn_dashboard 22 | build: 23 | context: . 24 | dockerfile: ./backend/config/docker/dashboard.dockerfile 25 | environment: 26 | FLASK_ENV: production 27 | FLASK_APP: "webserver.py:create_server()" 28 | FLASK_RUN_HOST: 0.0.0.0 29 | depends_on: 30 | - pn_database 31 | links: 32 | - pn_database 33 | networks: 34 | - pynance 35 | restart: on-failure 36 | 37 | pn_listener: 38 | container_name: pn_listener 39 | build: 40 | context: . 41 | dockerfile: ./backend/config/docker/listener.dockerfile 42 | depends_on: 43 | - pn_database 44 | - pn_dashboard 45 | links: 46 | - pn_database 47 | - pn_dashboard 48 | networks: 49 | - pynance 50 | restart: always 51 | 52 | nginx: 53 | container_name: nginx 54 | image: nginx:stable 55 | volumes: 56 | - ./backend/config/docker/nginx.conf:/etc/nginx/nginx.conf 57 | depends_on: 58 | - pn_dashboard 59 | links: 60 | - pn_dashboard 61 | ports: 62 | - 1337:5000 63 | networks: 64 | - pynance 65 | restart: 66 | always 67 | 68 | networks: 69 | pynance: 70 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # frontend 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "presets": [ 3 | "@vue/cli-plugin-babel/preset" 4 | ] 5 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pynance", 3 | "version": "3.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve --mode development", 7 | "build": "vue-cli-service build --mode production", 8 | "lint": "vue-cli-service lint", 9 | "i18n:report": "vue-cli-service i18n:report --src \"./src/**/*.?(js|vue)\" --locales \"./src/locales/**/*.json\"", 10 | "lint:fix": "vue-cli-service lint --fix", 11 | "watch": "vue-cli-service build --watch --mode development" 12 | }, 13 | "dependencies": { 14 | "axios": "^0.21.1", 15 | "bootstrap-vue": "^2.1.0", 16 | "core-js": "^3.6.5", 17 | "register-service-worker": "^1.7.1", 18 | "trading-vue-js": "^1.0.2", 19 | "vue": "^2.6.11", 20 | "vue-class-component": "^7.2.3", 21 | "vue-google-charts": "^0.3.3", 22 | "vue-property-decorator": "^9.1.2", 23 | "vue-router": "^3.2.0", 24 | "vuetify": "^2.4.0", 25 | "vuex": "^3.4.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/polyfill": "^7.7.0", 29 | "@typescript-eslint/eslint-plugin": "^4.18.0", 30 | "@typescript-eslint/parser": "^4.18.0", 31 | "@vue/cli-plugin-babel": "~4.5.0", 32 | "@vue/cli-plugin-eslint": "~4.5.0", 33 | "@vue/cli-plugin-pwa": "~4.5.0", 34 | "@vue/cli-plugin-router": "~4.5.0", 35 | "@vue/cli-plugin-typescript": "~4.5.0", 36 | "@vue/cli-plugin-vuex": "~4.5.0", 37 | "@vue/cli-service": "~4.5.0", 38 | "@vue/eslint-config-typescript": "^7.0.0", 39 | "bootstrap": "^4.3.1", 40 | "eslint": "^6.7.2", 41 | "eslint-plugin-vue": "^6.2.2", 42 | "mutationobserver-shim": "^0.3.3", 43 | "popper.js": "^1.16.0", 44 | "portal-vue": "^2.1.6", 45 | "sass": "^1.32.0", 46 | "sass-loader": "^10.0.0", 47 | "typescript": "~4.1.5", 48 | "vue-cli-plugin-bootstrap": "~0.4.0", 49 | "vue-cli-plugin-vuetify": "~2.4.0", 50 | "vue-template-compiler": "^2.6.11", 51 | "vuetify-loader": "^1.7.0" 52 | }, 53 | "eslintConfig": { 54 | "root": true, 55 | "env": { 56 | "node": true 57 | }, 58 | "extends": [ 59 | "plugin:vue/essential", 60 | "eslint:recommended", 61 | "@vue/typescript/recommended" 62 | ], 63 | "parserOptions": { 64 | "ecmaVersion": 2020 65 | }, 66 | "rules": { 67 | "no-debugger": 0, 68 | "no-useless-escape": "off", 69 | "prefer-const": "off", 70 | "@typescript-eslint/explicit-module-boundary-types": "off" 71 | } 72 | }, 73 | "browserslist": [ 74 | "> 1%", 75 | "last 2 versions", 76 | "not dead" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x78f1935/PyNance-Webinterface/ba57e6db8c3dc0aaf9ac80cfafd7332180e7b332/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x78f1935/PyNance-Webinterface/ba57e6db8c3dc0aaf9ac80cfafd7332180e7b332/frontend/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x78f1935/PyNance-Webinterface/ba57e6db8c3dc0aaf9ac80cfafd7332180e7b332/frontend/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x78f1935/PyNance-Webinterface/ba57e6db8c3dc0aaf9ac80cfafd7332180e7b332/frontend/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x78f1935/PyNance-Webinterface/ba57e6db8c3dc0aaf9ac80cfafd7332180e7b332/frontend/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/img/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x78f1935/PyNance-Webinterface/ba57e6db8c3dc0aaf9ac80cfafd7332180e7b332/frontend/public/img/icons/favicon.ico -------------------------------------------------------------------------------- /frontend/public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x78f1935/PyNance-Webinterface/ba57e6db8c3dc0aaf9ac80cfafd7332180e7b332/frontend/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /frontend/public/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 29 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |Welcome to PyNance!
18 |
19 | It looks like your setup is working correctly! You are a few steps away removed from using PyNance!
20 | In order to keep your experience as easy as possible PyNance will ask you a couple of questions.
21 | The questions relates to features PyNance has to offer. A couple of features do require some extra steps to make them work.
22 |
24 | Some questions can be skipped and can be configured at a later stage.
25 | Use the buttons below to navigate through the installation steps.
26 |
33 | This tool is not a money printing tool and contains risks.
34 | You are responsible for your own money and actions.
35 |
37 | By using this tool you agree to take FULL responsibility of your own money.
38 | Even if this would mean missed opportunities caused by the algorithm of this bot or worse; losses.
39 | Be responsible, Only trade with money you can afford to lose.
40 |
42 | By clicking Next you acknowledge the risks involved with crypto trading.
43 | Take chances, Make mistakes, Get messy.... To the moon!
44 |
48 | You can only access the bot with your master password once set!
49 | This action cannot be reverted. Keep your password safe.
50 |
75 | Would you like to see upcomming events? coinmarketcal.com provides amazing information on crypto.
76 | In order to enable news items you will need an API-key from coinmarketcal.com.
77 | Follow the next steps to obtain an API-key and provide it in the input field below:
78 |
97 | Thank you so much for using PyNance! A lot of work went into it.
98 | Make sure to leave a star on GitHub! Would be very much appriciated.
99 |
101 | You can hover over options to open-up tooltips!
102 | In the top left corner you will find the menu icon
103 | If you get stuck make sure to visit the
107 | If you like my work, feel free to send a tip to my BTC wallet 108 |
109 |
110 | 1HpywjQRi5jmpYxZXo32VAgVmyHZLacbJG
111 |
Status | 25 |Value | 26 |
---|---|
{{ item[0] }} | 31 |{{ item[1] }} | 32 |
Graph Legend | 48 |49 | |
---|---|
54 | |
56 | Current price | 57 |
60 | |
62 | Order placement | 63 |
66 | |
68 | Stop loss | 69 |
72 | |
74 | Take Profit | 75 |
Graph options | 92 |
---|
97 | |
109 |
112 | |
133 |