├── VERSION ├── app ├── backend │ ├── __init__.py │ ├── finance_reader │ │ ├── __init__.py │ │ ├── entities │ │ │ ├── brokers │ │ │ │ └── __init__.py │ │ │ ├── exchanges │ │ │ │ └── __init__.py │ │ │ ├── __init__.py │ │ │ └── csv_reader │ │ │ │ ├── __init__.py │ │ │ │ ├── coinbase.py │ │ │ │ ├── bitstamp.py │ │ │ │ └── kucoin.py │ │ └── handlers │ │ │ └── csv_read.py │ ├── .gitignore │ ├── README.md │ └── wallet_processor │ │ └── entities │ │ └── __init__.py ├── models │ ├── __init__.py │ ├── .gitignore │ ├── migrations │ │ ├── README │ │ ├── script.py.mako │ │ ├── versions │ │ │ └── 0002_2022-07-19-23-36.py │ │ └── alembic.ini │ ├── README.md │ ├── cryptography.py │ ├── alembic.ini │ └── dtos │ │ ├── broker_dtos.py │ │ └── exchange_dtos.py ├── api │ ├── api │ │ ├── accounts │ │ │ └── __init__.py │ │ ├── analysis │ │ │ └── __init__.py │ │ ├── crypto │ │ │ └── __init__.py │ │ ├── stock │ │ │ └── __init__.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_credential.py │ │ ├── users │ │ │ ├── __init__.py │ │ │ └── tests.py │ │ └── __init__.py │ ├── .gitignore │ ├── services │ │ ├── queue.py │ │ ├── redis.py │ │ └── elasticsearch.py │ ├── README.md │ └── tests.py ├── config │ ├── .gitignore │ ├── full.py │ ├── local.py │ └── __init__.py ├── Dockerfile.web ├── requirements.txt └── Dockerfile ├── frontend ├── public │ ├── static │ │ ├── .gitkeep │ │ └── img │ │ │ ├── vue-logo.png │ │ │ ├── background-2.jpg │ │ │ └── background │ │ │ ├── background-2.jpg │ │ │ └── background-2.png │ ├── robots.txt │ ├── img │ │ ├── queds.png │ │ ├── icons │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── mstile-150x150.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon-120x120.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── apple-touch-icon-180x180.png │ │ │ ├── apple-touch-icon-60x60.png │ │ │ ├── apple-touch-icon-76x76.png │ │ │ └── msapplication-icon-144x144.png │ │ └── laravel-vue.svg │ └── manifest.json ├── src │ ├── assets │ │ ├── sass │ │ │ ├── paper │ │ │ │ ├── _media-queries.scss │ │ │ │ ├── cards │ │ │ │ │ ├── _card-map.scss │ │ │ │ │ ├── _card-subcategories.scss │ │ │ │ │ ├── _card-info-area.scss │ │ │ │ │ ├── _card-lock.scss │ │ │ │ │ ├── _card-plain.scss │ │ │ │ │ ├── _card-tasks.scss │ │ │ │ │ ├── _card-contributions.scss │ │ │ │ │ ├── _card-chart.scss │ │ │ │ │ ├── _card-signup.scss │ │ │ │ │ ├── _card-stats.scss │ │ │ │ │ ├── _card-user.scss │ │ │ │ │ ├── _card-collapse.scss │ │ │ │ │ ├── _card-profile.scss │ │ │ │ │ ├── _card-stats-mini.scss │ │ │ │ │ ├── _card-background.scss │ │ │ │ │ ├── _card-testimonials.scss │ │ │ │ │ └── _card-pricing.scss │ │ │ │ ├── mixins │ │ │ │ │ ├── _tabs.scss │ │ │ │ │ ├── _cards.scss │ │ │ │ │ ├── _transparency.scss │ │ │ │ │ ├── _navbars.scss │ │ │ │ │ ├── _icons.scss │ │ │ │ │ ├── _page-header.scss │ │ │ │ │ ├── _modals.scss │ │ │ │ │ ├── _labels.scss │ │ │ │ │ ├── _dropdown.scss │ │ │ │ │ ├── _popovers.scss │ │ │ │ │ ├── _tags.scss │ │ │ │ │ ├── _social-buttons.scss │ │ │ │ │ ├── _wizard.scss │ │ │ │ │ ├── _sidebar.scss │ │ │ │ │ └── _chartist.scss │ │ │ │ ├── _images.scss │ │ │ │ ├── _mixins.scss │ │ │ │ ├── plugins │ │ │ │ │ ├── _plugin-vue-notifyjs.scss │ │ │ │ │ ├── element-ui │ │ │ │ │ │ ├── _plugin-slider.scss │ │ │ │ │ │ ├── _plugin-inputs.scss │ │ │ │ │ │ ├── _plugin-tables.scss │ │ │ │ │ │ ├── _plugin-tags.scss │ │ │ │ │ │ └── _plugin-select.scss │ │ │ │ │ └── _plugin-nprogress.scss │ │ │ │ ├── _progress.scss │ │ │ │ ├── _card-signup.scss │ │ │ │ ├── _footers.scss │ │ │ │ ├── _misc-extend.scss │ │ │ │ ├── _social-buttons.scss │ │ │ │ ├── _alerts.scss │ │ │ │ ├── _badges.scss │ │ │ │ ├── _button-icon.scss │ │ │ │ ├── _modals.scss │ │ │ │ ├── _cards.scss │ │ │ │ ├── _misc.scss │ │ │ │ ├── _pagination.scss │ │ │ │ ├── _tables.scss │ │ │ │ └── _typography.scss │ │ │ ├── paper-dashboard.scss │ │ │ └── demo.scss │ │ ├── logo.png │ │ ├── img │ │ │ └── vue-logo.png │ │ ├── fonts │ │ │ ├── themify.eot │ │ │ ├── themify.ttf │ │ │ ├── themify.woff │ │ │ ├── nucleo-icons.eot │ │ │ ├── nucleo-icons.ttf │ │ │ ├── nucleo-icons.woff │ │ │ ├── nucleo-icons.woff2 │ │ │ ├── glyphicons-halflings-regular.e18bbf6.ttf │ │ │ ├── glyphicons-halflings-regular.f4769f9.eot │ │ │ ├── glyphicons-halflings-regular.448c34a.woff2 │ │ │ └── glyphicons-halflings-regular.fa27723.woff │ │ ├── css │ │ │ └── custom.css │ │ └── custom.css │ ├── components │ │ ├── UIComponents │ │ │ ├── index.js │ │ │ ├── Charts │ │ │ │ ├── utils.js │ │ │ │ ├── mixins │ │ │ │ │ └── reactiveChart.js │ │ │ │ ├── PieChart.vue │ │ │ │ ├── BarChart.vue │ │ │ │ ├── plugins │ │ │ │ │ └── plugin-chart-text.js │ │ │ │ └── LineChart.vue │ │ │ ├── ValidationError.vue │ │ │ ├── SidebarPlugin │ │ │ │ ├── index.js │ │ │ │ ├── SidebarLink.vue │ │ │ │ └── SideBar.vue │ │ │ ├── Cards │ │ │ │ ├── StatsCard.vue │ │ │ │ ├── Card.vue │ │ │ │ └── ChartCard.vue │ │ │ └── Navbar │ │ │ │ └── Navbar.vue │ │ └── Dashboard │ │ │ └── Layout │ │ │ ├── Content.vue │ │ │ ├── AppFooter.vue │ │ │ ├── ContentFooter.vue │ │ │ ├── AppNavbar.vue │ │ │ ├── TopNavbar.vue │ │ │ └── NotFoundPage.vue │ ├── pages │ │ ├── 404.vue │ │ ├── Comments.vue │ │ ├── auth │ │ │ ├── Reset.vue │ │ │ ├── Email.vue │ │ │ └── Register.vue │ │ └── accounts │ │ │ └── UploadCSV.vue │ ├── isDemo.js │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── reset.js │ │ │ └── auth.js │ ├── middleware │ │ ├── guest.js │ │ └── auth.js │ ├── main.js │ ├── router │ │ └── index.js │ ├── filters.js │ ├── mixins │ │ └── form-mixin.js │ └── App.vue ├── .gitignore ├── Dockerfile ├── vite.config.js ├── package.json └── index.html ├── docs ├── img │ ├── taxes.png │ ├── wallet.png │ └── watchlist.png └── README.md ├── .gitattributes ├── .gitignore ├── nginx_template.conf └── docker-compose.yml /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.0 2 | -------------------------------------------------------------------------------- /app/backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.gitignore: -------------------------------------------------------------------------------- 1 | bank.py -------------------------------------------------------------------------------- /app/api/api/accounts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/api/analysis/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/api/crypto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/api/stock/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/api/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/api/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/backend/finance_reader/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/config/.gitignore: -------------------------------------------------------------------------------- 1 | heroku.py 2 | prod.py -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_media-queries.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /app/models/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /docs/img/taxes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/docs/img/taxes.png -------------------------------------------------------------------------------- /docs/img/wallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/docs/img/wallet.png -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-map.scss: -------------------------------------------------------------------------------- 1 | .map{ 2 | height: 500px; 3 | } 4 | -------------------------------------------------------------------------------- /docs/img/watchlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/docs/img/watchlist.png -------------------------------------------------------------------------------- /frontend/public/img/queds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/img/queds.png -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.vue linguist-detectable=false 2 | *.css linguist-detectable=false 3 | *.scss linguist-detectable=false -------------------------------------------------------------------------------- /app/backend/.gitignore: -------------------------------------------------------------------------------- 1 | wallet_processor/orders.csv 2 | wallet_processor/utils/*.csv 3 | wallet_processor/utils/*.json 4 | -------------------------------------------------------------------------------- /frontend/src/assets/img/vue-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/src/assets/img/vue-logo.png -------------------------------------------------------------------------------- /frontend/public/static/img/vue-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/static/img/vue-logo.png -------------------------------------------------------------------------------- /frontend/src/assets/fonts/themify.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/src/assets/fonts/themify.eot -------------------------------------------------------------------------------- /frontend/src/assets/fonts/themify.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/src/assets/fonts/themify.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/themify.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/src/assets/fonts/themify.woff -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-subcategories.scss: -------------------------------------------------------------------------------- 1 | .card-subcategories .card-body{ 2 | padding-bottom: 30px; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /frontend/public/static/img/background-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/static/img/background-2.jpg -------------------------------------------------------------------------------- /frontend/src/assets/fonts/nucleo-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/src/assets/fonts/nucleo-icons.eot -------------------------------------------------------------------------------- /frontend/src/assets/fonts/nucleo-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/src/assets/fonts/nucleo-icons.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/nucleo-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/src/assets/fonts/nucleo-icons.woff -------------------------------------------------------------------------------- /frontend/src/assets/fonts/nucleo-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/src/assets/fonts/nucleo-icons.woff2 -------------------------------------------------------------------------------- /frontend/public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_tabs.scss: -------------------------------------------------------------------------------- 1 | @mixin pill-style($color) { 2 | border: 1px solid $color; 3 | color: $color; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /frontend/public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /frontend/public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /frontend/public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /frontend/public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /frontend/public/static/img/background/background-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/static/img/background/background-2.jpg -------------------------------------------------------------------------------- /frontend/public/static/img/background/background-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/static/img/background/background-2.png -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_cards.scss: -------------------------------------------------------------------------------- 1 | @mixin icon-color($color) { 2 | box-shadow: 0px 9px 30px -6px $color; 3 | color: $color; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /frontend/src/assets/fonts/glyphicons-halflings-regular.e18bbf6.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/src/assets/fonts/glyphicons-halflings-regular.e18bbf6.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/glyphicons-halflings-regular.f4769f9.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/src/assets/fonts/glyphicons-halflings-regular.f4769f9.eot -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-info-area.scss: -------------------------------------------------------------------------------- 1 | .card .info-area{ 2 | padding: 40px 0 40px; 3 | text-align: center; 4 | position: relative; 5 | z-index: 2; 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/glyphicons-halflings-regular.448c34a.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/src/assets/fonts/glyphicons-halflings-regular.448c34a.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/glyphicons-halflings-regular.fa27723.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbatalle/queds/HEAD/frontend/src/assets/fonts/glyphicons-halflings-regular.fa27723.woff -------------------------------------------------------------------------------- /app/api/.gitignore: -------------------------------------------------------------------------------- 1 | flask/ 2 | *.pyc 3 | dev 4 | env/ 5 | env__/ 6 | .vscode/symbols.json 7 | app/db.sqlite3 8 | config/ 9 | models/ 10 | dist/ 11 | services/cookies.pickle 12 | fly.toml -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_images.scss: -------------------------------------------------------------------------------- 1 | img{ 2 | max-width: 100%; 3 | border-radius: $border-radius-small; 4 | } 5 | .img-raised{ 6 | box-shadow: $box-shadow-raised; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/index.js: -------------------------------------------------------------------------------- 1 | import Card from './Cards/Card.vue'; 2 | import Navbar from '../UIComponents/Navbar/Navbar.vue'; 3 | 4 | 5 | export { 6 | Card, 7 | Navbar 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/pages/404.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_transparency.scss: -------------------------------------------------------------------------------- 1 | // Opacity 2 | 3 | @mixin opacity($opacity) { 4 | opacity: $opacity; 5 | // IE8 filter 6 | $opacity-ie: ($opacity * 100); 7 | filter: #{alpha(opacity=$opacity-ie)}; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/isDemo.js: -------------------------------------------------------------------------------- 1 | // isDemo.js 2 | export default { 3 | install: (app) => { 4 | console.log("Demo mode:", import.meta.env.VITE_APP_IS_DEMO); 5 | app.config.globalProperties.$isDemo = import.meta.env.VITE_APP_IS_DEMO == 'true'; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex'; 2 | 3 | import auth from "./modules/auth"; 4 | import reset from "./modules/reset"; 5 | 6 | export default createStore({ 7 | modules: { 8 | auth, 9 | reset 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_navbars.scss: -------------------------------------------------------------------------------- 1 | @mixin navbar-color($color) { 2 | background-color: $color; 3 | } 4 | 5 | @mixin center-item() { 6 | left: 0; 7 | right: 0; 8 | margin-right: auto; 9 | margin-left: auto; 10 | position: absolute; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-lock.scss: -------------------------------------------------------------------------------- 1 | .card-lock{ 2 | .card-header{ 3 | img{ 4 | width: 120px; 5 | height: 120px; 6 | border-radius: 50%; 7 | margin-top: -70px; 8 | } 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/middleware/guest.js: -------------------------------------------------------------------------------- 1 | import store from "../store"; 2 | 3 | export default function guest({next, router}) { 4 | console.log("Middleware guest"); 5 | if (store.getters.isLoggedIn) { 6 | return router.push({path: "/"}); 7 | } 8 | return next(); 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/middleware/auth.js: -------------------------------------------------------------------------------- 1 | import store from "../store"; 2 | 3 | export default function auth({next, router}) { 4 | console.log("Middleware auth"); 5 | if (!store.getters.isLoggedIn) { 6 | return router.push({name: "Login"}); 7 | } 8 | return next(); 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node artifact files 2 | node_modules/ 3 | dist/ 4 | 5 | # Compiled Python bytecode 6 | *.py[cod] 7 | 8 | # Log files 9 | *.log 10 | 11 | # JetBrains IDE 12 | .idea/ 13 | 14 | 15 | **/__pycache__/ 16 | *.pyc 17 | docker-compose.heroku.yml 18 | deploy_fly.sh 19 | docker-compose.local.yml 20 | nginx_template.local.yml -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_icons.scss: -------------------------------------------------------------------------------- 1 | @mixin icon-background($icon-url) { 2 | background-image: url($icon-url); 3 | 4 | } 5 | 6 | @mixin icon-shape($size, $padding, $border-radius) { 7 | height: $size; 8 | width: $size; 9 | padding: $padding; 10 | border-radius: $border-radius; 11 | display: inline-table; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/config/full.py: -------------------------------------------------------------------------------- 1 | DEMO_MODE = False 2 | DEBUG = False 3 | 4 | SQL_CONF = { 5 | 'db_type': 'postgresql', 6 | 'user': 'queds_user', 7 | 'password': 'Kbh85n7M6Fxo', 8 | 'host': 'db', 9 | 'port': '5432', 10 | 'database': 'queds', 11 | 'options': {} 12 | } 13 | 14 | REDIS = { 15 | 'host': 'redis', 16 | 'port': 6379 17 | } -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/Charts/utils.js: -------------------------------------------------------------------------------- 1 | export function hexToRGB(hex, alpha) { 2 | const r = parseInt(hex.slice(1, 3), 16), 3 | g = parseInt(hex.slice(3, 5), 16), 4 | b = parseInt(hex.slice(5, 7), 16); 5 | 6 | if (alpha) { 7 | return `rgba(${r},${g},${b}, ${alpha})`; 8 | } else { 9 | return `rgb(${r},${g},${b})`; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .env 27 | -------------------------------------------------------------------------------- /app/config/local.py: -------------------------------------------------------------------------------- 1 | DEMO_MODE = False 2 | DEBUG = True 3 | 4 | SQL_CONF = { 5 | 'db_type': 'postgresql', 6 | 'user': 'queds_user', 7 | 'password': 'Kbh85n7M6Fxo', 8 | 'host': 'db', 9 | 'port': '5432', 10 | 'database': 'queds', 11 | 'options': {} 12 | } 13 | 14 | SQL_CONF = { 15 | 'db_type': 'sqlite', 16 | 'database': 'queds.sqlite' 17 | } 18 | 19 | REDIS = None 20 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-plain.scss: -------------------------------------------------------------------------------- 1 | 2 | .card-plain{ 3 | background: transparent; 4 | box-shadow: none; 5 | 6 | .card-header, 7 | .card-footer{ 8 | margin-left: 0; 9 | margin-right: 0; 10 | background-color: transparent; 11 | } 12 | 13 | &:not(.card-subcategories).card-body{ 14 | padding-left: 0; 15 | padding-right: 0; 16 | } 17 | } 18 | 19 | .card-transparent { 20 | background: transparent; 21 | box-shadow: none; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_page-header.scss: -------------------------------------------------------------------------------- 1 | @mixin linear-gradient($color1, $color2){ 2 | background: $color1; /* For browsers that do not support gradients */ 3 | background: -webkit-linear-gradient(90deg, $color1 , $color2); /* For Safari 5.1 to 6.0 */ 4 | background: -o-linear-gradient(90deg, $color1, $color2); /* For Opera 11.1 to 12.0 */ 5 | background: -moz-linear-gradient(90deg, $color1, $color2); /* For Firefox 3.6 to 15 */ 6 | background: linear-gradient(0deg, $color1 , $color2); /* Standard syntax */ 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_mixins.scss: -------------------------------------------------------------------------------- 1 | //Utilities 2 | @import "mixins/transparency"; 3 | 4 | @import "mixins/vendor-prefixes"; 5 | @import "mixins/vendor-prefixes-extend"; 6 | @import "mixins/buttons"; 7 | @import "mixins/inputs"; 8 | @import "mixins/dropdown"; 9 | @import "mixins/page-header"; 10 | @import "mixins/cards"; 11 | 12 | //Components 13 | @import "mixins/social-buttons"; 14 | @import "mixins/wizard"; 15 | @import "mixins/tags"; 16 | 17 | @import "mixins/popovers"; 18 | @import "mixins/modals"; 19 | @import "mixins/chartist"; 20 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "queds-finance-portfolio", 3 | "short_name": "queds", 4 | "icons": [ 5 | { 6 | "src": "./img/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./img/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "./index.html", 17 | "display": "standalone", 18 | "background_color": "#000000", 19 | "theme_color": "#4DBA87" 20 | } 21 | -------------------------------------------------------------------------------- /app/models/README.md: -------------------------------------------------------------------------------- 1 | README.md 2 | 3 | ## Requirements 4 | sudo apt-get install python-psycopg2 5 | 6 | ## Generate revision 7 | alembic revision --autogenerate 8 | 9 | ## Apply migration 10 | alembic -c local.ini upgrade head 11 | 12 | ## Downgrade migration 13 | alembic -c local.ini downgrade -1 14 | 15 | ## Use fixtures 16 | Use fixtures file in order to fill the database with basic information 17 | 18 | ## Problems 19 | - In case of delete all the database, some types should be removed: 20 | ``` 21 | DROP TYPE "mode"; 22 | DROP TYPE "cred_type"; 23 | ``` -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:21.7.1-bullseye 2 | 3 | ARG VITE_APP_BACKEND_URL 4 | ENV VITE_APP_BACKEND_URL=$VITE_APP_BACKEND_URL 5 | 6 | ARG VITE_APP_IS_DEMO 7 | ENV VITE_APP_IS_DEMO=$VITE_APP_IS_DEMO 8 | 9 | WORKDIR /app 10 | 11 | ENV PATH /app/node_modules/.bin:$PATH 12 | 13 | #RUN apk add python make g++ 14 | 15 | COPY ./package.json /app/package.json 16 | COPY ./package-lock.json /app/package-lock.json 17 | RUN npm install 18 | # RUN npm install @vue/cli -g 19 | 20 | COPY . /app 21 | # RUN rm -rf app/node_modules/.vite 22 | 23 | #CMD npm run dev 24 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/ValidationError.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 27 | -------------------------------------------------------------------------------- /frontend/src/components/Dashboard/Layout/Content.vue: -------------------------------------------------------------------------------- 1 | 10 | 13 | 27 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/Charts/mixins/reactiveChart.js: -------------------------------------------------------------------------------- 1 | import { Line, mixins } from 'vue-chartjs' 2 | 3 | export default { 4 | mixins: [mixins.reactiveData], 5 | data() { 6 | return { 7 | fallBackColor: '#f96332', 8 | options: {} 9 | } 10 | }, 11 | watch: { 12 | data: { 13 | deep: true, 14 | handler(data) { 15 | this.chartData = this.assignChartData({ data }); 16 | } 17 | }, 18 | datasets(datasets) { 19 | this.chartData = this.assignChartData({ datasets }); 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_modals.scss: -------------------------------------------------------------------------------- 1 | @mixin modal-colors($bg-color, $color) { 2 | .modal-content{ 3 | background-color: $bg-color; 4 | color: $color; 5 | } 6 | 7 | .modal-header .close{ 8 | color: $color; 9 | } 10 | 11 | //inputs 12 | @include input-coloured-bg($opacity-5, $white-color, $white-color, $transparent-bg, $opacity-1, $opacity-2); 13 | 14 | .input-group-addon, 15 | .form-group.no-border .input-group-addon, 16 | .input-group.no-border .input-group-addon{ 17 | color: $opacity-8; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/backend/finance_reader/entities/brokers/__init__.py: -------------------------------------------------------------------------------- 1 | from finance_reader.entities import AbstractEntity 2 | 3 | 4 | class AbstractBroker(AbstractEntity): 5 | 6 | def __init__(self): 7 | super(AbstractBroker, self).__init__() 8 | 9 | 10 | from finance_reader.entities.brokers.degiro import Degiro 11 | from finance_reader.entities.brokers.clicktrade import Clicktrade 12 | from finance_reader.entities.brokers.interactive_brokers import InteractiveBrokers 13 | 14 | 15 | SUPPORTED_BROKERS = { 16 | "degiro": Degiro(), 17 | "clicktrade": Clicktrade(), 18 | "ib": InteractiveBrokers() 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/plugins/_plugin-vue-notifyjs.scss: -------------------------------------------------------------------------------- 1 | .notifications.vue-notifyjs { 2 | .alert { 3 | z-index: 1000; 4 | position: fixed; 5 | } 6 | .notification-list-move { 7 | transition: transform 0.3s, opacity 0.4s; 8 | } 9 | .notification-list-item { 10 | display: inline-block; 11 | margin-right: 10px; 12 | 13 | } 14 | .notification-list-enter-active, .notification-list-leave-active { 15 | transition: opacity 0.4s; 16 | } 17 | .notification-list-enter, .notification-list-leave-to /* .list-leave-active for <2.1.8 */ 18 | { 19 | opacity: 0; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import Components from 'unplugin-vue-components/vite'; 4 | import path from 'path'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | Components() 11 | ], resolve: { 12 | alias: { 13 | '@/': `${path.resolve(__dirname, 'src')}/` 14 | } 15 | }, 16 | define: { 17 | __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, 18 | __VUE_OPTIONS_API__: true, 19 | __VUE_PROD_DEVTOOLS__: false, 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /app/models/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_labels.scss: -------------------------------------------------------------------------------- 1 | @mixin label-style() { 2 | padding: $padding-label-vertical $padding-label-horizontal; 3 | border: 1px solid $default-color; 4 | border-radius: $border-radius-small; 5 | color: $default-color; 6 | font-weight: $font-weight-semi; 7 | font-size: $font-size-small; 8 | text-transform: uppercase; 9 | display: inline-block; 10 | vertical-align: middle; 11 | } 12 | 13 | @mixin label-color($color) { 14 | border-color: $color; 15 | color: $color; 16 | } 17 | 18 | @mixin label-color-fill($color) { 19 | border-color: $color; 20 | color: $white-color; 21 | background-color: $color; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_dropdown.scss: -------------------------------------------------------------------------------- 1 | @mixin dropdown-colors($brand-color, $dropdown-header-color, $dropdown-color, $background-color ) { 2 | background-color: $brand-color; 3 | 4 | &:before{ 5 | color: $brand-color; 6 | } 7 | 8 | .dropdown-header:not([href]):not([tabindex]){ 9 | color: $dropdown-header-color; 10 | } 11 | 12 | .dropdown-item{ 13 | color: $dropdown-color; 14 | 15 | &:hover, 16 | &:focus{ 17 | background-color: $background-color; 18 | } 19 | } 20 | 21 | .dropdown-divider{ 22 | background-color: $background-color; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/assets/css/custom.css: -------------------------------------------------------------------------------- 1 | .login-page .full-page > .content { 2 | padding-top: 8vh; 3 | } 4 | 5 | .animation-transition-general { 6 | -webkit-transition: all 300ms linear; 7 | -moz-transition: all 300ms linear; 8 | -o-transition: all 300ms linear; 9 | -ms-transition: all 300ms linear; 10 | transition: all 300ms linear; 11 | } 12 | 13 | .btn-inner--icon i:not(.fa) { 14 | position: relative; 15 | top: 2px; 16 | } 17 | 18 | .sidebar .nav .btn-neutral p, .sidebar .nav .btn-neutral i{ 19 | color: black; 20 | } 21 | 22 | .vue-notifyjs .alert .alert-icon { 23 | font-size: 20px !important; 24 | margin-right: 5px; 25 | margin-bottom: 3px; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from './store'; 5 | import { toCurrency, round } from './filters'; 6 | import ElementPlus from 'element-plus'; 7 | 8 | import IsDemo from './isDemo'; 9 | 10 | import './assets/sass/paper-dashboard.scss'; 11 | import './assets/sass/demo.scss'; 12 | import './assets/custom.css'; 13 | 14 | const app = createApp(App); 15 | 16 | app.config.globalProperties.$filters = { 17 | toCurrency, 18 | round 19 | }; 20 | app.use(ElementPlus); 21 | 22 | store.dispatch('initialize') 23 | 24 | app.use(IsDemo).use(router).use(store).mount("#app"); 25 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-tasks.scss: -------------------------------------------------------------------------------- 1 | .card-tasks{ 2 | text-align: left; 3 | .table tbody{ 4 | td:last-child{ 5 | padding-right: 0; 6 | display: inline-flex; 7 | .btn{ 8 | padding: 3px; 9 | } 10 | } 11 | td:first-child{ 12 | padding-left: 0; 13 | } 14 | td { 15 | padding: 12px 8px !important; 16 | } 17 | } 18 | .table-full-width{ 19 | padding-bottom: 0 !important; 20 | } 21 | .card-footer{ 22 | padding-top: 0; 23 | } 24 | .table{ 25 | margin-bottom: 0 !important; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_progress.scss: -------------------------------------------------------------------------------- 1 | .progress { 2 | background-color: $medium-gray; 3 | border-radius: $border-radius-small; 4 | box-shadow: none; 5 | height: 8px; 6 | } 7 | .progress-thin{ 8 | height: 4px; 9 | } 10 | .progress-bar{ 11 | background-color: $primary-color; 12 | } 13 | .progress-bar-primary{ 14 | background-color: $primary-color; 15 | } 16 | .progress-bar-info{ 17 | background-color: $info-color; 18 | } 19 | .progress-bar-success{ 20 | background-color: $success-color; 21 | } 22 | .progress-bar-warning{ 23 | background-color: $warning-color; 24 | } 25 | .progress-bar-danger{ 26 | background-color: $danger-color; 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_popovers.scss: -------------------------------------------------------------------------------- 1 | @mixin popover-color($color, $text-color) { 2 | background-color: $color; 3 | 4 | .popover-body{ 5 | color: $text-color; 6 | } 7 | 8 | 9 | &.bs-popover-right .arrow:after{ 10 | border-right-color:$color; 11 | } 12 | 13 | &.bs-popover-top .arrow:after{ 14 | border-top-color:$color; 15 | } 16 | 17 | &.bs-popover-bottom .arrow:after{ 18 | border-bottom-color:$color; 19 | } 20 | 21 | &.bs-popover-left .arrow:after{ 22 | border-left-color:$color; 23 | } 24 | 25 | .popover-header{ 26 | color: $text-color; 27 | opacity: .6; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from "vue-router"; 2 | 3 | 4 | import routes from "./routes"; 5 | // 6 | // const routes = [ 7 | // { 8 | // path: "/:pathMatch(.*)*", 9 | // redirect: "/404", 10 | // }, 11 | // { 12 | // path: "/", 13 | // component: () => import("../pages/Home.vue"), 14 | // }, 15 | // { 16 | // path: "/home", 17 | // component: () => import("../pages/Home.vue"), 18 | // }, 19 | // { 20 | // path: "/404", 21 | // component: () => import("../pages/404.vue"), 22 | // }, 23 | // ]; 24 | const router = createRouter({ 25 | history: createWebHistory(), 26 | routes, 27 | }); 28 | 29 | export default router; 30 | -------------------------------------------------------------------------------- /frontend/src/components/Dashboard/Layout/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 16 | 26 | 28 | -------------------------------------------------------------------------------- /app/models/migrations/versions/0002_2022-07-19-23-36.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 0002 4 | Revises: 5 | Create Date: 2025-11-19 11:25:58.479160 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from models.fixtures import upgrade_fixtures 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '0002' 15 | down_revision = '0001' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | upgrade_fixtures() 22 | 23 | 24 | def downgrade(): 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | conn = op.get_bind() 27 | conn.execute(sa.text("""truncate entities cascade;""")) 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_tags.scss: -------------------------------------------------------------------------------- 1 | @mixin tag-color ($color){ 2 | background-color: $color; 3 | color: $white-color; 4 | border:none; 5 | .tagsinput-remove-link{ 6 | color: $white-color; 7 | } 8 | .tagsinput-add{ 9 | color: $color; 10 | } 11 | } 12 | 13 | @mixin create-colored-tags(){ 14 | &.tag-primary{ 15 | @include tag-color($brand-primary); 16 | } 17 | &.tag-info { 18 | @include tag-color($brand-info); 19 | } 20 | &.tag-success{ 21 | @include tag-color($brand-success); 22 | } 23 | &.tag-warning{ 24 | @include tag-color($brand-warning); 25 | } 26 | &.tag-danger{ 27 | @include tag-color($brand-danger); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Dockerfile.web: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim-buster 2 | 3 | RUN apt-get update && apt-get -y install libpq-dev gcc 4 | # to be used by heroku for demo page 5 | 6 | #COPY ./app /usr/src 7 | COPY requirements.txt /usr/src 8 | #COPY app/models /user/src/models 9 | #COPY ./app/config /user/src/config 10 | # COPY frontend/dist /user/src/dist 11 | COPY VERSION /usr/VERSION 12 | 13 | WORKDIR /usr/src 14 | 15 | # set environment variables 16 | ENV PYTHONDONTWRITEBYTECODE 1 17 | ENV PYTHONUNBUFFERED 1 18 | ENV BACKEND_SETTINGS config.heroku 19 | 20 | # install python dependencies 21 | RUN pip install --upgrade pip 22 | RUN pip install --no-cache-dir -r /usr/src/requirements.txt 23 | 24 | # gunicorn 25 | #CMD ["gunicorn", "--config", "gunicorn-cfg.py", "run:app"] 26 | 27 | CMD python app.py run -h 0.0.0.0 28 | -------------------------------------------------------------------------------- /app/api/api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_jwt_extended import get_jwt_identity 2 | 3 | from config import settings 4 | from functools import wraps 5 | 6 | blacklist = set() # jwt blacklist 7 | 8 | 9 | def add_token_to_blacklist(jti): 10 | blacklist.add(jti) 11 | 12 | 13 | def is_token_blacklisted(jti): 14 | return jti in blacklist 15 | 16 | 17 | def filter_by_username(object): 18 | user_id = get_jwt_identity() 19 | return object.query.filter(object.user_id == user_id) 20 | 21 | 22 | def demo_check(f): 23 | """Checks if the platform is in demo mode""" 24 | @wraps(f) 25 | def decorator(*args, **kwargs): 26 | if settings.DEMO_MODE: 27 | return {"success": False, "message": "Demo mode"}, 400 28 | return f(*args, **kwargs) 29 | 30 | return decorator 31 | -------------------------------------------------------------------------------- /frontend/src/components/Dashboard/Layout/ContentFooter.vue: -------------------------------------------------------------------------------- 1 | 14 | 24 | 27 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | # api 2 | Flask==3.0.3 3 | flask-restx==1.3.0 4 | flask_jwt_extended==4.7.1 5 | # Flask-SQLAlchemy==2.5.1 6 | flask_sqlalchemy==3.1.1 7 | Flask-Cors==3.0.10 8 | pytest==8.1.1 9 | gunicorn==20.1.0 10 | psycopg2-binary==2.9.9 11 | flask-bcrypt==0.7.1 12 | # SQLAlchemy==1.4.52 13 | sqlalchemy==2.0.35 14 | sqlalchemy_utils 15 | elasticsearch<7.14.0 16 | dnspython==1.16.0 17 | alembic==1.7.7 18 | jinja2==3.1.2 19 | 20 | # backend 21 | # psycopg2==2.9.3 22 | pytz==2025.2 23 | # SQLAlchemy==1.4.52 24 | pyexecjs==1.5.1 25 | bs4==0.0.1 26 | bcrypt==3.2.0 27 | lxml==5.4.0 28 | python-telegram-bot==13.13 29 | mock==5.1.0 30 | 31 | # generic 32 | requests==2.32.3 33 | kombu==5.5.0 34 | redis==4.2.0 35 | pycryptodome==3.22.0 36 | werkzeug==3.0.4 37 | 38 | curl_cffi==0.10.0 39 | 40 | python-dateutil==2.9.0 -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/plugins/element-ui/_plugin-slider.scss: -------------------------------------------------------------------------------- 1 | @import "../../variables"; 2 | 3 | @mixin slider($name, $color, $height: 6px){ 4 | .slider-#{$name}{ 5 | .el-slider__bar { 6 | height: $height; 7 | background-color: $color; 8 | } 9 | .el-tooltip{ 10 | border: none; 11 | } 12 | .el-slider__button{ 13 | &.hover, 14 | &:hover, 15 | &.dragging { 16 | background-color: darken($color, 10%) 17 | } 18 | background-color: $color; 19 | height: $height * 3; 20 | width: $height * 3; 21 | } 22 | } 23 | } 24 | 25 | @include slider('info', $info-color); 26 | @include slider('primary', $primary-color); 27 | @include slider('success', $success-color); 28 | @include slider('warning', $warning-color); 29 | @include slider('danger', $danger-color); 30 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-contributions.scss: -------------------------------------------------------------------------------- 1 | .card-contributions{ 2 | @extend %card-stats; 3 | 4 | .card-description{ 5 | max-width: 350px; 6 | margin: 0 auto; 7 | margin-bottom: 20px; 8 | } 9 | 10 | .card-title{ 11 | padding-top: 35px; 12 | } 13 | 14 | .card-stats{ 15 | display: flex; 16 | align-items: center; 17 | flex-direction: row; 18 | padding: 11px; 19 | } 20 | 21 | .card-footer{ 22 | [class*="col-"]:not(:first-child):before{ 23 | content: ""; 24 | position: absolute; 25 | left: 0; 26 | width: 1px; 27 | height: 100%; 28 | background-color: $hr-line; 29 | } 30 | } 31 | 32 | .bootstrap-switch{ 33 | margin: 0; 34 | } 35 | 36 | span{ 37 | padding-left: 15px; 38 | text-align: left; 39 | max-width: 125px; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base stage 2 | FROM python:3.12-slim AS base 3 | 4 | RUN apt-get update && apt-get -y install libpq-dev gcc 5 | 6 | WORKDIR /usr/src 7 | 8 | COPY ./requirements.txt /usr/src/requirements.txt 9 | 10 | # Set environment variables 11 | ENV PYTHONDONTWRITEBYTECODE 1 12 | ENV PYTHONUNBUFFERED 1 13 | 14 | # Install python dependencies 15 | RUN pip install --upgrade pip && \ 16 | pip install --no-cache-dir -r requirements.txt 17 | 18 | 19 | # API stage 20 | FROM base AS api 21 | 22 | WORKDIR /usr/src/ 23 | 24 | # Copy only API-related files 25 | # COPY ./api /usr/src/api 26 | RUN ls /usr/src 27 | 28 | # CMD ["python", "app.py", "run", "-h", "0.0.0.0:${PORT}"] 29 | 30 | # Backend stage 31 | FROM base AS backend 32 | 33 | WORKDIR /usr/src/ 34 | 35 | # Copy only backend-related files 36 | COPY ./backend /usr/src/backend 37 | 38 | # CMD ["python", "worker.py"] -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-chart.scss: -------------------------------------------------------------------------------- 1 | .card-chart { 2 | .card-header{ 3 | .card-title{ 4 | margin-top: 10px; 5 | margin-bottom: 0; 6 | } 7 | .card-category{ 8 | margin-bottom: 5px; 9 | } 10 | } 11 | 12 | .table{ 13 | margin-bottom: 0; 14 | 15 | td{ 16 | border-top: none; 17 | border-bottom: 1px solid #e9ecef; 18 | } 19 | } 20 | 21 | .card-progress { 22 | margin-top: 30px; 23 | } 24 | 25 | .chart-area { 26 | height: 190px; 27 | width: calc(100% + 30px); 28 | margin-left: -15px; 29 | margin-right: -15px; 30 | } 31 | .card-footer { 32 | margin-top: 15px; 33 | 34 | .stats{ 35 | color: $dark-gray; 36 | } 37 | } 38 | 39 | .dropdown{ 40 | position: absolute; 41 | right: 20px; 42 | top: 20px; 43 | 44 | .btn{ 45 | margin: 0; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/plugins/element-ui/_plugin-inputs.scss: -------------------------------------------------------------------------------- 1 | @import "../../variables"; 2 | 3 | .form-group { 4 | .el-date-editor { 5 | width: 100% !important; 6 | } 7 | .el-input__inner { 8 | @extend .form-control; 9 | width: 100%; 10 | } 11 | .el-select{ 12 | width: 100%; 13 | .el-input__inner{ 14 | cursor: pointer !important; 15 | } 16 | } 17 | .el-input-number{ 18 | width: 100%; 19 | .plus-button{ 20 | @extend .btn-round, .btn-primary; 21 | padding: 0 !important; 22 | border: 0; 23 | } 24 | .el-input-number__decrease{ 25 | @extend .plus-button; 26 | border-radius: $btn-round-radius 0 0 $btn-round-radius !important; 27 | } 28 | .el-input-number__increase{ 29 | @extend .plus-button; 30 | border-radius: 0 $btn-round-radius $btn-round-radius 0 !important; 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/plugins/element-ui/_plugin-tables.scss: -------------------------------------------------------------------------------- 1 | @import "../../variables"; 2 | 3 | // Element UI override 4 | 5 | .el-table table { 6 | @extend .table; 7 | } 8 | 9 | .el-table { 10 | .td-total { 11 | font-weight: $font-weight-bold; 12 | font-size: $font-size-h5; 13 | padding-top: 20px; 14 | text-align: right; 15 | } 16 | .td-price{ 17 | font-size: 26px; 18 | font-weight: $font-weight-light; 19 | margin-top: 5px; 20 | position: relative; 21 | top: 4px; 22 | text-align: right; 23 | } 24 | 25 | .table-actions{ 26 | .btn{ 27 | margin-right: 5px; 28 | &:last-child{ 29 | margin-right: 0px; 30 | } 31 | } 32 | } 33 | } 34 | 35 | .el-table, 36 | .el-table tr, 37 | .el-table thead th{ 38 | background-color: transparent !important; 39 | } 40 | 41 | .table-shopping .el-table table { 42 | @extend .table-shopping; 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/Charts/PieChart.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 39 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "queds-finance-portfolio", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "Finance portfolio", 6 | "author": "jbatalle ", 7 | "scripts": { 8 | "predev": "rm -rf node_modules/.vite", 9 | "dev": "vite --port 8080 --host 0.0.0.0", 10 | "build": "vite build", 11 | "preview": "vite preview --port 8080 --host 0.0.0.0" 12 | }, 13 | "dependencies": { 14 | "axios": "^1.6.4", 15 | "chart.js": "^4.4.1", 16 | "element-plus": "^2.4.4", 17 | "perfect-scrollbar": "^1.5.5", 18 | "sass": "^1.69.7", 19 | "vue": "^3.4.5", 20 | "vue-chartjs": "^5.3.0", 21 | "vue-router": "^4.1.2", 22 | "vue-trading-view": "^1.0.1", 23 | "vuex": "^4.1.0", 24 | "vite": "^3.0.2" 25 | }, 26 | "devDependencies": { 27 | "@vitejs/plugin-vue": "^3.0.1", 28 | "unplugin-vue-components": "^0.26.0", 29 | "vite": "^3.0.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/backend/README.md: -------------------------------------------------------------------------------- 1 | # Backend processor 2 | Process all the petitions received via Redis queue 3 | 4 | ## Requirements 5 | Redis 6 | 7 | ## Quick Start in `Docker` 8 | > Start the app in Docker 9 | 10 | ```bash 11 | $ docker build -t queds_backend 12 | $ docker run -it queds_backend 13 | ``` 14 | 15 | ## Development 16 | 17 | > **Step #1** - Create a virtual environment using python3 18 | ```bash 19 | $ mkvirtualenv -p /usr/bin/python3.7 queds_backend 20 | $ workon queds_backend 21 | ``` 22 | 23 | > **Step #2** - Install dependencies 24 | ```bash 25 | $ pip install -r requirements.txt 26 | ``` 27 | 28 | > **Step #3** - Install redis 29 | ```bash 30 | $ docker run --name some-redis -d redis 31 | ``` 32 | 33 | > **Step #4** - Start the worker. Will listen to redis queues 34 | ``` 35 | BACKEND_SETTINGS=config.local python worker.py 36 | ``` 37 | 38 | ## Test message using client 39 | 40 | ``` 41 | BACKEND_SETTINGS=config.local python client.py 42 | ``` 43 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_social-buttons.scss: -------------------------------------------------------------------------------- 1 | // for social buttons 2 | @mixin social-buttons-color ($color, $state-color){ 3 | background-color: $color; 4 | color: $white-color; 5 | 6 | &:focus, 7 | &:active, 8 | &:hover{ 9 | background-color: $state-color !important; 10 | color: $white-color !important; 11 | } 12 | 13 | &.btn-simple{ 14 | color: $color; 15 | background-color: $transparent-bg; 16 | box-shadow: none; 17 | border-color: $color; 18 | 19 | &:hover, 20 | &:focus, 21 | &:active{ 22 | color: $state-color; 23 | border-color: $state-color; 24 | } 25 | } 26 | 27 | &.btn-neutral{ 28 | color: $color; 29 | background-color: $white-color; 30 | 31 | 32 | &:hover, 33 | &:focus, 34 | &:active{ 35 | color: $state-color !important; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/filters.js: -------------------------------------------------------------------------------- 1 | export function toCurrency(value, currency, digits = 2) { 2 | if (typeof value !== "number") { 3 | return value; 4 | } 5 | if (currency === undefined) { 6 | return Number(value).toFixed(digits); 7 | } 8 | try { 9 | let formatter = new Intl.NumberFormat('en-US', { 10 | style: 'currency', 11 | currency: currency, 12 | maximumFractionDigits: digits 13 | }); 14 | return formatter.format(value); 15 | } 16 | catch(err) { 17 | //console.log("Error formatting currency: " + currency + ": " + err); 18 | let formatter = new Intl.NumberFormat('en-US', { 19 | style: 'currency', 20 | currency: "BTC", 21 | maximumFractionDigits: digits 22 | }); 23 | return formatter.format(value).replace("BTC", currency); 24 | } 25 | } 26 | 27 | export function round(value) { 28 | if (typeof value !== "number") { 29 | return value; 30 | } 31 | return Number(value).toFixed(2); 32 | } -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_card-signup.scss: -------------------------------------------------------------------------------- 1 | .card-signup{ 2 | .header{ 3 | margin-left: 20px; 4 | margin-right: 20px; 5 | padding: 30px 0; 6 | } 7 | .text-divider{ 8 | margin-top: 30px; 9 | margin-bottom: 0px; 10 | text-align: center; 11 | } 12 | .content{ 13 | padding: 0px 30px; 14 | } 15 | 16 | .form-check{ 17 | margin-top: 20px; 18 | 19 | label{ 20 | margin-left: 7px; 21 | padding-left: 38px; 22 | } 23 | } 24 | 25 | .social-line{ 26 | margin-top: 20px; 27 | text-align: center; 28 | 29 | .btn.btn-icon , 30 | .btn.btn-icon .btn-icon{ 31 | margin-left: 5px; 32 | margin-right: 5px; 33 | box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.2); 34 | } 35 | } 36 | 37 | .card-footer{ 38 | margin-bottom: 10px; 39 | margin-top: 24px; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/models/cryptography.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | from hashlib import md5 3 | from base64 import b64decode, b64encode 4 | 5 | BLOCK_SIZE = 16 # Bytes 6 | pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE) 7 | unpad = lambda s: s[:-ord(s[len(s) - 1:])] 8 | 9 | 10 | class AESCipher: 11 | """ 12 | Usage: 13 | c = AESCipher('password').encrypt('message') 14 | m = AESCipher('password').decrypt(c) 15 | """ 16 | 17 | def __init__(self, key): 18 | self.key = md5(key.encode('utf8')).hexdigest() 19 | 20 | def encrypt(self, raw): 21 | raw = pad(raw) 22 | cipher = AES.new(self.key.encode("utf8"), AES.MODE_ECB) 23 | return b64encode(cipher.encrypt(raw.encode('utf8'))) 24 | 25 | def decrypt(self, enc): 26 | enc = b64decode(enc) 27 | cipher = AES.new(self.key.encode("utf8"), AES.MODE_ECB) 28 | return unpad(cipher.decrypt(enc)).decode('utf8') 29 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-signup.scss: -------------------------------------------------------------------------------- 1 | .card-signup{ 2 | .header{ 3 | margin-left: 20px; 4 | margin-right: 20px; 5 | padding: 30px 0; 6 | } 7 | .text-divider{ 8 | margin-top: 30px; 9 | margin-bottom: 0px; 10 | text-align: center; 11 | } 12 | .content{ 13 | padding: 0px 30px; 14 | } 15 | 16 | .form-check{ 17 | margin-top: 20px; 18 | 19 | label{ 20 | margin-left: 7px; 21 | padding-left: 38px; 22 | } 23 | } 24 | 25 | .social-line{ 26 | margin-top: 20px; 27 | text-align: center; 28 | 29 | .btn.btn-icon , 30 | .btn.btn-icon .btn-icon{ 31 | margin-left: 5px; 32 | margin-right: 5px; 33 | box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.2); 34 | } 35 | } 36 | 37 | .card-footer{ 38 | margin-bottom: 10px; 39 | margin-top: 24px; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-stats.scss: -------------------------------------------------------------------------------- 1 | %card-stats{ 2 | hr{ 3 | margin: 5px 15px; 4 | } 5 | } 6 | 7 | 8 | .card-stats{ 9 | .card-body{ 10 | padding: 15px 15px 0px; 11 | 12 | .numbers{ 13 | text-align: right; 14 | font-size: 2em; 15 | 16 | p{ 17 | margin-bottom: 0; 18 | } 19 | .card-category { 20 | color: $dark-gray; 21 | font-size: 16px; 22 | line-height: 1.4em; 23 | } 24 | } 25 | } 26 | .card-footer{ 27 | padding: 0px 15px 15px; 28 | 29 | .stats{ 30 | color: $dark-gray; 31 | } 32 | 33 | hr{ 34 | margin-top: 10px; 35 | margin-bottom: 15px; 36 | } 37 | } 38 | .icon-big { 39 | font-size: 3em; 40 | min-height: 64px; 41 | 42 | i{ 43 | line-height: 59px; 44 | } 45 | } 46 | 47 | 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/plugins/element-ui/_plugin-tags.scss: -------------------------------------------------------------------------------- 1 | @import "../../variables"; 2 | 3 | @mixin tag($type, $color){ 4 | .input-new-tag.input-#{$type} .el-input__inner{ 5 | border-color: $color !important; 6 | } 7 | .el-tag, 8 | .el-tag.el-tag--#{$type} { 9 | .el-tag__close { 10 | color: white; 11 | } 12 | .el-tag__close:hover{ 13 | background-color: white; 14 | color: $color; 15 | } 16 | background-color: $color !important; 17 | color: white; 18 | 19 | } 20 | } 21 | .el-tag{ 22 | border-radius: 12px !important; 23 | margin-left:10px; 24 | margin-bottom:5px; 25 | } 26 | .input-new-tag{ 27 | margin-left:10px; 28 | width: 150px !important; 29 | height: 32px; 30 | display: inline; 31 | 32 | } 33 | 34 | @include tag('info', $info-color); 35 | @include tag('primary', $primary-color); 36 | @include tag('success', $success-color); 37 | @include tag('warning', $warning-color); 38 | @include tag('danger', $danger-color); 39 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/Charts/BarChart.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 39 | -------------------------------------------------------------------------------- /frontend/src/mixins/form-mixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data() { 3 | return { 4 | isLoading: false, 5 | apiValidationErrors: {} 6 | }; 7 | }, 8 | methods: { 9 | /* extract API server validation errors and assigns them to local mixin data */ 10 | setApiValidation(serverErrors, refs = null) { 11 | console.log(serverErrors); 12 | this.apiValidationErrors = serverErrors.reduce( 13 | (accumulator, errorObject) => { 14 | if(typeof errorObject.source === 'undefined') 15 | return false; 16 | 17 | const errorFieldName = errorObject.source.pointer.split('/')[3]; 18 | const errorDetail = (accumulator[errorFieldName] || []).concat(errorObject.detail); 19 | 20 | return { 21 | ...accumulator, 22 | [errorFieldName]: errorDetail 23 | }; 24 | }, 25 | {} 26 | ); 27 | }, 28 | 29 | resetApiValidation() { 30 | this.apiValidationErrors = {}; 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /app/api/services/queue.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from config import settings 3 | from kombu import Connection, Exchange 4 | from kombu.pools import producers 5 | 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def enqueue_job(queue_name, data): 11 | logger.info(f"Queue process {queue_name}") 12 | connection = Connection(f'redis://{settings.REDIS.get("host")}') 13 | payload = {'args': data, 'kwargs': {}} 14 | queue = queue_name 15 | 16 | task_exchange = Exchange('tasks', type='direct') 17 | with producers[connection].acquire(block=True) as producer: 18 | producer.publish(payload, serializer='pickle', compression='bzip2', exchange=task_exchange, 19 | declare=[task_exchange], routing_key=queue) 20 | 21 | 22 | def queue_read(data, queue_name): 23 | if not queue_name: 24 | return None 25 | enqueue_job(queue_name, data) 26 | return True 27 | 28 | 29 | def queue_process(data): 30 | queue = "wallet" 31 | enqueue_job(queue, data) 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-user.scss: -------------------------------------------------------------------------------- 1 | .card-user{ 2 | .image{ 3 | height: 130px; 4 | 5 | img { 6 | border-radius: 12px; 7 | } 8 | } 9 | 10 | .author{ 11 | text-align: center; 12 | text-transform: none; 13 | margin-top: -77px; 14 | 15 | a + p.description{ 16 | margin-top: -7px; 17 | } 18 | } 19 | 20 | .avatar{ 21 | width: 124px; 22 | height: 124px; 23 | border: 1px solid $white-color; 24 | position: relative; 25 | } 26 | 27 | .card-body{ 28 | min-height: 240px; 29 | } 30 | 31 | hr{ 32 | margin: 5px 15px 15px; 33 | } 34 | 35 | .card-body + .card-footer { 36 | padding-top: 0; 37 | } 38 | 39 | .card-footer { 40 | h5 { 41 | font-size: 1.25em; 42 | margin-bottom: 0; 43 | } 44 | } 45 | 46 | .button-container{ 47 | margin-bottom: 6px; 48 | text-align: center; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-collapse.scss: -------------------------------------------------------------------------------- 1 | .card-collapse{ 2 | padding-bottom: 10px; 3 | .card{ 4 | margin-bottom: 0px; 5 | 6 | .card-header{ 7 | position: relative; 8 | padding: 20px 0; 9 | 10 | a[data-toggle="collapse"]{ 11 | display: block; 12 | color: $light-black; 13 | 14 | i{ 15 | float: right; 16 | position: relative; 17 | color: #f96332; 18 | top: 3px; 19 | right: 5px; 20 | @extend .animation-transition-general; 21 | } 22 | } 23 | 24 | &:after{ 25 | content: ""; 26 | position: absolute; 27 | bottom: 0; 28 | left: 0; 29 | width: 100%; 30 | height: 1px; 31 | background-color: $light-gray; 32 | } 33 | } 34 | 35 | .card-body{ 36 | padding: 20px 15px; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-profile.scss: -------------------------------------------------------------------------------- 1 | %card-profile{ 2 | margin-top: 30px; 3 | text-align: center; 4 | 5 | .card-body .card-title{ 6 | margin-top: 0; 7 | } 8 | 9 | [class*=col-]{ 10 | .card-description{ 11 | margin-bottom: 0; 12 | 13 | & + .card-footer{ 14 | margin-top: 8px; 15 | } 16 | } 17 | 18 | 19 | } 20 | 21 | .card-header-avatar{ 22 | max-width: 130px; 23 | max-height: 130px; 24 | margin: -60px auto 0; 25 | 26 | img{ 27 | border-radius: 50% !important; 28 | } 29 | 30 | & + .card-body{ 31 | margin-top: 15px; 32 | } 33 | } 34 | 35 | &.card-plain{ 36 | .card-header-avatar{ 37 | margin-top: 0; 38 | } 39 | } 40 | .card-body{ 41 | .card-avatar{ 42 | margin: 0 auto 30px; 43 | } 44 | } 45 | } 46 | 47 | .card-profile{ 48 | @extend %card-profile; 49 | } 50 | -------------------------------------------------------------------------------- /app/api/services/redis.py: -------------------------------------------------------------------------------- 1 | import json 2 | import redis 3 | from config import settings 4 | 5 | 6 | class RedisClient: 7 | def __init__(self, config=None): 8 | self._config = config or settings.REDIS 9 | self.pool = None 10 | self.client = None # type: redis.StrictRedis 11 | self.default_expiration = 600 12 | self.connect() 13 | 14 | def connect(self): 15 | if self._config: 16 | self.client = redis.StrictRedis(**self._config) 17 | self.client.ping() 18 | 19 | def store(self, key, data, expiration=None): 20 | json_data = json.dumps(data) 21 | expiration = expiration or self.default_expiration 22 | self.client.set(key, json_data, expiration) 23 | 24 | def get(self, key): 25 | json_data = self.client.get(key) 26 | 27 | if json_data: 28 | return json.loads(json_data.decode()) 29 | 30 | return None 31 | 32 | def exists(self, key): 33 | return self.client.exists(key) 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/models/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | script_location = models/migrations 11 | 12 | 13 | # Logging configuration 14 | [loggers] 15 | keys = root,sqlalchemy,alembic 16 | 17 | [handlers] 18 | keys = console 19 | 20 | [formatters] 21 | keys = generic 22 | 23 | [logger_root] 24 | level = WARN 25 | handlers = console 26 | qualname = 27 | 28 | [logger_sqlalchemy] 29 | level = WARN 30 | handlers = 31 | qualname = sqlalchemy.engine 32 | 33 | [logger_alembic] 34 | level = INFO 35 | handlers = 36 | qualname = alembic 37 | 38 | [handler_console] 39 | class = StreamHandler 40 | args = (sys.stderr,) 41 | level = NOTSET 42 | formatter = generic 43 | 44 | [formatter_generic] 45 | format = %(levelname)-5.5s [%(name)s] %(message)s 46 | datefmt = %H:%M:%S 47 | -------------------------------------------------------------------------------- /nginx_template.conf: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 80; 4 | server_name localhost; 5 | 6 | location / { 7 | proxy_pass http://frontend:8080; 8 | proxy_http_version 1.1; 9 | proxy_redirect default; 10 | proxy_set_header Upgrade $http_upgrade; 11 | proxy_set_header Connection "upgrade"; 12 | proxy_set_header Host $host; 13 | proxy_set_header X-Real-IP $remote_addr; 14 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 15 | proxy_set_header X-Forwarded-Host $server_name; 16 | } 17 | 18 | # location /api/ { 19 | location ~ (api|swaggerui) { 20 | proxy_pass http://api:5000; 21 | proxy_http_version 1.1; 22 | proxy_redirect default; 23 | proxy_set_header Upgrade $http_upgrade; 24 | proxy_set_header Connection "upgrade"; 25 | proxy_set_header Host $host; 26 | proxy_set_header X-Real-IP $remote_addr; 27 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 28 | proxy_set_header X-Forwarded-Host $server_name; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-stats-mini.scss: -------------------------------------------------------------------------------- 1 | .card-stats-mini { 2 | &.card-background::after { 3 | background-image: linear-gradient(to right, #434343 0%, black 100%); 4 | opacity: .94; 5 | } 6 | 7 | .card-body { 8 | &::after { 9 | clear: both; 10 | content: ''; 11 | display: block; 12 | } 13 | } 14 | 15 | .card-footer { 16 | border-top: 1px solid $opacity-2; 17 | margin: 0 15px; 18 | } 19 | 20 | &.card-background { 21 | .card-body { 22 | min-height: auto; 23 | padding-top: 15px; 24 | padding-bottom: 15px; 25 | } 26 | } 27 | 28 | .card-title { 29 | margin-top: 0; 30 | margin-bottom: 5px; 31 | } 32 | 33 | .info-area { 34 | text-align: left; 35 | width: 40%; 36 | float: left; 37 | padding: 15px 0; 38 | 39 | .icon > i { 40 | font-size: 2em; 41 | } 42 | } 43 | 44 | .chart-area { 45 | float: left; 46 | width: 60%; 47 | } 48 | 49 | .stats { 50 | text-align: left; 51 | color: $white-color; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_footers.scss: -------------------------------------------------------------------------------- 1 | .footer{ 2 | padding: 4px 0; 3 | 4 | &.footer-default{ 5 | background-color: #f2f2f2; 6 | } 7 | 8 | nav{ 9 | display: inline-block; 10 | float: left; 11 | padding-left: 0; 12 | } 13 | 14 | ul{ 15 | margin-bottom: 0; 16 | padding: 0; 17 | list-style: none; 18 | 19 | li{ 20 | display: inline-block; 21 | 22 | a{ 23 | color: inherit; 24 | padding: $padding-base-vertical; 25 | font-size: $font-size-small; 26 | text-transform: uppercase; 27 | text-decoration: none; 28 | 29 | &:hover{ 30 | text-decoration: none; 31 | } 32 | } 33 | } 34 | } 35 | 36 | .copyright{ 37 | font-size: $font-size-small; 38 | line-height: 1.8; 39 | } 40 | 41 | &:after{ 42 | display: table; 43 | clear: both; 44 | content: " "; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/models/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | file_template = %%(rev)s%%(slug)s_%%(year)d-%%(month).2d-%%(day).2d-%%(hour).2d-%%(minute).2d 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | script_location = migrations 12 | 13 | # Logging configuration 14 | [loggers] 15 | keys = root,sqlalchemy,alembic 16 | 17 | [handlers] 18 | keys = console 19 | 20 | [formatters] 21 | keys = generic 22 | 23 | [logger_root] 24 | level = WARN 25 | handlers = console 26 | qualname = 27 | 28 | [logger_sqlalchemy] 29 | level = WARN 30 | handlers = 31 | qualname = sqlalchemy.engine 32 | 33 | [logger_alembic] 34 | level = INFO 35 | handlers = 36 | qualname = alembic 37 | 38 | [handler_console] 39 | class = StreamHandler 40 | args = (sys.stderr,) 41 | level = NOTSET 42 | formatter = generic 43 | 44 | [formatter_generic] 45 | format = %(levelname)-5.5s [%(name)s] %(message)s 46 | datefmt = %H:%M:%S 47 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_wizard.scss: -------------------------------------------------------------------------------- 1 | @mixin set-wizard-color($color) { 2 | .moving-tab{ 3 | color: $color; 4 | } 5 | 6 | .picture{ 7 | &:hover{ 8 | border-color: $color; 9 | } 10 | } 11 | 12 | .choice{ 13 | &:hover, 14 | &.active{ 15 | .icon{ 16 | border-color: $color; 17 | color: $color; 18 | } 19 | } 20 | } 21 | 22 | // .form-group{ 23 | // .form-control{ 24 | // background-image: linear-gradient($color, $color), linear-gradient($mdb-input-underline-color, $mdb-input-underline-color); 25 | // } 26 | // } 27 | 28 | .checkbox input[type=checkbox]:checked + .checkbox-material{ 29 | .check{ 30 | background-color: $color; 31 | } 32 | } 33 | 34 | .radio input[type=radio]:checked ~ .check { 35 | background-color: $color; 36 | } 37 | 38 | .radio input[type=radio]:checked ~ .circle { 39 | border-color: $color; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/store/modules/reset.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import store from "@/store"; 3 | 4 | const url = "http://localhost:6060/api/users"; 5 | //const url = process.env.VUE_APP_API_BASE_URL; 6 | 7 | const actions = { 8 | async forgotPassword({ commit }, data) { 9 | await axios.post(`${url}/password-forgot`, { data }); 10 | }, 11 | async createNewPassword({ commit }, data) { 12 | await axios.post(`${url}/password-reset`, { data }); 13 | 14 | const user = { 15 | data: { 16 | type: "token", 17 | attributes: { 18 | email: data.attributes.email, 19 | password: data.attributes.password 20 | } 21 | } 22 | }; 23 | 24 | const requestOptions = { 25 | headers: { 26 | Accept: "application/vnd.api+json", 27 | "Content-Type": "application/vnd.api+json" 28 | } 29 | }; 30 | 31 | store.dispatch("login", { user, requestOptions }, { root: true }); 32 | } 33 | }; 34 | 35 | const reset = { 36 | namespaced: true, 37 | actions 38 | }; 39 | 40 | export default reset; 41 | -------------------------------------------------------------------------------- /app/backend/finance_reader/entities/exchanges/__init__.py: -------------------------------------------------------------------------------- 1 | from finance_reader.entities import AbstractEntity 2 | import time 3 | 4 | 5 | class AbstractExchange(AbstractEntity): 6 | 7 | def __init__(self): 8 | super(AbstractExchange, self).__init__() 9 | 10 | @staticmethod 11 | def nonce(): 12 | """ 13 | Creates a Nonce value for signature generation 14 | :return: 15 | """ 16 | return str(round(100000 * time.time()) * 2) 17 | 18 | 19 | from finance_reader.entities.exchanges.bitstamp import Bitstamp 20 | from finance_reader.entities.exchanges.kraken import Kraken 21 | from finance_reader.entities.exchanges.bittrex import Bittrex 22 | from finance_reader.entities.exchanges.binance import Binance 23 | # from finance_reader.entities.exchanges.kucoin import Kucoin 24 | # from finance_reader.entities.exchanges.bitfinex import Bitfinex 25 | 26 | 27 | SUPPORTED_EXCHANGES = { 28 | "bitstamp": Bitstamp(), 29 | "kraken": Kraken(), 30 | "bittrex": Bittrex(), 31 | "binance": Binance(), 32 | # "kucoin": Kucoin(), 33 | # "bitfinex": Bitfinex() 34 | } 35 | -------------------------------------------------------------------------------- /app/backend/wallet_processor/entities/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABC 3 | 4 | 5 | class EntityType: 6 | BANK = 0 7 | BROKER = 1 8 | CROWD = 2 9 | EXCHANGE = 3 10 | 11 | 12 | class AbstractEntity(ABC): 13 | 14 | source_type = None 15 | 16 | def __init__(self): 17 | self._state = None 18 | self._logger = None # type: logging.Logger 19 | self._init_logger() 20 | 21 | def trade(self, queue, order): 22 | raise NotImplementedError 23 | 24 | def preprocess(self, orders): 25 | pass 26 | 27 | def clean(self): 28 | pass 29 | 30 | def get_accounts(self, user_id): 31 | pass 32 | 33 | def get_orders(self, accounts): 34 | pass 35 | 36 | def get_transactions(self, accounts): 37 | pass 38 | 39 | def create_closed_orders(self): 40 | pass 41 | 42 | def calc_wallet(self): 43 | pass 44 | 45 | def calc_balance_with_orders(self): 46 | pass 47 | 48 | def _init_logger(self): 49 | tmp_logger_name = type(self).__name__ 50 | self._logger = logging.getLogger(tmp_logger_name) 51 | -------------------------------------------------------------------------------- /app/api/README.md: -------------------------------------------------------------------------------- 1 | # Flask API Server 2 | 3 | ## Quick Start in `Docker` 4 | > Start the app in Docker 5 | 6 | ```bash 7 | $ docker build -t queds_api 8 | $ docker run -it queds_api 9 | ``` 10 | 11 | The API server will start using the PORT `5000` 12 | 13 | ## Development 14 | 15 | > **Step #1** - Create a virtual environment using python3 16 | 17 | ```bash 18 | $ mkvirtualenv -p /usr/bin/python3.7 queds_api 19 | $ workon queds_api 20 | ``` 21 | 22 | > **Step #2** - Install dependencies 23 | 24 | ```bash 25 | $ pip install -r requirements.txt 26 | ``` 27 | 28 | > **Step #3** - Start the server at `localhost:5000` 29 | 30 | Check configuration parameters in `../config/` folder. 31 | 32 | > **Step #4** - Start the server at `localhost:5000` 33 | 34 | ```bash 35 | $ WEB_SETTINGS=config/local.py python app.py 36 | ``` 37 | 38 | > **Step #5** - Check Swagger Dashboard at http://localhost:5000/api/ 39 | 40 | ## Testing 41 | 42 | Using pytest: 43 | ``` 44 | pytest tests.py 45 | ``` 46 | 47 | Or unittests 48 | ``` 49 | BACKEND_SETTINGS=config.local python -m unittest 50 | ``` 51 | 52 | ## Pycharm debug 53 | Disable `Gevent compatible` in Debugger settings -------------------------------------------------------------------------------- /app/backend/finance_reader/entities/__init__.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import logging 3 | from abc import ABC, abstractmethod 4 | 5 | DEFAULT_REQUEST_TIMEOUT = 60 6 | 7 | 8 | class TimeoutRequestsSession(requests.Session): 9 | def request(self, *args, **kwargs): 10 | if kwargs.get('timeout') is None: 11 | kwargs['timeout'] = DEFAULT_REQUEST_TIMEOUT 12 | return super(TimeoutRequestsSession, self).request(*args, **kwargs) 13 | 14 | 15 | class EntityType: 16 | BANK = 0 17 | BROKER = 1 18 | CROWD = 2 19 | EXCHANGE = 3 20 | 21 | 22 | class AbstractEntity(ABC): 23 | 24 | source_type = None 25 | 26 | def __init__(self): 27 | self._state = None 28 | self._client = TimeoutRequestsSession() 29 | self._logger = None # type: logging.Logger 30 | self._init_logger() 31 | 32 | @abstractmethod 33 | def login(self, parameters: dict): 34 | raise NotImplementedError 35 | 36 | def logout(self): 37 | pass 38 | 39 | def read(self): 40 | pass 41 | 42 | def _init_logger(self): 43 | tmp_logger_name = type(self).__name__ 44 | self._logger = logging.getLogger(tmp_logger_name) 45 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_sidebar.scss: -------------------------------------------------------------------------------- 1 | @mixin sidebar-background-color($background-color, $font-color) { 2 | &:after, 3 | &:before { 4 | background-color: $background-color; 5 | } 6 | 7 | #style-3::-webkit-scrollbar-track 8 | { 9 | -webkit-box-shadow: inset 0 0 6px $background-color; 10 | background-color: $background-color; 11 | } 12 | 13 | #style-3::-webkit-scrollbar 14 | { 15 | width: 6px; 16 | background-color: $font-color; 17 | } 18 | 19 | #style-3::-webkit-scrollbar-thumb 20 | { 21 | background-color: $background-color; 22 | } 23 | 24 | 25 | .logo { 26 | border-bottom: 1px solid rgba($font-color, .3); 27 | 28 | p { 29 | color: $font-color; 30 | } 31 | 32 | .simple-text { 33 | color: $font-color; 34 | } 35 | } 36 | 37 | .nav { 38 | .nav-item:not(.active) { 39 | > .nav-link { 40 | color: $font-color; 41 | } 42 | } 43 | .divider { 44 | background-color: rgba($font-color, .2); 45 | } 46 | 47 | } 48 | 49 | } 50 | 51 | @mixin sidebar-active-color($font-color) { 52 | .nav { 53 | .nav-item { 54 | &.active > .nav-link { 55 | color: $font-color; 56 | opacity: 1; 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /frontend/src/components/Dashboard/Layout/AppNavbar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 35 | 47 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 46 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_misc-extend.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .ct-label.ct-horizontal.ct-end, 4 | .ct-label.ct-vertical.ct-start { 5 | font-size: 14px; 6 | } 7 | 8 | .ct-chart.ct-perfect-fourth { 9 | .ct-chart-pie .ct-label { 10 | font-size: 14px; 11 | } 12 | } 13 | 14 | .card { 15 | .card-footer .footer-line { 16 | padding-top: 3px; 17 | } 18 | 19 | &.bootstrap-table { 20 | .dropdown-item.active { 21 | background-color: $default-color; 22 | color: $white-color; 23 | } 24 | } 25 | 26 | .team-members { 27 | .avatar { 28 | margin-top: 5px; 29 | } 30 | .text-right .btn{ 31 | margin-top: 5px; 32 | } 33 | } 34 | } 35 | 36 | .btn-group-sm { 37 | .btn-round { 38 | border-radius: 30px; 39 | } 40 | } 41 | 42 | // Pulsing Heart (footer) 43 | .heart { 44 | color: #EB5E28; 45 | animation: hearthing 1s ease infinite,; 46 | } 47 | 48 | @keyframes hearthing { 49 | 0% { transform: scale( .75 ); } 50 | 20% { transform: scale( 1 ); } 51 | 40% { transform: scale( .75 ); } 52 | 60% { transform: scale( 1 ); } 53 | 80% { transform: scale( .75 ); } 54 | 100% { transform: scale( .75 ); } 55 | } 56 | 57 | 58 | // Datetimepicker 59 | 60 | .datepicker { 61 | .table-condensed { 62 | tbody > tr:first-of-type { 63 | .day { 64 | padding-top: 5px; 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /frontend/src/components/Dashboard/Layout/TopNavbar.vue: -------------------------------------------------------------------------------- 1 | 21 | 44 | 57 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_social-buttons.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .btn{ 4 | // social buttons 5 | &.btn-facebook { 6 | @include social-buttons-color($social-facebook, $social-facebook-state-color); 7 | } 8 | &.btn-twitter { 9 | @include social-buttons-color($social-twitter, $social-twitter-state-color); 10 | } 11 | &.btn-pinterest { 12 | @include social-buttons-color($social-pinterest, $social-pinterest-state-color); 13 | } 14 | &.btn-google { 15 | @include social-buttons-color($social-google, $social-google-state-color); 16 | } 17 | &.btn-linkedin { 18 | @include social-buttons-color($social-linkedin, $social-linkedin-state-color); 19 | } 20 | &.btn-dribbble { 21 | @include social-buttons-color($social-dribbble, $social-dribbble-state-color); 22 | } 23 | &.btn-github { 24 | @include social-buttons-color($social-github, $social-github-state-color); 25 | } 26 | &.btn-youtube { 27 | @include social-buttons-color($social-youtube, $social-youtube-state-color); 28 | } 29 | &.btn-instagram { 30 | @include social-buttons-color($social-instagram, $social-instagram-state-color); 31 | } 32 | &.btn-reddit { 33 | @include social-buttons-color($social-reddit, $social-reddit-state-color); 34 | } 35 | &.btn-tumblr { 36 | @include social-buttons-color($social-tumblr, $social-tumblr-state-color); 37 | } 38 | &.btn-behance { 39 | @include social-buttons-color($social-behance, $social-behance-state-color); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-background.scss: -------------------------------------------------------------------------------- 1 | .card-background{ 2 | background-position: center center; 3 | background-size: cover; 4 | text-align: center; 5 | 6 | .card-body{ 7 | position: relative; 8 | z-index: 2; 9 | min-height: 370px; 10 | max-width: 530px; 11 | margin: 0 auto; 12 | padding-top: 60px; 13 | padding-bottom: 60px;; 14 | } 15 | .card-footer{ 16 | position: relative; 17 | z-index: 2; 18 | } 19 | 20 | &.card-background-product .card-body{ 21 | max-width: 400px; 22 | .card-title{ 23 | margin-top: 30px; 24 | } 25 | } 26 | 27 | .stats{ 28 | color: $white-color; 29 | } 30 | 31 | .card-footer{ 32 | .stats-link > a{ 33 | color: $white-color; 34 | line-height: 1.9; 35 | } 36 | } 37 | 38 | .category, 39 | .card-description, 40 | small{ 41 | color: $opacity-8; 42 | } 43 | 44 | .card-title { 45 | color: $white-color; 46 | margin-top: 130px; 47 | } 48 | 49 | &:not(.card-pricing) .btn { 50 | margin-bottom: 0; 51 | } 52 | 53 | &::after { 54 | position: absolute; 55 | z-index: 1; 56 | width: 100%; 57 | height: 100%; 58 | display: block; 59 | left: 0; 60 | top: 0; 61 | content: ""; 62 | background-color: rgba(0,0,0,.63); 63 | border-radius: $border-radius-large; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/SidebarPlugin/index.js: -------------------------------------------------------------------------------- 1 | import Sidebar from './SideBar.vue'; 2 | import SidebarItem from './SidebarItem.vue'; 3 | 4 | const SidebarStore = { 5 | showSidebar: false, 6 | sidebarLinks: [], 7 | isMinimized: false, 8 | displaySidebar (value) { 9 | this.showSidebar = value; 10 | }, 11 | toggleMinimize () { 12 | document.body.classList.toggle('sidebar-mini') 13 | // we simulate the window Resize so the charts will get updated in realtime. 14 | const simulateWindowResize = setInterval(() => { 15 | window.dispatchEvent(new Event('resize')) 16 | }, 180) 17 | 18 | // we stop the simulation of Window Resize after the animations are completed 19 | setTimeout(() => { 20 | clearInterval(simulateWindowResize) 21 | }, 1000) 22 | 23 | this.isMinimized = !this.isMinimized; 24 | } 25 | } 26 | 27 | const SidebarPlugin = { 28 | 29 | install (Vue, options) { 30 | if (options && options.sidebarLinks) { 31 | SidebarStore.sidebarLinks = options.sidebarLinks 32 | } 33 | Vue.mixin({ 34 | data () { 35 | return { 36 | sidebarStore: SidebarStore 37 | } 38 | } 39 | }) 40 | 41 | Vue.prototype.$sidebar = SidebarStore 42 | Object.defineProperty(Vue.prototype, '$sidebar', { 43 | get () { 44 | return this.$root.sidebarStore 45 | } 46 | }) 47 | Vue.component('side-bar', Sidebar) 48 | Vue.component('sidebar-item', SidebarItem) 49 | } 50 | } 51 | 52 | export default SidebarPlugin 53 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/SidebarPlugin/SidebarLink.vue: -------------------------------------------------------------------------------- 1 | 19 | 68 | 70 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/Cards/StatsCard.vue: -------------------------------------------------------------------------------- 1 | 30 | 54 | 59 | -------------------------------------------------------------------------------- /app/api/api/tests/test_credential.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from app import app, db 3 | from mock import patch, create_autospec 4 | from models.system import User 5 | 6 | 7 | class BaseTestClass(unittest.TestCase): 8 | def setUp(self): 9 | self.app = app#create_app(settings_module="config.testing") 10 | self.client = self.app.test_client() 11 | # Crea un contexto de aplicación 12 | with self.app.app_context(): 13 | # Crea las tablas de la base de datos 14 | # db.create_all() 15 | print("Create all") 16 | 17 | def tearDown(self): 18 | with self.app.app_context(): 19 | # Elimina todas las tablas de la base de datos 20 | #db.session.remove() 21 | #db.drop_all() 22 | print("Delete all") 23 | 24 | 25 | class CredentialTestCase(BaseTestClass): 26 | 27 | # @patch('models.system.User.check_password') 28 | @patch('models.system.User.get_by_email') 29 | def test_create_credential(self, mock): 30 | data = { 31 | "parameters": [ 32 | { 33 | "value": "value1", 34 | "credential_type_id": 0 35 | } 36 | ], 37 | "encrypt_password": "password" 38 | } 39 | mock.return_value = User(email="mail", password="blabla") 40 | res = self.client.post('/api/entities/accounts/1/credentials', json=data) 41 | response = res.get_json() 42 | 43 | self.assertEqual(200, res.status_code) 44 | self.assertIn('token', response) 45 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_alerts.scss: -------------------------------------------------------------------------------- 1 | .alert{ 2 | border: 0; 3 | border-radius: $border-radius-small; 4 | color: $white-color; 5 | padding-top: .9rem; 6 | padding-bottom: .9rem; 7 | position: relative; 8 | 9 | &.alert-success{ 10 | background-color: lighten($success-color, 5%); 11 | } 12 | 13 | &.alert-danger{ 14 | background-color: lighten($danger-color, 5%); 15 | } 16 | 17 | &.alert-warning{ 18 | background-color: lighten($warning-color, 5%); 19 | } 20 | 21 | &.alert-info{ 22 | background-color: lighten($info-color, 5%); 23 | } 24 | 25 | &.alert-primary{ 26 | background-color: lighten($primary-color, 5%); 27 | } 28 | 29 | .close{ 30 | color: $white-color; 31 | opacity: .9; 32 | text-shadow: none; 33 | line-height: 0; 34 | outline: 0; 35 | 36 | i.fa, 37 | i.nc-icon{ 38 | font-size: 14px !important; 39 | } 40 | 41 | &:hover, 42 | &:focus { 43 | opacity: 1; 44 | } 45 | } 46 | 47 | span[data-notify="icon"]{ 48 | font-size: 27px; 49 | display: block; 50 | left: 19px; 51 | position: absolute; 52 | top: 50%; 53 | margin-top: -11px; 54 | } 55 | 56 | button.close{ 57 | position: absolute; 58 | right: 10px; 59 | top: 50%; 60 | margin-top: -13px; 61 | width: 25px; 62 | height: 25px; 63 | padding: 3px; 64 | } 65 | 66 | .close ~ span{ 67 | display: block; 68 | max-width: 89%; 69 | } 70 | 71 | &.alert-with-icon{ 72 | padding-left: 65px; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Queds - Finance portfolio 10 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/backend/finance_reader/entities/csv_reader/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import logging 3 | 4 | 5 | class CSVProcessor(ABC): 6 | 7 | def __init__(self): 8 | self.log = logging.getLogger(self.__class__.__name__) 9 | 10 | @abstractmethod 11 | def process_csv(self, csv_data): 12 | raise NotImplementedError 13 | 14 | def insert_db(self, orders, transactions): 15 | raise NotImplementedError 16 | 17 | 18 | class BrokerCSVProcessor(CSVProcessor): 19 | 20 | def __init__(self): 21 | super(BrokerCSVProcessor, self).__init__() 22 | 23 | def insert_db(self, orders, transactions): 24 | from models.broker import StockTransaction 25 | StockTransaction.bulk_insert(orders) 26 | 27 | 28 | class ExchangeCSVProcessor(CSVProcessor): 29 | 30 | def __init__(self): 31 | super(ExchangeCSVProcessor, self).__init__() 32 | 33 | def insert_db(self, orders, transactions): 34 | from models.crypto import CryptoEvent 35 | CryptoEvent.bulk_insert([o.to_dict() for o in orders + transactions]) 36 | 37 | 38 | from finance_reader.entities.csv_reader.bitstamp import Bitstamp 39 | from finance_reader.entities.csv_reader.bittrex import Bittrex 40 | from finance_reader.entities.csv_reader.degiro import Degiro 41 | from finance_reader.entities.csv_reader.kucoin import Kucoin 42 | from finance_reader.entities.csv_reader.coinbase import Coinbase 43 | from finance_reader.entities.csv_reader.binance import Binance 44 | 45 | 46 | SUPPORTED_CSV_READER = { 47 | "bitstamp": Bitstamp(), 48 | "bittrex": Bittrex(), 49 | "degiro": Degiro(), 50 | "kucoin": Kucoin(), 51 | "coinbase": Coinbase(), 52 | "binance": Binance() 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-testimonials.scss: -------------------------------------------------------------------------------- 1 | .card-testimonial{ 2 | margin-top: 30px; 3 | text-align: center; 4 | 5 | .icon{ 6 | padding: 0; 7 | 8 | i{ 9 | font-size: 30px; 10 | border: 0; 11 | display: block; 12 | line-height: 100px; 13 | margin: 0px auto; 14 | margin-bottom: 0px; 15 | } 16 | } 17 | .card-body{ 18 | padding: 15px 30px; 19 | 20 | .card-description{ 21 | font-style: italic; 22 | font-size: 16px; 23 | } 24 | .card-category{ 25 | margin-bottom: 20px; 26 | } 27 | 28 | +.card-footer { 29 | padding-top: 0; 30 | margin-top: -20px; 31 | } 32 | } 33 | 34 | .card-avatar{ 35 | margin-top: 0; 36 | .img{ 37 | border-radius: 50%; 38 | width: 100px; 39 | height: 100px; 40 | } 41 | } 42 | 43 | .card-footer{ 44 | .card-title{ 45 | color: $black-color; 46 | text-align: center; 47 | } 48 | .card-category{ 49 | color: $dark-gray; 50 | } 51 | .card-avatar{ 52 | margin-top: 20px; 53 | .img{ 54 | border-radius: 50%; 55 | width: 60px; 56 | height: 60px; 57 | } 58 | } 59 | } 60 | } 61 | 62 | .card-testimonial .card-description + .card-title{ 63 | margin-top: 30px; 64 | 65 | & .card-image{ 66 | .img{ 67 | border-radius: $border-radius-extreme; 68 | } 69 | 70 | .card-title{ 71 | text-align: center; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /frontend/src/assets/custom.css: -------------------------------------------------------------------------------- 1 | 2 | .red { 3 | color: red 4 | } 5 | 6 | .green { 7 | color: green 8 | } 9 | .blue { 10 | color: blue 11 | } 12 | 13 | .login-page .full-page > .content { 14 | padding-top: 8vh; 15 | } 16 | 17 | .gap-2 { 18 | grid-gap: 0.5rem; 19 | gap: 0.5rem; 20 | } 21 | 22 | /*.badge-default {*/ 23 | /* color: #fff;*/ 24 | /*}*/ 25 | /*.badge {*/ 26 | /* text-transform: uppercase;*/ 27 | /* display: inline-block;*/ 28 | /* padding: .35rem .375rem;*/ 29 | /* font-size: 66%;*/ 30 | /* font-weight: 600;*/ 31 | /* line-height: 1;*/ 32 | /* text-align: center;*/ 33 | /* white-space: nowrap;*/ 34 | /* vertical-align: baseline;*/ 35 | /* border-radius: .375rem;*/ 36 | /* transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;*/ 37 | /*}*/ 38 | 39 | /*.ck-editor__editable {*/ 40 | /* min-height: 200px;*/ 41 | /* width: auto;*/ 42 | /*}*/ 43 | .sidebar .nav .btn-danger,.sidebar .nav .btn-dark,.sidebar .nav .btn-info,.sidebar .nav .btn-neutral { 44 | margin-bottom: .5rem; 45 | margin-left: .5rem; 46 | margin-right: .5rem; 47 | } 48 | .sidebar .nav .btn-danger a, .sidebar .nav .btn-info a, .sidebar .nav .btn-dark a, .sidebar .nav .btn-neutral a{ 49 | padding: 0px; 50 | margin: 0px; 51 | opacity: unset; 52 | } 53 | .sidebar .nav .btn-neutral a, .sidebar .nav .btn-neutral i{ 54 | color: black; 55 | } 56 | 57 | .sidebar .nav .example > a, .sidebar .nav .example > a i { 58 | color: #3cab79 !important; 59 | opacity: 1; 60 | } 61 | 62 | .login-page .footer-link { 63 | color: #66615b; 64 | } 65 | 66 | .reset-container { 67 | min-height: 70vh; 68 | } 69 | 70 | .wrapper .sidebar:before, .wrapper .sidebar:after { 71 | display: block; 72 | content: ""; 73 | opacity: 1; 74 | position: absolute; 75 | width: 100%; 76 | height: 100%; 77 | top: 0; 78 | left: 0; 79 | } -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/plugins/_plugin-nprogress.scss: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | @import "../variables"; 3 | #nprogress { 4 | pointer-events: none; 5 | } 6 | 7 | #nprogress .bar { 8 | background: $primary-color; 9 | 10 | position: fixed; 11 | z-index: 1031; 12 | top: 0; 13 | left: 0; 14 | 15 | width: 100%; 16 | height: 2px; 17 | } 18 | 19 | /* Fancy blur effect */ 20 | #nprogress .peg { 21 | display: block; 22 | position: absolute; 23 | right: 0px; 24 | width: 100px; 25 | height: 100%; 26 | box-shadow: 0 0 10px $primary-color, 0 0 5px $primary-color; 27 | opacity: 1.0; 28 | 29 | -webkit-transform: rotate(3deg) translate(0px, -4px); 30 | -ms-transform: rotate(3deg) translate(0px, -4px); 31 | transform: rotate(3deg) translate(0px, -4px); 32 | } 33 | 34 | /* Remove these to get rid of the spinner */ 35 | #nprogress .spinner { 36 | display: block; 37 | position: fixed; 38 | z-index: 1031; 39 | top: 15px; 40 | right: 15px; 41 | } 42 | 43 | #nprogress .spinner-icon { 44 | width: 18px; 45 | height: 18px; 46 | box-sizing: border-box; 47 | 48 | border: solid 2px transparent; 49 | border-top-color: $primary-color; 50 | border-left-color: $primary-color; 51 | border-radius: 50%; 52 | 53 | -webkit-animation: nprogress-spinner 400ms linear infinite; 54 | animation: nprogress-spinner 400ms linear infinite; 55 | } 56 | 57 | .nprogress-custom-parent { 58 | overflow: hidden; 59 | position: relative; 60 | } 61 | 62 | .nprogress-custom-parent #nprogress .spinner, 63 | .nprogress-custom-parent #nprogress .bar { 64 | position: absolute; 65 | } 66 | 67 | @-webkit-keyframes nprogress-spinner { 68 | 0% { -webkit-transform: rotate(0deg); } 69 | 100% { -webkit-transform: rotate(360deg); } 70 | } 71 | @keyframes nprogress-spinner { 72 | 0% { transform: rotate(0deg); } 73 | 100% { transform: rotate(360deg); } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/Charts/plugins/plugin-chart-text.js: -------------------------------------------------------------------------------- 1 | export default { 2 | id: 'text-plugin', 3 | beforeDraw: function(chart) { 4 | if (chart.config.options.elements.center) { 5 | //Get ctx from string 6 | let ctx = chart.chart.ctx; 7 | 8 | //Get options from the center object in options 9 | let centerConfig = chart.config.options.elements.center; 10 | let fontStyle = centerConfig.fontStyle || 'Arial'; 11 | let txt = centerConfig.text; 12 | let color = centerConfig.color || '#000'; 13 | let sidePadding = centerConfig.sidePadding || 20; 14 | let sidePaddingCalculated = (sidePadding / 100) * (chart.innerRadius * 2) 15 | //Start with a base font of 30px 16 | ctx.font = "30px " + fontStyle; 17 | 18 | //Get the width of the string and also the width of the element minus 10 to give it 5px side padding 19 | let stringWidth = ctx.measureText(txt).width; 20 | let elementWidth = (chart.innerRadius * 2) - sidePaddingCalculated; 21 | 22 | // Find out how much the font can grow in width. 23 | let widthRatio = elementWidth / stringWidth; 24 | let newFontSize = Math.floor(30 * widthRatio); 25 | let elementHeight = (chart.innerRadius * 2); 26 | 27 | // Pick a new font size so it will not be larger than the height of label. 28 | let fontSizeToUse = Math.min(newFontSize, elementHeight); 29 | 30 | //Set font settings to draw it correctly. 31 | ctx.textAlign = 'center'; 32 | ctx.textBaseline = 'middle'; 33 | let centerX = ((chart.chartArea.left + chart.chartArea.right) / 2); 34 | let centerY = ((chart.chartArea.top + chart.chartArea.bottom) / 2); 35 | ctx.font = fontSizeToUse + "px " + fontStyle; 36 | ctx.fillStyle = color; 37 | 38 | //Draw text in center 39 | ctx.fillText(txt, centerX, centerY); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_badges.scss: -------------------------------------------------------------------------------- 1 | /* badges */ 2 | .p-badge{ 3 | padding: 4px 8px; 4 | text-transform: uppercase; 5 | font-size: $font-size-mini; 6 | line-height: 12px; 7 | background-color: $transparent-bg; 8 | border: $border; 9 | text-decoration: none; 10 | color: $white-color; 11 | margin-bottom: 5px; 12 | 13 | &:hover, 14 | &:focus{ 15 | text-decoration: none; 16 | } 17 | } 18 | .badge-icon{ 19 | padding: 0.4em 0.55em; 20 | i{ 21 | font-size: 0.8em; 22 | } 23 | } 24 | .badge-default{ 25 | @include badge-color($default-color); 26 | } 27 | .badge-primary{ 28 | @include badge-color($primary-color); 29 | } 30 | .badge-info{ 31 | @include badge-color($info-color); 32 | } 33 | .badge-success{ 34 | @include badge-color($success-color); 35 | } 36 | .badge-warning{ 37 | @include badge-color($warning-color); 38 | } 39 | .badge-danger{ 40 | @include badge-color($danger-color); 41 | } 42 | .badge-neutral{ 43 | @include badge-color($white-color); 44 | color: inherit; 45 | } 46 | 47 | .badge-primary[href]:focus, 48 | .badge-primary[href]:hover{ 49 | @include badge-hover-href($white-color, darken($primary-color, 3%)); 50 | } 51 | 52 | .badge-warning[href]:focus, 53 | .badge-warning[href]:hover{ 54 | @include badge-hover-href($white-color, darken($warning-color, 3%)); 55 | } 56 | 57 | .badge-info[href]:focus, 58 | .badge-info[href]:hover{ 59 | @include badge-hover-href($white-color, darken($info-color, 3%)); 60 | } 61 | 62 | .badge-danger[href]:focus, 63 | .badge-danger[href]:hover{ 64 | @include badge-hover-href($white-color, darken($danger-color, 3%)); 65 | } 66 | 67 | .badge-success[href]:focus, 68 | .badge-success[href]:hover{ 69 | @include badge-hover-href($white-color, darken($success-color, 3%)); 70 | } 71 | 72 | .badge-default[href]:focus, 73 | .badge-default[href]:hover{ 74 | @include badge-hover-href($white-color, darken($default-color, 3%)); 75 | } 76 | -------------------------------------------------------------------------------- /frontend/src/components/Dashboard/Layout/NotFoundPage.vue: -------------------------------------------------------------------------------- 1 | 29 | 52 | 54 | -------------------------------------------------------------------------------- /app/models/dtos/broker_dtos.py: -------------------------------------------------------------------------------- 1 | 2 | class Ticker: 3 | 4 | class Status: 5 | ACTIVE = 0 6 | INACTIVE = 1 7 | 8 | def __init__(self): 9 | self.isin = '' 10 | self.ticker = None 11 | self.name = None 12 | self.active = True 13 | self.exchange = None 14 | 15 | def __str__(self): 16 | return f"{self.ticker}-{self.isin}-{self.name}-{'inactive' if self.active else 'active'}" 17 | 18 | 19 | class BrokerAccount: 20 | def __init__(self): 21 | self.name = None 22 | self.account_id = '' 23 | self.currency = '' 24 | self.entity_id = '' 25 | self.balance = 0 26 | self.virtual_balance = 0 27 | 28 | 29 | class Transaction: 30 | 31 | class Type: 32 | BUY = 0 33 | SELL = 1 34 | SPLIT_BUY = 2 35 | SPLIT_SELL = 3 36 | OTC_BUY = 4 37 | OTC_SELL = 5 38 | SPIN_OFF_BUY = 6 39 | SPIN_OFF_SELL = 7 40 | SCRIPT_DIVIDEND = 8 41 | 42 | def __init__(self): 43 | self.name = '' 44 | self.value_date = None 45 | self.external_id = 0 46 | self.ticker = None 47 | self.shares = 0 48 | self.type = 0 49 | self.currency = 'EUR' 50 | self.price = 0 51 | self.fee = 0 52 | self.exchange_fee = 0 53 | self.currency_rate = 1 54 | 55 | def __str__(self): 56 | return f"{self.value_date}-{self.ticker.ticker}-{self.shares}@{self.price}-{self.name}" 57 | 58 | def to_dict(self): 59 | d = { 60 | "name": self.name, 61 | "value_date": self.value_date, 62 | "external_id": self.external_id, 63 | # "ticker": self.ticker, 64 | "shares": self.shares, 65 | "type": self.type, 66 | "price": self.price, 67 | "fee": self.fee, 68 | "exchange_fee": self.exchange_fee, 69 | "currency_rate": self.currency_rate, 70 | "currency": self.currency 71 | } 72 | return d 73 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_button-icon.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Icon buttons 3 | // 4 | 5 | .btn-icon { 6 | .btn-inner--icon { 7 | img { 8 | width: 20px; 9 | } 10 | } 11 | 12 | .btn-inner--text:not(:first-child) { 13 | margin-left: 0.75em; 14 | } 15 | 16 | .btn-inner--text:not(:last-child) { 17 | margin-right: 0.75em; 18 | } 19 | } 20 | 21 | 22 | // Button only with icon and NO text 23 | 24 | .btn-icon-only { 25 | width: 2.375rem; 26 | height: 2.375rem; 27 | padding: 0; 28 | } 29 | 30 | a.btn-icon-only { 31 | line-height: 2.5; 32 | } 33 | 34 | .btn-icon-only.btn-sm { 35 | width: 2rem; 36 | height: 2rem; 37 | } 38 | 39 | 40 | // 41 | // Clipboard button 42 | // dedicated element for copying icons 43 | // 44 | 45 | .btn-icon-clipboard { 46 | margin: 0; 47 | padding: 1.5rem; 48 | font-size: $font-size-base; 49 | font-weight: $font-weight-normal; 50 | line-height: 1.25; 51 | color: $gray-800; 52 | background-color: $gray-100; 53 | border-radius: $border-radius; 54 | border: 0; 55 | text-align: left; 56 | font-family: inherit; 57 | display: inline-block; 58 | vertical-align: middle; 59 | text-decoration: none; 60 | -moz-appearance: none; 61 | cursor: pointer; 62 | width: 100%; 63 | margin: .5rem 0; 64 | 65 | &:hover { 66 | background-color: $white; 67 | box-shadow: rgba(0, 0, 0, .1) 0 0 0 1px, rgba(0, 0, 0, .1) 0 4px 16px; 68 | } 69 | 70 | > div { 71 | align-items: center; 72 | display: flex; 73 | } 74 | 75 | i { 76 | box-sizing: content-box; 77 | color: theme-color("primary"); 78 | vertical-align: middle; 79 | font-size: 1.5rem; 80 | } 81 | 82 | span { 83 | display: inline-block; 84 | font-size: 0.875rem; 85 | line-height: 1.5; 86 | margin-left: 16px; 87 | overflow: hidden; 88 | white-space: nowrap; 89 | text-overflow: ellipsis; 90 | vertical-align: middle; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/backend/finance_reader/handlers/csv_read.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from models.system import Account, Entity 3 | from models.broker import Ticker, StockTransaction 4 | from finance_reader.entities.csv_reader import SUPPORTED_CSV_READER 5 | from finance_reader.handlers.broker_read import BrokerReader 6 | from finance_reader.handlers.exchange_read import ExchangeReader 7 | 8 | logger = logging.getLogger("csv_read") 9 | 10 | 11 | class CSVReader: 12 | def __init__(self): 13 | pass 14 | 15 | @staticmethod 16 | def _validate_data(data): 17 | logger.info("Validating data...") 18 | required_fields = ['account_id', 'entity_name', 'data'] 19 | for field in required_fields: 20 | if field not in data: 21 | logger.error(f"Missing required field: {field}") 22 | return False 23 | return True 24 | 25 | def process(self, data): 26 | if not self._validate_data(data): 27 | logger.error("Invalid request data") 28 | return 29 | 30 | logger.info(f"Starting CSV processing for {data['entity_name']}...") 31 | account_id = data.get('account_id') 32 | entity_type = data.get('entity_type') 33 | broker_name = data.get('entity_name').lower() 34 | content = data.get('data') 35 | 36 | csv_handler = SUPPORTED_CSV_READER.get(broker_name) 37 | if not csv_handler: 38 | return 39 | 40 | account = Account.get_by_account_id(account_id) 41 | logger.info("Processing CSV for account {account.id}...") 42 | 43 | orders, transactions = csv_handler.process_csv(content, account) 44 | logger.info(f"Found {len(orders)} orders in {broker_name}") 45 | 46 | if entity_type == Entity.Type.EXCHANGE: 47 | reader = ExchangeReader() 48 | orders = reader._join_orders(orders, transactions) 49 | reader.parse_read(account_id, [], orders) 50 | else: 51 | broker_reader = BrokerReader() 52 | broker_reader.parse_read(account_id, account, orders) 53 | 54 | return 55 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_modals.scss: -------------------------------------------------------------------------------- 1 | .modal-header { 2 | border-bottom: 1px solid $medium-gray; 3 | padding: 20px; 4 | text-align: center; 5 | display: block !important; 6 | 7 | &.no-border-header{ 8 | border-bottom: 0 none !important; 9 | & .modal-title{ 10 | margin-top: 20px; 11 | 12 | } 13 | } 14 | button.close{ 15 | &:focus { 16 | outline: none; 17 | } 18 | } 19 | 20 | .modal-profile { 21 | width: 70px; 22 | height: 70px; 23 | border-radius: 50%; 24 | text-align: center; 25 | line-height: 6.4; 26 | border: 1px solid rgba(0,0,0,.3); 27 | 28 | i { 29 | font-size: 30px; 30 | } 31 | } 32 | } 33 | .modal-dialog{ 34 | &.modal-sm, 35 | &.modal-register{ 36 | .modal-header{ 37 | button.close{ 38 | margin-top: 0; 39 | } 40 | } 41 | } 42 | } 43 | 44 | .modal-content { 45 | border: 0 none; 46 | border-radius: 10px; 47 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.15), 0 0 1px 1px rgba(0, 0, 0, 0.1); 48 | .modal-header{ 49 | h6{ 50 | margin-top: 10px; 51 | } 52 | } 53 | } 54 | 55 | .modal-dialog { 56 | padding-top: 60px; 57 | } 58 | .modal-body{ 59 | padding: 20px 50px; 60 | color: #000; 61 | } 62 | .modal-footer { 63 | border-top: 1px solid $medium-gray; 64 | padding: 0px; 65 | 66 | &.no-border-footer{ 67 | border-top: 0 none; 68 | } 69 | } 70 | .modal-footer .left-side, 71 | .modal-footer .right-side{ 72 | display: inline-block; 73 | text-align: center; 74 | width: 50%; 75 | padding: 5px; 76 | } 77 | .modal-footer .btn-link{ 78 | padding: 20px; 79 | width: 100%; 80 | margin: 0; 81 | } 82 | .modal-footer .divider{ 83 | background-color: $medium-gray; 84 | display: inline-block; 85 | float: inherit; 86 | height: 63px; 87 | margin: 0px -3px; 88 | // position: absolute; 89 | width: 1px; 90 | } 91 | .modal-register .modal-footer{ 92 | text-align: center; 93 | margin-bottom: 25px; 94 | padding: 20px 0 15px; 95 | span{ 96 | width: 100%; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/api/services/elasticsearch.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from elasticsearch import Elasticsearch 3 | 4 | 5 | class ESClient: 6 | def __init__(self, host): 7 | self.es = Elasticsearch([{'host': host, 'port': 9200}]) 8 | 9 | def save_comment(self, source, id, tickers, message, date): 10 | res = self.es.index(index='sw', doc_type=source, id=id, 11 | body={ 12 | "ticker": tickers, 13 | "message": message, 14 | "timestamp": datetime.now(), 15 | "date": date}) 16 | if not res['created']: 17 | print(res) 18 | 19 | def get_last_page(self): 20 | f = { 21 | "size": 10, 22 | "sort": {"date": "desc"}, 23 | "query": { 24 | "match_all": {} 25 | } 26 | } 27 | res = self.es.search(index="sw", doc_type="rankia", body=f) 28 | return res['hits']['hits'][0]["_id"].split("_")[0] 29 | 30 | def get_all_messages(self): 31 | res = self.es.search(index="sw", doc_type="rankia") 32 | for r in res['hits']['hits']: 33 | print(r['_source']['message']) 34 | 35 | def search_by_ticker(self, ticker): 36 | res = self.es.search(index="sw", body={'size': 100, "sort": {"date": "desc"}, "query": {"match": {'ticker': ticker}}}) 37 | return res['hits']['hits'] 38 | 39 | def search_comment(self, search_text): 40 | res = self.es.search(index="sw", body={ 41 | "query": { 42 | "more_like_this": { 43 | "fields": [ 44 | "message" 45 | ], 46 | "like_text": search_text, 47 | "min_term_freq": 1, 48 | "min_doc_freq": 1, 49 | "max_query_terms": 12 50 | } 51 | } 52 | }) 53 | 54 | # res = es.search(index="sw", body={"query": {"match": {'like_message': search_text}}}) 55 | for r in res['hits']['hits']: 56 | print(f"M: {r['_source'].get('date', '')}: {r['_source']['message']}") 57 | -------------------------------------------------------------------------------- /frontend/src/pages/Comments.vue: -------------------------------------------------------------------------------- 1 | 21 | 60 | 76 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Overview 3 | This dashboard displays various metrics related to an investment portfolio. It consists of multiple statistics cards and charts that provide insights into portfolio performance. 4 | 5 | ## Sections 6 | ### Portfolio Value Card 7 | Displays the total value of the investment portfolio based on current prices. 8 | 9 | Footer Stats: 10 | - Cost of the portfolio. 11 | - Unrealized profit and percentage. 12 | 13 | ### Realized Gains Card (Closed Positions) 14 | Shows the realized profit from closed positions. 15 | 16 | Footer Stats: 17 | - Realized gains year-to-date (YTD). 18 | - (Commented-out) Average gain per position. 19 | 20 | ### Total P/L Card 21 | Displays the total profit/loss, including both open and closed positions. 22 | Calculated as total_wallet_value + total_gain - total_invested. 23 | 24 | Footer Stats: 25 | - Return on Investment (ROI). 26 | - Realized profit percentage. 27 | 28 | ### Available Fiat Card 29 | Displays the available fiat currency in the account. 30 | 31 | Footer Stats: 32 | - Liquidity ratio. 33 | 34 | ## Charts 35 | 36 | ### Portfolio Distribution Chart 37 | Displays the distribution of wallet funds across brokers and exchanges using a Pie chart 38 | 39 | Data: Brokers, exchanges and fiat. 40 | 41 | ### Distribution by Account Chart 42 | Displays how the portfolio is distributed among different accounts using a Pie chart 43 | 44 | Data: Each account's virtual balance. 45 | 46 | ### Total Portfolio Chart 47 | Visualizes the total distribution of assets in the portfolio using a Pie chart 48 | 49 | Data: Proportion of total wallet value. 50 | 51 | ### Investment Distribution Chart 52 | Displays how investments are distributed across different brokers. 53 | 54 | Data: Brokers and Percentage of each broker's virtual balance in the total wallet. 55 | 56 | ### Wallet Distribution by Account (Bar Chart) 57 | 58 | Purpose: Displays the wallet distribution among different accounts using a bar chart. 59 | 60 | Chart Type: Bar chart. 61 | 62 | Title: "Wallet Distribution by Account". 63 | 64 | Data: Account-wise wallet distribution. 65 | 66 | ### Portfolio Growth Over Time 67 | 68 | Purpose: Shows the performance of the portfolio over time. 69 | 70 | Chart Type: Line chart. 71 | 72 | Title: "Portfolio Growth Over Time". 73 | 74 | Data: Time-series data of portfolio value. -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper-dashboard.scss: -------------------------------------------------------------------------------- 1 | @import 'paper/variables'; 2 | @import 'paper/mixins'; 3 | 4 | // Plugins CSS 5 | @import 'paper/plugins/plugin-bootstrap-switch'; 6 | @import 'paper/plugins/plugin-perfect-scrollbar'; 7 | @import 'paper/plugins/plugin-sweetalert2'; 8 | @import 'paper/plugins/plugin-card-wizard'; 9 | @import 'paper/plugins/plugin-jquery.jvectormap'; 10 | // @import 'paper/plugins/plugin-fullcalendar'; 11 | @import 'paper/plugins/plugin-vue-notifyjs'; 12 | @import 'paper/plugins/plugin-nprogress'; 13 | 14 | // Element UI plugins 15 | @import "paper/plugins/element-ui/plugin-tags"; 16 | @import "paper/plugins/element-ui/plugin-slider"; 17 | @import "paper/plugins/element-ui/plugin-select"; 18 | @import "paper/plugins/element-ui/plugin-inputs"; 19 | @import "paper/plugins/element-ui/plugin-tables"; 20 | 21 | // Core CSS 22 | @import 'paper/nucleo-outline'; 23 | @import 'paper/buttons'; 24 | @import 'paper/social-buttons'; 25 | @import 'paper/inputs'; 26 | @import 'paper/typography'; 27 | @import 'paper/misc'; 28 | @import 'paper/misc-extend'; 29 | @import 'paper/navbar'; 30 | @import 'paper/dropdown'; 31 | @import 'paper/alerts'; 32 | @import 'paper/images'; 33 | @import 'paper/tables'; 34 | @import 'paper/footers'; 35 | 36 | // components 37 | @import 'paper/checkboxes-radio'; 38 | @import 'paper/progress'; 39 | @import 'paper/pagination'; 40 | @import 'paper/info-areas'; 41 | @import 'paper/pills'; 42 | @import 'paper/tabs'; 43 | @import 'paper/modals'; 44 | @import 'paper/sidebar-and-main-panel'; 45 | // @import 'paper/timeline'; 46 | 47 | // cards 48 | @import 'paper/cards'; 49 | @import "paper/cards/card-chart"; 50 | @import "paper/cards/card-map"; 51 | @import "paper/cards/card-user"; 52 | 53 | @import "paper/cards/card-background"; 54 | @import "paper/cards/card-collapse"; 55 | @import "paper/cards/card-contributions"; 56 | @import "paper/cards/card-info-area"; 57 | @import "paper/cards/card-lock"; 58 | @import "paper/cards/card-pricing"; 59 | @import "paper/cards/card-profile"; 60 | @import "paper/cards/card-plain"; 61 | @import "paper/cards/card-signup"; 62 | @import "paper/cards/card-stats-mini"; 63 | @import "paper/cards/card-stats"; 64 | @import "paper/cards/card-subcategories"; 65 | @import "paper/cards/card-testimonials"; 66 | @import "paper/cards/card-tasks"; 67 | 68 | 69 | 70 | // example pages and sections 71 | @import 'paper/example-pages'; 72 | @import 'paper/sections'; 73 | 74 | 75 | @import 'paper/responsive'; 76 | @import 'paper/media-queries'; 77 | @import "paper/fixed-social-plugin"; 78 | 79 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/plugins/element-ui/_plugin-select.scss: -------------------------------------------------------------------------------- 1 | @import "../../variables"; 2 | @mixin select($type, $color) { 3 | .select-#{$type}.el-select { 4 | } 5 | .select-#{$type}.el-select .el-input { 6 | .el-input__suffix{ 7 | display: flex; 8 | align-items: center; 9 | } 10 | &:hover{ 11 | .el-input__icon, 12 | input { 13 | &::placeholder{ 14 | color: blue; 15 | } 16 | color: red; 17 | } 18 | input, 19 | .el-input__icon{ 20 | background-color: $color; 21 | transition: none; 22 | } 23 | 24 | } 25 | .el-input__icon{ 26 | border-radius:20px; 27 | height: 40px; 28 | width: 30px; 29 | } 30 | 31 | input{ 32 | background-color: red; 33 | border-color: $color !important; 34 | border-width: 2px; 35 | border-radius: 20px; 36 | color: $color; 37 | } 38 | .el-input__icon{ 39 | color:$color; 40 | } 41 | } 42 | .select-#{$type} { 43 | 44 | .el-tag, .el-tag.el-tag--info { 45 | line-height: 24px; 46 | background-color: red !important; 47 | border: none !important; 48 | 49 | .el-tag__close { 50 | width: 20px; 51 | height: 20px; 52 | color: white; 53 | background-color: transparent; 54 | 55 | &:hover { 56 | background-color: red; 57 | color: $color; 58 | } 59 | 60 | &:before { 61 | margin-top: 2px; 62 | } 63 | 64 | } 65 | } 66 | 67 | &.el-select-dropdown__item.selected, 68 | &.el-select-dropdown__item.selected.hover { 69 | background-color: red; 70 | color: white; 71 | } 72 | 73 | &.el-select-dropdown.is-multiple 74 | .el-select-dropdown__item.selected { 75 | &.select-#{$type} { 76 | color: $color; 77 | } 78 | } 79 | } 80 | } 81 | 82 | .el-select-dropdown__item.hover { 83 | background-color: #f5f7fa; 84 | } 85 | 86 | .el-select .el-input { 87 | &:hover { 88 | .el-input__icon, 89 | input { 90 | &::placeholder { 91 | color: white; 92 | } 93 | color: black; 94 | } 95 | } 96 | } 97 | .el-select-dropdown { 98 | border-radius:10px; 99 | } 100 | 101 | @include select('default', $default-color); 102 | @include select('info', $info-color); 103 | @include select('primary', $primary-color); 104 | @include select('success', $success-color); 105 | @include select('warning', $warning-color); 106 | @include select('danger', $danger-color); 107 | -------------------------------------------------------------------------------- /app/backend/finance_reader/entities/csv_reader/coinbase.py: -------------------------------------------------------------------------------- 1 | from finance_reader.entities.csv_reader import ExchangeCSVProcessor 2 | from models.dtos.exchange_dtos import Order, Transaction, OrderType 3 | from datetime import datetime 4 | 5 | 6 | class Coinbase(ExchangeCSVProcessor): 7 | 8 | def __init__(self): 9 | super(Coinbase, self).__init__() 10 | 11 | def process_csv(self, csv_data, account): 12 | self.log.info(f"Processing Coinbase CSV file for account {account.id}") 13 | lines = csv_data.splitlines() 14 | headers = lines[0].split(",") 15 | 16 | orders = [] 17 | transactions = [] 18 | for line in lines[1:]: 19 | data = dict(zip(headers, line.split(","))) 20 | type = None 21 | 22 | if data.get('Transaction Type') == 'Buy' and data.get('EUR Spot Price at Transaction') == '1': 23 | type = OrderType.DEPOSIT 24 | elif data.get('Transaction Type') == 'Buy': 25 | type = OrderType.BUY 26 | elif data.get('Transaction Type') == 'Sell': 27 | type = OrderType.SELL 28 | elif data.get('Transaction Type') == 'Send': 29 | type = OrderType.WITHDRAWAL 30 | 31 | amount = float(data.get("Quantity Transacted")) 32 | value_date = datetime.strptime(data.get('Timestamp'), "%Y-%m-%dT%H:%M:%SZ") 33 | currency = data.get("Asset") 34 | external_id = f"{value_date}_{currency}_{amount}" 35 | fees = float(data.get('EUR Fees') or 0) 36 | 37 | if type in [OrderType.WITHDRAWAL, OrderType.DEPOSIT]: 38 | trans = Transaction() 39 | trans.account_id = account.id 40 | trans.external_id = external_id 41 | trans.value_date = value_date 42 | trans.amount = abs(amount) 43 | trans.currency = data.get("Asset") 44 | trans.type = type 45 | try: 46 | trans.rx_address = data.get('Notes').split(f'{trans.currency} to ')[1] 47 | except: 48 | pass 49 | trans.fee = fees 50 | transactions.append(trans) 51 | continue 52 | 53 | order = Order() 54 | order.account_id = account.id 55 | order.external_id = external_id 56 | order.value_date = value_date 57 | order.pair = f"{data.get('Asset')}/EUR" 58 | order.amount = amount 59 | order.type = type 60 | order.price = float(data.get("EUR Subtotal"))/float(data.get('Quantity Transacted')) 61 | order.fee = fees 62 | orders.append(order) 63 | 64 | return orders, transactions 65 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/Cards/Card.vue: -------------------------------------------------------------------------------- 1 | 41 | 92 | 94 | -------------------------------------------------------------------------------- /app/api/api/users/tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | 4 | # from api import app 5 | from app import app, initialize_app 6 | initialize_app(app) 7 | 8 | """ 9 | Sample test data 10 | """ 11 | 12 | DUMMY_USERNAME = "test" 13 | DUMMY_EMAIL = "test@test.com" 14 | DUMMY_PASS = "newpassword" 15 | 16 | 17 | @pytest.fixture 18 | def client(): 19 | with app.test_client() as client: 20 | yield client 21 | 22 | 23 | def test_user_signup(client): 24 | """ 25 | Tests /users/register API 26 | """ 27 | response = client.post( 28 | "api/users/register", 29 | data=json.dumps( 30 | { 31 | "username": DUMMY_USERNAME, 32 | "email": DUMMY_EMAIL, 33 | "password": DUMMY_PASS 34 | } 35 | ), 36 | content_type="application/json") 37 | 38 | data = json.loads(response.data.decode()) 39 | print(response.data) 40 | assert response.status_code == 200 41 | assert "The user was successfully registered" in data["message"] 42 | 43 | 44 | def test_user_signup_invalid_data(client): 45 | """ 46 | Tests /users/register API: invalid data like email field empty 47 | """ 48 | response = client.post( 49 | "api/users/register", 50 | data=json.dumps( 51 | { 52 | "username": DUMMY_USERNAME, 53 | "email": "", 54 | "password": DUMMY_PASS 55 | } 56 | ), 57 | content_type="application/json") 58 | 59 | data = json.loads(response.data.decode()) 60 | assert response.status_code == 400 61 | # assert "'' is too short" in data["message"] 62 | 63 | 64 | def test_user_login_correct(client): 65 | """ 66 | Tests /users/signup API: Correct credentials 67 | """ 68 | response = client.post( 69 | "api/users/login", 70 | data=json.dumps( 71 | { 72 | "email": DUMMY_EMAIL, 73 | "password": DUMMY_PASS 74 | } 75 | ), 76 | content_type="application/json") 77 | 78 | data = json.loads(response.data.decode()) 79 | assert response.status_code == 200 80 | assert data["token"] != "" 81 | 82 | 83 | def test_user_login_error(client): 84 | """ 85 | Tests /users/signup API: Wrong credentials 86 | """ 87 | response = client.post( 88 | "api/users/login", 89 | data=json.dumps( 90 | { 91 | "email": DUMMY_EMAIL, 92 | "password": DUMMY_EMAIL 93 | } 94 | ), 95 | content_type="application/json") 96 | 97 | data = json.loads(response.data.decode()) 98 | assert response.status_code == 400 99 | assert "Wrong credentials." in data["message"] 100 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/Navbar/Navbar.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 77 | 78 | 108 | -------------------------------------------------------------------------------- /app/api/tests.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import pytest 3 | import json 4 | 5 | # from api import app 6 | from app import app, initialize_app 7 | # initialize_app(app) 8 | 9 | """ 10 | Sample test data 11 | """ 12 | 13 | DUMMY_USERNAME = "test" 14 | DUMMY_EMAIL = "test@test.com" 15 | DUMMY_PASS = "newpassword" 16 | 17 | 18 | @pytest.fixture 19 | def client(): 20 | with app.test_client() as client: 21 | yield client 22 | 23 | 24 | def test_user_signup(client): 25 | """ 26 | Tests /users/register API 27 | """ 28 | response = client.post( 29 | "api/users/register", 30 | data=json.dumps( 31 | { 32 | "username": DUMMY_USERNAME, 33 | "email": DUMMY_EMAIL, 34 | "password": DUMMY_PASS 35 | } 36 | ), 37 | content_type="application/json") 38 | 39 | data = json.loads(response.data.decode()) 40 | print(response.data) 41 | assert response.status_code == 200 42 | assert "The user was successfully registered" in data["message"] 43 | 44 | 45 | def test_user_signup_invalid_data(client): 46 | """ 47 | Tests /users/register API: invalid data like email field empty 48 | """ 49 | response = client.post( 50 | "api/users/register", 51 | data=json.dumps( 52 | { 53 | "username": DUMMY_USERNAME, 54 | "email": "", 55 | "password": DUMMY_PASS 56 | } 57 | ), 58 | content_type="application/json") 59 | 60 | data = json.loads(response.data.decode()) 61 | assert response.status_code == 400 62 | # assert "'' is too short" in data["message"] 63 | 64 | 65 | def test_user_login_correct(client): 66 | """ 67 | Tests /users/signup API: Correct credentials 68 | """ 69 | response = client.post( 70 | "api/users/login", 71 | data=json.dumps( 72 | { 73 | "email": DUMMY_EMAIL, 74 | "password": DUMMY_PASS 75 | } 76 | ), 77 | content_type="application/json") 78 | 79 | data = json.loads(response.data.decode()) 80 | assert response.status_code == 200 81 | assert data["token"] != "" 82 | 83 | 84 | def test_user_login_error(client): 85 | """ 86 | Tests /users/signup API: Wrong credentials 87 | """ 88 | response = client.post( 89 | "api/users/login", 90 | data=json.dumps( 91 | { 92 | "email": DUMMY_EMAIL, 93 | "password": DUMMY_EMAIL 94 | } 95 | ), 96 | content_type="application/json") 97 | 98 | data = json.loads(response.data.decode()) 99 | assert response.status_code == 400 100 | assert "Wrong credentials." in data["message"] 101 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/cards/_card-pricing.scss: -------------------------------------------------------------------------------- 1 | .card-pricing{ 2 | text-align: center; 3 | 4 | .card-body{ 5 | padding: 15px!important; 6 | } 7 | .card-category{ 8 | margin: 10px 0 10px; 9 | color: $black-color; 10 | } 11 | .card-icon{ 12 | padding: 15px 0 5px; 13 | transform: translateY(0%); 14 | color: $black-color; 15 | 16 | &.icon-info{ 17 | color: $info-color; 18 | } 19 | &.icon-danger{ 20 | color: $danger-color; 21 | } 22 | &.icon-primary{ 23 | color: $primary-color; 24 | } 25 | &.icon-success{ 26 | color: $success-color; 27 | } 28 | &.icon-warning{ 29 | color: $warning-color; 30 | } 31 | 32 | i { 33 | font-size: 40px; 34 | width: 105px; 35 | border: 2px solid $table-line-color; 36 | border-radius: 50%; 37 | height: 105px; 38 | line-height: 105px; 39 | } 40 | } 41 | .card-title{ 42 | margin-top: 30px !important; 43 | } 44 | ul{ 45 | list-style: none; 46 | padding: 0; 47 | max-width: 240px; 48 | margin: 20px auto; 49 | 50 | li{ 51 | padding: 5px 0; 52 | list-style-type: none; 53 | b{ 54 | font-weight: 600; 55 | color: $black-color; 56 | } 57 | } 58 | } 59 | .btn-neutral{ 60 | color: $default-color; 61 | 62 | &:hover, :focus{ 63 | color: $default-states-color; 64 | } 65 | } 66 | 67 | &.card-background-image{ 68 | ul{ 69 | li{ 70 | color: $white-color; 71 | text-align: center; 72 | border-color: rgba(255,255,255,.3); 73 | 74 | b{ 75 | color: $white-color !important; 76 | } 77 | } 78 | } 79 | .card-description{ 80 | color: $white-color !important; 81 | } 82 | .card-title{ 83 | small{ 84 | color: rgba(255, 255, 255, 0.6); 85 | } 86 | } 87 | } 88 | } 89 | 90 | .card-pricing.card-plain{ 91 | .card-category, 92 | .card-title{ 93 | color: $black-color; 94 | } 95 | ul{ 96 | li{ 97 | b{ 98 | font-weight: 600; 99 | color: $black-color; 100 | } 101 | } 102 | } 103 | } 104 | 105 | 106 | .card[data-background="image"] .card-icon i, 107 | .card[data-background="color"] .card-icon i, 108 | .card[data-color] .card-icon i { 109 | color: $white-color; 110 | border: 2px solid rgba(255, 255, 255, 0.3); 111 | } 112 | -------------------------------------------------------------------------------- /app/backend/finance_reader/entities/csv_reader/bitstamp.py: -------------------------------------------------------------------------------- 1 | from finance_reader.entities.csv_reader import ExchangeCSVProcessor 2 | from models.dtos.exchange_dtos import Order, Transaction, OrderType 3 | from datetime import datetime 4 | 5 | 6 | class Bitstamp(ExchangeCSVProcessor): 7 | 8 | def __init__(self): 9 | super(Bitstamp, self).__init__() 10 | 11 | def process_csv(self, csv_data, account): 12 | self.log.info(f"Processing Bitstamp CSV file for account {account.id}") 13 | lines = csv_data.splitlines() 14 | headers = lines[0].split(",") 15 | 16 | orders = [] 17 | transactions = [] 18 | for line in lines[1:]: 19 | data = dict(zip(headers, line.split(","))) 20 | if data.get('Type') in ('Deposit', 'Withdrawal'): 21 | trans = self.create_transaction(account, data) 22 | if trans: 23 | transactions.append(trans) 24 | else: 25 | order = self.create_order(account, data) 26 | if order: 27 | orders.append(order) 28 | 29 | return orders, transactions 30 | 31 | def create_order(self, account, data): 32 | type = None 33 | if data.get('Subtype') == 'Sell': 34 | type = OrderType.SELL 35 | elif data.get('Subtype') == 'Buy': 36 | type = OrderType.BUY 37 | 38 | if type == None: 39 | self.log.error(f"Unknown order type: {data.get('Type')} - {data.get('Subtype')}") 40 | return None 41 | 42 | order = Order() 43 | order.account_id = account.id 44 | order.external_id = data.get("ID") 45 | order.value_date = datetime.strptime(data.get("Datetime"), "%Y-%m-%dT%H:%M:%SZ") 46 | order.pair = data.get("Amount currency") + "/" + data.get("Value currency") 47 | order.amount = float(data.get("Amount")) 48 | order.type = type 49 | order.price = float(data.get("Rate")) 50 | order.fee = float(data.get("Fee") or 0) 51 | return order 52 | 53 | def create_transaction(self, account, data): 54 | type = None 55 | if data.get('Type') == 'Deposit': 56 | type = OrderType.DEPOSIT 57 | elif data.get('Type') == 'Withdrawal': 58 | type = OrderType.WITHDRAWAL 59 | 60 | if not type: 61 | self.log.error(f"Unknown transaction type: {data.get('Type')} - {data.get('Subtype')}") 62 | return None 63 | 64 | trans = Transaction() 65 | trans.account_id = account.id 66 | trans.external_id = data.get("ID") 67 | trans.value_date = datetime.strptime(data.get("Datetime"), "%Y-%m-%dT%H:%M:%SZ") 68 | trans.amount = float(data.get("Amount")) 69 | trans.currency = data.get("Amount currency") 70 | trans.type = type 71 | trans.rx_address = "" 72 | # TODO: check the fee! 73 | trans.fee = float(data.get("Fee") or 0) 74 | return trans 75 | -------------------------------------------------------------------------------- /frontend/src/pages/auth/Reset.vue: -------------------------------------------------------------------------------- 1 | 28 | 81 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | api: 5 | build: 6 | context: ./app 7 | dockerfile: Dockerfile 8 | target: api 9 | command: python app.py run -h 0.0.0.0 10 | volumes: 11 | - ./app/api:/usr/src/ 12 | - ./app/config:/usr/src/config 13 | - ./app/models:/usr/src/models 14 | - ./VERSION:/usr/VERSION 15 | environment: 16 | - FLASK_ENV=development 17 | - BACKEND_SETTINGS=config.full 18 | networks: 19 | - api_bridge 20 | restart: always 21 | depends_on: 22 | - db 23 | - redis 24 | 25 | backend: 26 | build: 27 | context: ./app 28 | dockerfile: Dockerfile 29 | target: backend 30 | command: python worker.py 31 | volumes: 32 | - ./app/backend:/usr/src/ 33 | - ./app/config:/usr/src/config 34 | - ./app/models:/usr/src/models 35 | environment: 36 | - BACKEND_SETTINGS=config.full 37 | - PYTHON_FILE=worker.py 38 | - PYTHON_ARGS="" 39 | networks: 40 | - api_bridge 41 | restart: always 42 | deploy: 43 | replicas: 1 44 | depends_on: 45 | - db 46 | - redis 47 | 48 | migrate: 49 | image: queds_api 50 | command: bash -c "alembic -c /usr/src/models/migrations/alembic.ini upgrade head" 51 | volumes: 52 | - ./app/config:/usr/src/config 53 | - ./app/models:/usr/src/models 54 | environment: 55 | - BACKEND_SETTINGS=config.full 56 | networks: 57 | - api_bridge 58 | depends_on: 59 | - db 60 | - api 61 | 62 | frontend: 63 | build: 64 | context: ./frontend 65 | dockerfile: Dockerfile 66 | args: 67 | - VITE_APP_BACKEND_URL=/api 68 | - VITE_APP_IS_DEMO=false 69 | command: npm run dev 70 | volumes: 71 | - './frontend:/app' 72 | - frontend_node_modules:/app/node_modules 73 | environment: 74 | - CHOKIDAR_USEPOLLING=true 75 | - NODE_ENV=development 76 | networks: 77 | - api_bridge 78 | depends_on: 79 | - db 80 | - api 81 | 82 | nginx: 83 | image: nginx 84 | volumes: 85 | - ./nginx_template.conf:/etc/nginx/conf.d/default.conf 86 | restart: always 87 | ports: 88 | - 6060:80 89 | depends_on: 90 | - backend 91 | - frontend 92 | networks: 93 | - api_bridge 94 | 95 | redis: 96 | image: bitnami/redis 97 | environment: 98 | - ALLOW_EMPTY_PASSWORD=yes 99 | logging: 100 | driver: none 101 | networks: 102 | - api_bridge 103 | 104 | db: 105 | image: timescale/timescaledb:2.23.1-pg16 106 | restart: always 107 | environment: 108 | POSTGRES_DB: queds 109 | POSTGRES_USER: queds_user 110 | POSTGRES_PASSWORD: Kbh85n7M6Fxo 111 | volumes: 112 | - users_data:/var/lib/postgresql/data/ 113 | networks: 114 | - api_bridge 115 | ports: 116 | - 15432:5432 117 | 118 | networks: 119 | api_bridge: 120 | driver: bridge 121 | 122 | volumes: 123 | users_data: 124 | frontend_node_modules: 125 | -------------------------------------------------------------------------------- /frontend/src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import router from "@/router"; 2 | import axios from "axios"; 3 | const API_URL = "/api/users"; 4 | 5 | export default { 6 | state: { 7 | token: localStorage.getItem("access_token") || '' 8 | }, 9 | 10 | getters: { 11 | token: state => state.token, 12 | isLoggedIn: state => !!state.token, 13 | }, 14 | 15 | mutations: { 16 | auth_success(state, token) { 17 | state.status = true; 18 | state.token = token; 19 | }, 20 | logout(state) { 21 | console.log("Mutation state to null"); 22 | state.status = ''; 23 | state.token = ''; 24 | }, 25 | }, 26 | 27 | actions: { 28 | initialize(context) { 29 | const token = localStorage.getItem("access_token"); 30 | if (token) { 31 | axios.defaults.headers.common['Authorization'] = token; 32 | context.commit("auth_success", token); 33 | } 34 | }, 35 | login(context, payload) { 36 | return axios 37 | .post(API_URL + '/login', { 38 | email: payload.user.email, 39 | password: payload.user.password 40 | }) 41 | .then(response => { 42 | let token = "Bearer " + response.data['token']; 43 | localStorage.setItem("access_token", token); 44 | localStorage.setItem("base_currency", response.data['base_currency']); 45 | axios.defaults.headers.common['Authorization'] = token; 46 | 47 | context.commit("auth_success", token); 48 | router.push({path: "/"}); 49 | })/*.catch(function (error) { 50 | console.log("Unable to login: " + error); 51 | context.commit('logout'); 52 | router.push({name: "Login"}); 53 | })*/; 54 | }, 55 | register(context, payload) { 56 | return axios 57 | .post(API_URL + '/register', { 58 | email: payload.user.email, 59 | password: payload.user.password, 60 | password_confirmation: payload.user.password_confirmation 61 | }) 62 | .then(response => { 63 | router.push({path: "/"}); 64 | }); 65 | }, 66 | logout(context, payload) { 67 | return axios 68 | .post(API_URL + '/logout', { 69 | current_user: "" 70 | }) 71 | .then(response => { 72 | context.commit('logout'); 73 | localStorage.removeItem("access_token"); 74 | router.push({name: "Login"}); 75 | //return response.data; 76 | }).catch(function (error) { 77 | console.log("Error with logout, redirect to login page"); 78 | localStorage.removeItem("access_token"); 79 | router.push({name: "Login"}); 80 | }); 81 | } 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /frontend/src/pages/auth/Email.vue: -------------------------------------------------------------------------------- 1 | 30 | 80 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/Charts/LineChart.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 116 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_cards.scss: -------------------------------------------------------------------------------- 1 | .card{ 2 | border-radius: $border-radius-extreme; 3 | box-shadow: 0 6px 10px -4px rgba(0, 0, 0, 0.15); 4 | background-color: #FFFFFF; 5 | color: $card-black-color; 6 | margin-bottom: 20px; 7 | position: relative; 8 | border: 0 none; 9 | 10 | -webkit-transition: transform 300ms cubic-bezier(0.34, 2, 0.6, 1), box-shadow 200ms ease; 11 | -moz-transition: transform 300ms cubic-bezier(0.34, 2, 0.6, 1), box-shadow 200ms ease; 12 | -o-transition: transform 300ms cubic-bezier(0.34, 2, 0.6, 1), box-shadow 200ms ease; 13 | -ms-transition: transform 300ms cubic-bezier(0.34, 2, 0.6, 1), box-shadow 200ms ease; 14 | transition: transform 300ms cubic-bezier(0.34, 2, 0.6, 1), box-shadow 200ms ease; 15 | 16 | .card-body{ 17 | padding: 15px 15px 10px 15px; 18 | 19 | &.table-full-width{ 20 | padding-left: 0; 21 | padding-right: 0; 22 | } 23 | } 24 | 25 | .card-header{ 26 | &:not([data-background-color]){ 27 | background-color: transparent; 28 | } 29 | padding: 15px 15px 0; 30 | border: 0; 31 | 32 | .card-title{ 33 | margin-top: 10px; 34 | } 35 | } 36 | 37 | .map{ 38 | border-radius: $border-radius-small; 39 | 40 | &.map-big{ 41 | height: 400px; 42 | } 43 | } 44 | 45 | &[data-background-color="orange"]{ 46 | background-color: $primary-color; 47 | 48 | .card-header{ 49 | background-color: $primary-color; 50 | } 51 | 52 | .card-footer{ 53 | .stats{ 54 | color: $white-color; 55 | } 56 | } 57 | } 58 | 59 | &[data-background-color="red"]{ 60 | background-color: $danger-color; 61 | } 62 | 63 | &[data-background-color="yellow"]{ 64 | background-color: $warning-color; 65 | } 66 | 67 | &[data-background-color="blue"]{ 68 | background-color: $info-color; 69 | } 70 | 71 | &[data-background-color="green"]{ 72 | background-color: $success-color; 73 | } 74 | 75 | .image{ 76 | overflow: hidden; 77 | height: 200px; 78 | position: relative; 79 | } 80 | 81 | .avatar{ 82 | width: 30px; 83 | height: 30px; 84 | overflow: hidden; 85 | border-radius: 50%; 86 | margin-bottom: 15px; 87 | } 88 | 89 | .numbers { 90 | font-size: 2em; 91 | } 92 | 93 | .big-title { 94 | font-size: 12px; 95 | text-align: center; 96 | font-weight: 500; 97 | padding-bottom: 15px; 98 | } 99 | 100 | label{ 101 | font-size: $font-size-small; 102 | margin-bottom: 5px; 103 | color: $dark-gray; 104 | } 105 | 106 | .card-footer{ 107 | background-color: transparent; 108 | border: 0; 109 | 110 | 111 | .stats{ 112 | i{ 113 | margin-right: 5px; 114 | position: relative; 115 | top: 0px; 116 | color: $default-color; 117 | } 118 | } 119 | 120 | .btn{ 121 | margin: 0; 122 | } 123 | } 124 | 125 | &.card-plain{ 126 | background-color: transparent; 127 | box-shadow: none; 128 | border-radius: 0; 129 | 130 | 131 | .card-body{ 132 | padding-left: 5px; 133 | padding-right: 5px; 134 | } 135 | 136 | img{ 137 | border-radius: $border-radius-extreme; 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /frontend/src/pages/accounts/UploadCSV.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 96 | 97 | -------------------------------------------------------------------------------- /app/models/dtos/exchange_dtos.py: -------------------------------------------------------------------------------- 1 | 2 | class ExchangeWallet: 3 | def __init__(self): 4 | self.account_id = None 5 | self.currency = None 6 | self.balance = None 7 | 8 | def to_dict(self): 9 | d = { 10 | "account_id": self.account_id, 11 | "currency": self.currency, 12 | "balance": self.balance 13 | } 14 | return d 15 | 16 | 17 | class OrderType: 18 | BUY = 0 19 | SELL = 1 20 | DEPOSIT = 2 21 | WITHDRAWAL = 3 22 | CASH_IN = 4 23 | CASH_OUT = 5 24 | STAKING = 6 25 | AIRDROP = 7 26 | 27 | 28 | class Order: 29 | def __init__(self): 30 | self.account_id = None 31 | self.external_id = None 32 | self.pair = None 33 | self.value_date = None 34 | self.type = None 35 | self.price = None 36 | self.amount = None 37 | self.fee = None 38 | 39 | def to_dict(self): 40 | d = { 41 | "account_id": self.account_id, 42 | "external_id": self.external_id, 43 | "symbol": self.pair, 44 | "value_date": self.value_date, 45 | "type": self.type, 46 | "price": self.price, 47 | "fee": self.fee, 48 | "amount": self.amount, 49 | "event_type": "exchange_order" 50 | } 51 | return d 52 | 53 | 54 | class Transaction: 55 | def __init__(self): 56 | self.account_id = None 57 | self.external_id = 0 58 | self.value_date = None 59 | self.amount = 0 60 | self.type = 0 61 | self.currency = None 62 | self.rx_address = 0 63 | self.tx_address = 0 64 | self.fee = 0 65 | 66 | def to_dict(self): 67 | d = { 68 | "account_id": self.account_id, 69 | "external_id": self.external_id, 70 | "symbol": self.currency, 71 | "value_date": self.value_date, 72 | "type": self.type, 73 | "rx_address": self.rx_address, 74 | "tx_address": self.tx_address, 75 | "fee": self.fee, 76 | "amount": self.amount, 77 | "event_type": "exchange_transaction", 78 | "price": 0 79 | } 80 | return d 81 | 82 | class CryptoEventDTO: 83 | def __init__(self): 84 | self.account_id = None 85 | self.external_id = None 86 | self.value_date = None 87 | self.symbol = None 88 | self.amount = None 89 | self.price = None 90 | self.fee = None 91 | 92 | self.event_type = None 93 | self.type = None 94 | 95 | self.status = None 96 | self.rx_address = None 97 | self.tx_address = None 98 | 99 | def to_dict(self): 100 | d = { 101 | "account_id": self.account_id, 102 | "external_id": self.external_id, 103 | "value_date": self.value_date, 104 | "symbol": self.symbol, 105 | "amount": self.amount, 106 | "price": self.price, 107 | "fee": self.fee, 108 | "event_type": self.event_type, 109 | "type": self.type, 110 | "rx_address": self.rx_address, 111 | "tx_address": self.tx_address 112 | } 113 | return d -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/mixins/_chartist.scss: -------------------------------------------------------------------------------- 1 | // Scales for responsive SVG containers 2 | $ct-scales: ((1), (15/16), (8/9), (5/6), (4/5), (3/4), (2/3), (5/8), (1/1.618), (3/5), (9/16), (8/15), (1/2), (2/5), (3/8), (1/3), (1/4)) !default; 3 | $ct-scales-names: (ct-square, ct-minor-second, ct-major-second, ct-minor-third, ct-major-third, ct-perfect-fourth, ct-perfect-fifth, ct-minor-sixth, ct-golden-section, ct-major-sixth, ct-minor-seventh, ct-major-seventh, ct-octave, ct-major-tenth, ct-major-eleventh, ct-major-twelfth, ct-double-octave) !default; 4 | 5 | // Class names to be used when generating CSS 6 | $ct-class-chart: ct-chart !default; 7 | $ct-class-chart-line: ct-chart-line !default; 8 | $ct-class-chart-bar: ct-chart-bar !default; 9 | $ct-class-horizontal-bars: ct-horizontal-bars !default; 10 | $ct-class-chart-pie: ct-chart-pie !default; 11 | $ct-class-chart-donut: ct-chart-donut !default; 12 | $ct-class-label: ct-label !default; 13 | $ct-class-series: ct-series !default; 14 | $ct-class-line: ct-line !default; 15 | $ct-class-point: ct-point !default; 16 | $ct-class-area: ct-area !default; 17 | $ct-class-bar: ct-bar !default; 18 | $ct-class-slice-pie: ct-slice-pie !default; 19 | $ct-class-slice-donut: ct-slice-donut !default; 20 | $ct-class-grid: ct-grid !default; 21 | $ct-class-vertical: ct-vertical !default; 22 | $ct-class-horizontal: ct-horizontal !default; 23 | $ct-class-start: ct-start !default; 24 | $ct-class-end: ct-end !default; 25 | 26 | // Container ratio 27 | $ct-container-ratio: (1/1.618) !default; 28 | 29 | // Text styles for labels 30 | $ct-text-color: rgba(0, 0, 0, 0.4) !default; 31 | $ct-text-size: 1.3rem !default; 32 | $ct-text-align: flex-start !default; 33 | $ct-text-justify: flex-start !default; 34 | $ct-text-line-height: 1; 35 | 36 | // Grid styles 37 | $ct-grid-color: rgba(0, 0, 0, 0.2) !default; 38 | $ct-grid-dasharray: 2px !default; 39 | $ct-grid-width: 1px !default; 40 | 41 | // Line chart properties 42 | $ct-line-width: 3px !default; 43 | $ct-line-dasharray: false !default; 44 | $ct-point-size: 8px !default; 45 | // Line chart point, can be either round or square 46 | $ct-point-shape: round !default; 47 | // Area fill transparency between 0 and 1 48 | $ct-area-opacity: 0.8 !default; 49 | 50 | // Bar chart bar width 51 | $ct-bar-width: 10px !default; 52 | 53 | // Donut width (If donut width is to big it can cause issues where the shape gets distorted) 54 | $ct-donut-width: 60px !default; 55 | 56 | // If set to true it will include the default classes and generate CSS output. If you're planning to use the mixins you 57 | // should set this property to false 58 | $ct-include-classes: true !default; 59 | 60 | // If this is set to true the CSS will contain colored series. You can extend or change the color with the 61 | // properties below 62 | $ct-include-colored-series: $ct-include-classes !default; 63 | 64 | // If set to true this will include all responsive container variations using the scales defined at the top of the script 65 | $ct-include-alternative-responsive-containers: $ct-include-classes !default; 66 | 67 | // Series names and colors. This can be extended or customized as desired. Just add more series and colors. 68 | $ct-series-names: (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) !default; 69 | $ct-series-colors: ( 70 | $new-blue, 71 | $new-red, 72 | $new-orange, 73 | $new-purple, 74 | $new-green, 75 | $new-dark-blue, 76 | $new-black, 77 | $social-google, 78 | $social-tumblr, 79 | $social-youtube, 80 | $social-twitter, 81 | $social-pinterest, 82 | $social-behance, 83 | #6188e2, 84 | #a748ca 85 | ) !default; 86 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_misc.scss: -------------------------------------------------------------------------------- 1 | body{ 2 | color: $black-color; 3 | font-size: $font-size-base; 4 | font-family: $sans-serif-family; 5 | -moz-osx-font-smoothing: grayscale; 6 | -webkit-font-smoothing: antialiased; 7 | } 8 | 9 | .main{ 10 | position: relative; 11 | background: $white-color; 12 | } 13 | /* Animations */ 14 | .nav-pills .nav-link, 15 | .navbar, 16 | .nav-tabs .nav-link, 17 | .sidebar .nav a, 18 | .sidebar .nav a i, 19 | .animation-transition-general, 20 | .tag, 21 | .tag [data-role="remove"], 22 | .animation-transition-general{ 23 | @include transition($general-transition-time, $transition-ease); 24 | } 25 | 26 | //transition for dropdown caret 27 | .dropdown-toggle:after, 28 | .bootstrap-switch-label:before, 29 | .caret{ 30 | @include transition($fast-transition-time, $transition-ease); 31 | } 32 | 33 | .dropdown-toggle[aria-expanded="true"]:after, 34 | a[data-toggle="collapse"][aria-expanded="true"] .caret, 35 | .card-collapse .card a[data-toggle="collapse"][aria-expanded="true"] i, 36 | .card-collapse .card a[data-toggle="collapse"].expanded i{ 37 | @include rotate-180(); 38 | } 39 | 40 | .button-bar{ 41 | display: block; 42 | position: relative; 43 | width: 22px; 44 | height: 1px; 45 | border-radius: 1px; 46 | background: $white-bg; 47 | 48 | & + .button-bar{ 49 | margin-top: 7px; 50 | } 51 | 52 | &:nth-child(2){ 53 | width: 17px; 54 | } 55 | } 56 | 57 | .caret{ 58 | display: inline-block; 59 | width: 0; 60 | height: 0; 61 | margin-left: 2px; 62 | vertical-align: middle; 63 | border-top: 4px dashed; 64 | border-top: 4px solid\9; 65 | border-right: 4px solid transparent; 66 | border-left: 4px solid transparent; 67 | } 68 | 69 | .pull-left{ 70 | float: left; 71 | } 72 | .pull-right{ 73 | float: right; 74 | } 75 | 76 | 77 | .offline-doc { 78 | .navbar.navbar-transparent{ 79 | padding-top: 25px; 80 | border-bottom: none; 81 | 82 | .navbar-minimize { 83 | display: none; 84 | } 85 | .navbar-brand, 86 | .collapse .navbar-nav .nav-link { 87 | color: $white-color !important; 88 | } 89 | } 90 | .footer { 91 | z-index: 3 !important; 92 | } 93 | .page-header{ 94 | .container { 95 | z-index: 3; 96 | } 97 | &:after { 98 | background-color: rgba(0, 0, 0, 0.5); 99 | content: ""; 100 | display: block; 101 | height: 100%; 102 | left: 0; 103 | position: absolute; 104 | top: 0; 105 | width: 100%; 106 | z-index: 2; 107 | } 108 | } 109 | } 110 | 111 | .fixed-plugin { 112 | .dropdown-menu li { 113 | padding: 2px !important; 114 | } 115 | } 116 | 117 | // badge color 118 | 119 | .badge{ 120 | &.badge-default{ 121 | @include badge-color($default-color); 122 | } 123 | &.badge-primary{ 124 | @include badge-color($primary-color); 125 | } 126 | &.badge-info{ 127 | @include badge-color($info-color); 128 | } 129 | &.badge-success{ 130 | @include badge-color($success-color); 131 | } 132 | &.badge-warning{ 133 | @include badge-color($warning-color); 134 | } 135 | &.badge-danger{ 136 | @include badge-color($danger-color); 137 | } 138 | &.badge-neutral{ 139 | @include badge-color($white-color); 140 | color: inherit; 141 | } 142 | } 143 | 144 | .card-user { 145 | form { 146 | .form-group { 147 | margin-bottom: 20px; 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/SidebarPlugin/SideBar.vue: -------------------------------------------------------------------------------- 1 | 19 | 94 | 130 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/demo.scss: -------------------------------------------------------------------------------- 1 | a{ 2 | text-decoration:none; 3 | } 4 | 5 | .tim-row { 6 | margin-bottom: 20px; 7 | } 8 | 9 | .tim-white-buttons { 10 | background-color: #777777; 11 | } 12 | 13 | .typography-line { 14 | padding-left: 25%; 15 | margin-bottom: 35px; 16 | position: relative; 17 | display: block; 18 | width: 100%; 19 | } 20 | 21 | .typography-line span { 22 | bottom: 10px; 23 | color: #c0c1c2; 24 | display: block; 25 | font-weight: 400; 26 | font-size: 13px; 27 | line-height: 13px; 28 | left: 0; 29 | position: absolute; 30 | width: 260px; 31 | text-transform: none; 32 | } 33 | 34 | .tim-row { 35 | padding-top: 60px; 36 | } 37 | 38 | .tim-row h3 { 39 | margin-top: 0; 40 | } 41 | 42 | .offline-doc .page-header { 43 | display: flex; 44 | align-items: center; 45 | } 46 | 47 | .offline-doc .footer { 48 | position: absolute; 49 | width: 100%; 50 | background: transparent; 51 | bottom: 0; 52 | color: #fff; 53 | z-index: 1; 54 | } 55 | 56 | #map { 57 | position: relative; 58 | width: 100%; 59 | height: 100vh; 60 | } 61 | 62 | .demo-iconshtml { 63 | font-size: 62.5%; 64 | } 65 | 66 | .demo-icons body { 67 | font-size: 1.6rem; 68 | font-family: sans-serif; 69 | color: #333333; 70 | background: white; 71 | } 72 | 73 | .demo-icons a { 74 | color: #608CEE; 75 | text-decoration: none; 76 | } 77 | 78 | .demo-icons header { 79 | text-align: center; 80 | padding: 100px 0 0; 81 | } 82 | 83 | .demo-icons header h1 { 84 | font-size: 2.8rem; 85 | } 86 | 87 | .demo-icons header p { 88 | font-size: 1.4rem; 89 | margin-top: 1em; 90 | } 91 | 92 | .demo-icons header a:hover { 93 | //text-decoration: underline; 94 | } 95 | 96 | .demo-icons .nc-icon { 97 | font-size: 34px; 98 | } 99 | 100 | .demo-icons section h2 { 101 | border-bottom: 1px solid #e2e2e2; 102 | padding: 0 0 1em .2em; 103 | margin-bottom: 1em; 104 | } 105 | 106 | .demo-icons ul { 107 | padding-left: 0; 108 | } 109 | 110 | .demo-icons ul::after { 111 | clear: both; 112 | content: ""; 113 | display: table; 114 | } 115 | 116 | .demo-icons ul li { 117 | width: 20%; 118 | float: left; 119 | padding: 16px 0; 120 | text-align: center; 121 | border-radius: .25em; 122 | -webkit-transition: background 0.2s; 123 | -moz-transition: background 0.2s; 124 | transition: background 0.2s; 125 | -webkit-user-select: none; 126 | -moz-user-select: none; 127 | -ms-user-select: none; 128 | user-select: none; 129 | overflow: hidden; 130 | } 131 | 132 | .demo-icons ul li:hover { 133 | background: #f4f4f4; 134 | } 135 | 136 | .demo-icons ul p, 137 | .demo-icons ul em, 138 | .demo-icons ul input { 139 | display: inline-block; 140 | font-size: 1rem; 141 | color: #999999; 142 | -webkit-user-select: auto; 143 | -moz-user-select: auto; 144 | -ms-user-select: auto; 145 | user-select: auto; 146 | white-space: nowrap; 147 | width: 100%; 148 | overflow: hidden; 149 | text-overflow: ellipsis; 150 | cursor: pointer; 151 | } 152 | 153 | .demo-icons ul p { 154 | padding: 20px 0 0; 155 | font-size: 12px; 156 | margin: 0; 157 | } 158 | 159 | .demo-icons ul p::selection, 160 | .demo-icons ul em::selection { 161 | background: #608CEE; 162 | color: #efefef; 163 | } 164 | 165 | .demo-icons ul em { 166 | font-size: 12px; 167 | } 168 | 169 | .demo-icons ul em::before { 170 | content: '['; 171 | } 172 | 173 | .demo-icons ul em::after { 174 | content: ']'; 175 | } 176 | 177 | .demo-icons ul input { 178 | text-align: center; 179 | background: transparent; 180 | border: none; 181 | box-shadow: none; 182 | outline: none; 183 | display: none; 184 | } 185 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_pagination.scss: -------------------------------------------------------------------------------- 1 | .pagination { 2 | .page-item .page-link { 3 | border: 0; 4 | border-radius: 30px !important; 5 | transition: all .3s; 6 | padding: 0px 11px; 7 | margin: 0 3px; 8 | min-width: 30px; 9 | text-align: center; 10 | height: 30px; 11 | line-height: 30px; 12 | color: $black-color; 13 | cursor: pointer; 14 | font-size: $font-size-base; 15 | text-transform: uppercase; 16 | background: transparent; 17 | outline: none; 18 | 19 | &:hover, 20 | &:focus { 21 | color: $black-color; 22 | background-color: $opacity-gray-3; 23 | border: none; 24 | } 25 | 26 | &:focus, 27 | &:active:focus { 28 | box-shadow: none; 29 | } 30 | } 31 | 32 | .arrow-margin-left, 33 | .arrow-margin-right { 34 | position: absolute; 35 | } 36 | 37 | .arrow-margin-right { 38 | right: 0; 39 | } 40 | 41 | .arrow-margin-left { 42 | left: 0; 43 | } 44 | 45 | .page-item.active > .page-link { 46 | color: $white-color; 47 | box-shadow: $box-shadow; 48 | 49 | &, 50 | &:focus, 51 | &:hover{ 52 | background-color: $primary-color; 53 | border-color: $primary-color; 54 | color: $white-color; 55 | } 56 | } 57 | 58 | .page-item.disabled > .page-link{ 59 | opacity: .5; 60 | } 61 | 62 | // Colors 63 | &.pagination-info{ 64 | .page-item.active > .page-link{ 65 | &, 66 | &:focus, 67 | &:hover{ 68 | background-color: $brand-info; 69 | border-color: $brand-info; 70 | } 71 | } 72 | } 73 | 74 | &.pagination-success{ 75 | .page-item.active > .page-link{ 76 | &, 77 | &:focus, 78 | &:hover{ 79 | background-color: $brand-success; 80 | border-color: $brand-success; 81 | } 82 | } 83 | } 84 | 85 | &.pagination-primary{ 86 | .page-item.active > .page-link{ 87 | &, 88 | &:focus, 89 | &:hover{ 90 | background-color: $brand-primary; 91 | border-color: $brand-primary; 92 | } 93 | } 94 | } 95 | 96 | &.pagination-warning{ 97 | .page-item.active > .page-link{ 98 | &, 99 | &:focus, 100 | &:hover{ 101 | background-color: $brand-warning; 102 | border-color: $brand-warning; 103 | } 104 | } 105 | } 106 | 107 | &.pagination-danger{ 108 | .page-item.active > .page-link{ 109 | &, 110 | &:focus, 111 | &:hover{ 112 | background-color: $brand-danger; 113 | border-color: $brand-danger; 114 | } 115 | } 116 | } 117 | 118 | &.pagination-neutral{ 119 | .page-item > .page-link{ 120 | color: $white-color; 121 | 122 | &:focus, 123 | &:hover{ 124 | background-color: $opacity-2; 125 | color: $white-color; 126 | } 127 | } 128 | 129 | .page-item.active > .page-link{ 130 | &, 131 | &:focus, 132 | &:hover{ 133 | background-color: $white-bg; 134 | border-color: $white-bg; 135 | color: $brand-primary; 136 | } 137 | } 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /frontend/public/img/laravel-vue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | background 5 | 6 | 7 | 8 | Layer 1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/config/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | import importlib.util 4 | import logging 5 | import logging.config 6 | 7 | ENVIRONMENT_VARIABLE = "BACKEND_SETTINGS" 8 | 9 | 10 | class Settings: 11 | def __init__(self, settings_module='config.local'): 12 | self._settings_module = settings_module 13 | self.environment = self._settings_module.split('.')[-1].lower() 14 | print(f"Loading settings for env: {self.environment}") 15 | 16 | self.DEMO_MODE = False 17 | self.DEBUG = False 18 | self.JWT_SECRET_KEY = 'tQjMCau8W939' 19 | 20 | self.SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") 21 | self.BCRYPT_LOG_ROUNDS = 4 22 | self.SQL_CONF = { 23 | 'user': 'queds', 24 | 'password': '', 25 | 'host': '', 26 | 'port': '5432', 27 | 'database': 'queds', 28 | 'options': {} 29 | } 30 | 31 | self.TELEGRAM_CONFIG = { 32 | "token": "", 33 | "chat": None 34 | } 35 | 36 | self.REDIS = { 37 | 'host': '0.0.0.0', 38 | 'port': 6379 39 | } 40 | 41 | mod = importlib.import_module(settings_module) 42 | for setting in dir(mod): 43 | if setting.startswith('_'): 44 | continue 45 | 46 | setting_value = getattr(mod, setting) 47 | 48 | if hasattr(self, setting): 49 | if isinstance(setting_value, dict): 50 | dst = getattr(self, setting) 51 | dst.update(setting_value) 52 | else: 53 | setattr(self, setting, setting_value) 54 | 55 | self.LOG_LEVEL = logging.DEBUG 56 | self.std_format = '%(asctime)s - %(levelname)s - %(name)s - %(message)s' 57 | self.enabled_handlers = ['default'] 58 | logging.config.dictConfig({ 59 | 'version': 1, 60 | 'disable_existing_loggers': False, 61 | 'filters': { 62 | }, 63 | 'formatters': { 64 | 'standard': { 65 | 'format': self.std_format 66 | } 67 | }, 68 | 'handlers': { 69 | 'default': { 70 | 'level': self.LOG_LEVEL, 71 | 'formatter': 'standard', 72 | 'class': 'logging.StreamHandler', 73 | 'filters': [] 74 | }, 75 | }, 76 | 'loggers': { 77 | '': { 78 | 'handlers': self.enabled_handlers, 79 | 'level': self.LOG_LEVEL, 80 | 'propagate': True 81 | }, 82 | 'urllib3': { 83 | 'handlers': self.enabled_handlers, 84 | 'level': logging.WARNING 85 | }, 86 | 'requests': { # disable requests library logging 87 | 'handlers': self.enabled_handlers, 88 | 'level': logging.WARNING 89 | }, 90 | 'curl_cffi.requests': { # disable requests library logging 91 | 'handlers': self.enabled_handlers, 92 | 'level': logging.WARNING 93 | }, 94 | 'sqlalchemy': { 95 | 'handlers': self.enabled_handlers, 96 | 'level': logging.ERROR 97 | }, 98 | 'sqlalchemy.engine': { 99 | 'handlers': self.enabled_handlers, 100 | 'level': logging.CRITICAL 101 | }, 102 | } 103 | }) 104 | 105 | 106 | env = os.environ.setdefault(ENVIRONMENT_VARIABLE, "config.local") 107 | 108 | settings = Settings(env) 109 | -------------------------------------------------------------------------------- /app/backend/finance_reader/entities/csv_reader/kucoin.py: -------------------------------------------------------------------------------- 1 | from finance_reader.entities.csv_reader import ExchangeCSVProcessor 2 | from models.dtos.exchange_dtos import Order, Transaction, OrderType 3 | from datetime import datetime 4 | 5 | 6 | class Kucoin(ExchangeCSVProcessor): 7 | 8 | def __init__(self): 9 | super(Kucoin, self).__init__() 10 | 11 | def process_csv(self, csv_data, account): 12 | self.log.info(f"Processing Kucoin CSV file for account {account.id}") 13 | lines = csv_data.splitlines() 14 | headers = lines[0].split(",") 15 | if len(headers) == 9: 16 | return [], self._process_transaction_csv(lines, account) 17 | 18 | orders = [] 19 | transactions = [] 20 | for line in lines[1:]: 21 | data = dict(zip(headers, line.split(","))) 22 | type = None 23 | if data.get('Side') == 'BUY': 24 | type = OrderType.BUY 25 | elif data.get('Side') == 'SELL': 26 | type = OrderType.SELL 27 | else: 28 | self.log.info(f"Unhandled type: {data.get('Remarks')}") 29 | 30 | order = Order() 31 | order.account_id = account.id 32 | order.external_id = data.get("Order ID") 33 | order.value_date = datetime.strptime(data.get("Filled Time(UTC)"), "%Y-%m-%d %H:%M:%S") 34 | order.pair = data.get("Symbol").replace("-", "/") 35 | order.amount = float(data.get("Order Amount")) 36 | order.type = type 37 | order.price = float(data.get("Order Price")) 38 | order.fee = float(data.get('Fee') or 0) 39 | orders.append(order) 40 | 41 | return orders, transactions 42 | 43 | def _process_transaction_csv(self, lines, account): 44 | transactions = [] 45 | headers = lines[0].split(",") 46 | for line in lines[1:]: 47 | data = dict(zip(headers, line.split(","))) 48 | 49 | if data.get('Status') != 'SUCCESS': 50 | continue 51 | 52 | type = None 53 | if data.get('Remarks') == 'Deposit': 54 | type = OrderType.DEPOSIT 55 | elif data.get('Type') == 'Transfer': 56 | type = OrderType.WITHDRAWAL 57 | else: 58 | self.log.info(f"Unhandled type: {data.get('Remarks')}") 59 | 60 | trans = Transaction() 61 | trans.account_id = account.id 62 | trans.value_date = datetime.strptime(data.get("Time(UTC)"), "%Y-%m-%d %H:%M:%S") 63 | trans.amount = abs(float(data.get("Amount"))) 64 | trans.currency = data.get("Coin") 65 | trans.type = type 66 | trans.rx_address = "" 67 | trans.fee = float(data.get("Fee") or 0) 68 | trans.external_id = f"{trans.value_date}_{trans.currency}_{trans.amount}" 69 | transactions.append(trans) 70 | return transactions 71 | 72 | def _process_converted_orders(self, csv_data, account): 73 | lines = csv_data.splitlines() 74 | headers = lines[0].split(",") 75 | 76 | orders = [] 77 | transactions = [] 78 | for line in lines[1:]: 79 | data = dict(zip(headers, line.split(","))) 80 | type = OrderType.BUY 81 | 82 | order = Order() 83 | order.account_id = account.id 84 | order.value_date = datetime.strptime(data.get("Time of Update(UTC)"), "%Y-%m-%d %H:%M:%S") 85 | order.pair = f'{data.get("Buy").split(" ")[1]}/{data.get("Sell").split(" ")[1]}' 86 | order.amount = float(data.get("Buy").split(" ")[0]) 87 | order.type = type 88 | order.price = float(float(data.get("Sell").split(" ")[0])/order.amount) 89 | order.fee = 0 90 | order.external_id = f"{order.value_date}_{order.pair}_{order.amount}" 91 | orders.append(order) 92 | 93 | return orders, transactions 94 | -------------------------------------------------------------------------------- /frontend/src/components/UIComponents/Cards/ChartCard.vue: -------------------------------------------------------------------------------- 1 | 52 | 128 | 130 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_tables.scss: -------------------------------------------------------------------------------- 1 | .table{ 2 | 3 | .img-wrapper{ 4 | width: 40px; 5 | height: 40px; 6 | border-radius: 50%; 7 | overflow: hidden; 8 | margin: 0 auto; 9 | } 10 | 11 | .img-row{ 12 | max-width: 60px; 13 | width: 60px; 14 | } 15 | 16 | .form-check{ 17 | margin: 0; 18 | 19 | & label .form-check-sign::before, 20 | & label .form-check-sign::after{ 21 | top: -17px; 22 | left: 4px; 23 | } 24 | } 25 | 26 | .btn{ 27 | margin: 0; 28 | } 29 | 30 | small,.small{ 31 | font-weight: 300; 32 | } 33 | 34 | .card-tasks .card-body &{ 35 | margin-bottom: 0; 36 | 37 | > thead > tr > th, 38 | > tbody > tr > th, 39 | > tfoot > tr > th, 40 | > thead > tr > td, 41 | > tbody > tr > td, 42 | > tfoot > tr > td{ 43 | padding-top: 0; 44 | padding-bottom: 0; 45 | } 46 | } 47 | 48 | > thead > tr > th{ 49 | font-size: 14px; 50 | font-weight: $font-weight-bold; 51 | padding-bottom: 0; 52 | text-transform: uppercase; 53 | border: 0; 54 | } 55 | 56 | .radio, 57 | .checkbox{ 58 | margin-top: 0; 59 | margin-bottom: 0; 60 | padding: 0; 61 | width: 15px; 62 | 63 | .icons{ 64 | position: relative; 65 | } 66 | 67 | label{ 68 | &:after, 69 | &:before{ 70 | top: -17px; 71 | left: -3px; 72 | } 73 | } 74 | } 75 | > thead > tr > th, 76 | > tbody > tr > th, 77 | > tfoot > tr > th, 78 | > thead > tr > td, 79 | > tbody > tr > td, 80 | > tfoot > tr > td{ 81 | padding: 12px 7px; 82 | vertical-align: middle; 83 | } 84 | 85 | .th-description{ 86 | max-width: 150px; 87 | } 88 | .td-price{ 89 | font-size: 26px; 90 | font-weight: $font-weight-light; 91 | margin-top: 5px; 92 | position: relative; 93 | top: 4px; 94 | text-align: right; 95 | } 96 | .td-total{ 97 | font-weight: $font-weight-bold; 98 | font-size: $font-size-h5; 99 | padding-top: 20px; 100 | text-align: right; 101 | } 102 | 103 | .td-actions .btn{ 104 | margin: 0px; 105 | & + .btn { 106 | margin-left: 5px; 107 | } 108 | } 109 | 110 | > tbody > tr{ 111 | position: relative; 112 | } 113 | } 114 | 115 | .table-shopping{ 116 | > thead > tr > th{ 117 | font-size: $font-size-h6; 118 | text-transform: uppercase; 119 | } 120 | > tbody > tr > td{ 121 | font-size: $font-paragraph; 122 | 123 | b{ 124 | display: block; 125 | margin-bottom: 5px; 126 | } 127 | } 128 | .td-name{ 129 | font-weight: $font-weight-normal; 130 | font-size: 1.5em; 131 | small{ 132 | color: $dark-gray; 133 | font-size: 0.75em; 134 | font-weight: $font-weight-light; 135 | } 136 | } 137 | .td-number{ 138 | font-weight: $font-weight-light; 139 | font-size: $font-size-h4; 140 | } 141 | .td-name{ 142 | min-width: 200px; 143 | } 144 | .td-number{ 145 | text-align: right; 146 | min-width: 170px; 147 | 148 | small{ 149 | margin-right: 3px; 150 | } 151 | } 152 | 153 | .img-container{ 154 | width: 120px; 155 | max-height: 160px; 156 | overflow: hidden; 157 | display: block; 158 | 159 | img{ 160 | width: 100%; 161 | } 162 | } 163 | } 164 | 165 | .table-responsive{ 166 | overflow: scroll; 167 | padding-bottom: 10px; 168 | } 169 | 170 | #tables .table-responsive{ 171 | margin-bottom: 30px; 172 | } 173 | 174 | .table-hover>tbody>tr:hover{ 175 | background-color: #f5f5f5; 176 | } 177 | -------------------------------------------------------------------------------- /frontend/src/assets/sass/paper/_typography.scss: -------------------------------------------------------------------------------- 1 | button, 2 | input, 3 | optgroup, 4 | select, 5 | textarea{ 6 | font-family: $sans-serif-family; 7 | } 8 | h1,h2,h3,h4,h5,h6{ 9 | font-weight: $font-weight-normal; 10 | } 11 | 12 | a{ 13 | color: $primary-color; 14 | &:hover, 15 | &:focus{ 16 | color: $primary-color; 17 | } 18 | } 19 | h1, .h1 { 20 | font-size: $font-size-h1; 21 | line-height: 1.15; 22 | margin-bottom: $margin-base-vertical * 2; 23 | 24 | small{ 25 | font-weight: $font-weight-bold; 26 | text-transform: uppercase; 27 | opacity: .8; 28 | } 29 | } 30 | h2, .h2{ 31 | font-size: $font-size-h2; 32 | margin-bottom: $margin-base-vertical * 2; 33 | } 34 | h3, .h3{ 35 | font-size: $font-size-h3; 36 | margin-bottom: $margin-base-vertical * 2; 37 | line-height: 1.4em; 38 | } 39 | h4, .h4{ 40 | font-size: $font-size-h4; 41 | line-height: 1.45em; 42 | margin-top: $margin-base-vertical * 2; 43 | margin-bottom: $margin-base-vertical; 44 | 45 | & + .category, 46 | &.title + .category{ 47 | margin-top: -10px; 48 | } 49 | } 50 | h5, .h5 { 51 | font-size: $font-size-h5; 52 | line-height: 1.4em; 53 | margin-bottom: 15px; 54 | } 55 | h6, .h6{ 56 | font-size: $font-size-h6; 57 | font-weight: $font-weight-bold; 58 | text-transform: uppercase; 59 | } 60 | p{ 61 | &.description{ 62 | font-size: 1.14em; 63 | } 64 | } 65 | 66 | // i.fa{ 67 | // font-size: 18px; 68 | // position: relative; 69 | // top: 1px; 70 | // } 71 | 72 | .title{ 73 | font-weight: $font-weight-bold; 74 | 75 | &.title-up{ 76 | text-transform: uppercase; 77 | 78 | a{ 79 | color: $black-color; 80 | text-decoration: none; 81 | } 82 | } 83 | & + .category{ 84 | margin-top: -10px; 85 | } 86 | } 87 | 88 | .description, 89 | .card-description, 90 | .footer-big p, 91 | .card .footer .stats{ 92 | color: $dark-gray; 93 | font-weight: $font-weight-light; 94 | } 95 | .category, 96 | .card-category{ 97 | text-transform: capitalize; 98 | font-weight: $font-weight-normal; 99 | color: $dark-gray; 100 | font-size: $font-size-mini; 101 | } 102 | 103 | .card-category{ 104 | font-size: $font-size-h6; 105 | } 106 | 107 | .text-primary, 108 | a.text-primary:focus, a.text-primary:hover { 109 | color: $brand-primary !important; 110 | } 111 | .text-info, 112 | a.text-info:focus, a.text-info:hover { 113 | color: $brand-info !important; 114 | } 115 | .text-success, 116 | a.text-success:focus, a.text-success:hover { 117 | color: $brand-success !important; 118 | } 119 | .text-warning, 120 | a.text-warning:focus, a.text-warning:hover { 121 | color: $brand-warning !important; 122 | } 123 | .text-danger, 124 | a.text-danger:focus, a.text-danger:hover { 125 | color: $brand-danger !important; 126 | } 127 | 128 | .text-gray, 129 | a.text-gray:focus, a.text-gray:hover{ 130 | color: $light-gray !important; 131 | } 132 | 133 | 134 | .blockquote{ 135 | border-left: none; 136 | border: 1px solid $default-color; 137 | padding: 20px; 138 | font-size: $font-size-blockquote; 139 | line-height: 1.8; 140 | 141 | small{ 142 | color: $default-color; 143 | font-size: $font-size-small; 144 | text-transform: uppercase; 145 | } 146 | 147 | &.blockquote-primary{ 148 | border-color: $primary-color; 149 | color: $primary-color; 150 | 151 | small{ 152 | color: $primary-color; 153 | } 154 | } 155 | 156 | &.blockquote-danger{ 157 | border-color: $danger-color; 158 | color: $danger-color; 159 | 160 | small{ 161 | color: $danger-color; 162 | } 163 | } 164 | 165 | &.blockquote-white{ 166 | border-color: $opacity-8; 167 | color: $white-color; 168 | 169 | small{ 170 | color: $opacity-8; 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /frontend/src/pages/auth/Register.vue: -------------------------------------------------------------------------------- 1 | 35 | 100 | 102 | --------------------------------------------------------------------------------