├── .gitbook.yaml
├── docs
├── troubleshooting
│ ├── error-syncing.md
│ ├── README.md
│ └── migrate-to-v1.md
├── .gitbook
│ └── assets
│ │ ├── screen-shot-2021-09-20-at-11.03.05-am.png
│ │ ├── screen-shot-2021-09-20-at-5.02.15-pm.png
│ │ └── screen-shot-2021-10-01-at-10.30.24-am.png
├── developers
│ └── setup-a-dev-build
│ │ ├── packaging-the-application.md
│ │ └── README.md
├── frequently-asked-questions
│ ├── metrics.md
│ ├── active-deals.md
│ └── google-sheets.md
├── SUMMARY.md
├── configuration
│ └── initial-setup.md
└── README.md
├── .DS_Store
├── assets
├── palette.pdf
├── icons
│ ├── icon.ico
│ ├── icon.png
│ ├── icons.icns
│ ├── 256x256.png
│ ├── 512x512.png
│ └── icon-old.icns
├── icons.iconset
│ ├── icon_16x16.png
│ ├── icon_32x32.png
│ ├── icon_128x128.png
│ ├── icon_16x16@2x.png
│ ├── icon_256x256.png
│ ├── icon_32x32@2x.png
│ ├── icon_512x512.png
│ ├── icon_128x128@2x.png
│ ├── icon_256x256@2.png
│ └── icon_512x512@2x.png
└── assets
│ ├── backwardClock.svg
│ ├── coffee.svg
│ ├── tradingview-v2-svgrepo-com.svg
│ ├── moon.svg
│ ├── pieChart.svg
│ ├── activeDeals.svg
│ ├── cog.svg
│ ├── power.svg
│ ├── botmanager.svg
│ └── sun.svg
├── src
├── app
│ ├── Components
│ │ ├── Charts
│ │ │ ├── Pie
│ │ │ │ └── index.ts
│ │ │ ├── Line
│ │ │ │ ├── index.ts
│ │ │ │ └── Components
│ │ │ │ │ └── PairSelector.tsx
│ │ │ ├── Area
│ │ │ │ └── index.ts
│ │ │ ├── Speedometer
│ │ │ │ ├── index.ts
│ │ │ │ └── MaxRiskPercent.tsx
│ │ │ ├── Scatter
│ │ │ │ └── index.ts
│ │ │ ├── DataCards
│ │ │ │ ├── CustomToolTip.ts
│ │ │ │ ├── Card.scss
│ │ │ │ ├── metrics
│ │ │ │ │ ├── ActiveDeals.tsx
│ │ │ │ │ ├── EnabledBots.tsx
│ │ │ │ │ ├── TotalDeals.tsx
│ │ │ │ │ ├── DropCoverage.tsx
│ │ │ │ │ ├── TotalBoughtVolume.tsx
│ │ │ │ │ ├── MaxDca.tsx
│ │ │ │ │ ├── TotalProfit.tsx
│ │ │ │ │ ├── AverageDailyProfit.tsx
│ │ │ │ │ ├── TotalDayProfit.tsx
│ │ │ │ │ ├── TotalUnrealizedProfit.tsx
│ │ │ │ │ ├── ActiveDealReserve.tsx
│ │ │ │ │ ├── AverageDealHours.tsx
│ │ │ │ │ ├── TotalInDeals.tsx
│ │ │ │ │ ├── TotalBankRoll.tsx
│ │ │ │ │ ├── MaxRiskPercent.tsx
│ │ │ │ │ └── TotalRoi.tsx
│ │ │ │ ├── Card.tsx
│ │ │ │ └── index.ts
│ │ │ ├── Bar
│ │ │ │ └── index.ts
│ │ │ └── formatting.tsx
│ │ ├── icons
│ │ │ ├── Loading
│ │ │ │ ├── Loading.scss
│ │ │ │ └── Loading.tsx
│ │ │ ├── BackwardClock.tsx
│ │ │ ├── Index.ts
│ │ │ ├── Coffee.tsx
│ │ │ ├── TradingViewLogo.tsx
│ │ │ ├── Moon.tsx
│ │ │ ├── PieChart.tsx
│ │ │ ├── Cog.tsx
│ │ │ ├── ActiveDeals.tsx
│ │ │ ├── BotPlanner.tsx
│ │ │ └── Sun.tsx
│ │ ├── Sidebar
│ │ │ ├── Components
│ │ │ │ ├── index.ts
│ │ │ │ ├── SidebarLink.tsx
│ │ │ │ └── SidebarNav.tsx
│ │ │ ├── svg
│ │ │ │ ├── backwardClock.svg
│ │ │ │ ├── coffee.svg
│ │ │ │ ├── moon.svg
│ │ │ │ ├── pieChart.svg
│ │ │ │ ├── cog.svg
│ │ │ │ ├── botmanager.svg
│ │ │ │ └── sun.svg
│ │ │ ├── DisplaySwitcher.tsx
│ │ │ ├── Sidebar.scss
│ │ │ └── Sidebar.tsx
│ │ ├── Buttons
│ │ │ ├── Index.tsx
│ │ │ ├── ToggleRefresh.scss
│ │ │ └── UpdateData.tsx
│ │ ├── Selectors
│ │ │ ├── index.ts
│ │ │ └── AccountSelector.tsx
│ │ └── DataTable
│ │ │ ├── Index.tsx
│ │ │ ├── Components
│ │ │ ├── index.ts
│ │ │ └── OpenIn3Commas.tsx
│ │ │ ├── Table.scss
│ │ │ └── FormatDeals.tsx
│ ├── Pages
│ │ ├── DailyStats
│ │ │ ├── Components
│ │ │ │ ├── KPIs
│ │ │ │ │ └── index.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── 3Commas
│ │ │ │ │ └── type_dailydashboard.ts
│ │ │ └── DailyStats.scss
│ │ ├── BotPlanner
│ │ │ ├── Components
│ │ │ │ ├── index.ts
│ │ │ │ ├── SaveButton.tsx
│ │ │ │ └── Risk.tsx
│ │ │ └── BotPlanner.scss
│ │ ├── ActiveDeals
│ │ │ └── Components
│ │ │ │ ├── SubrowTabs
│ │ │ │ ├── Index.tsx
│ │ │ │ └── Orders.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── EditDeal.scss
│ │ │ │ ├── NotificationsSettings.tsx
│ │ │ │ └── Subrow.tsx
│ │ ├── Stats
│ │ │ ├── Type_stats.tsx
│ │ │ ├── Views
│ │ │ │ ├── Index.ts
│ │ │ │ ├── SummaryStatistics.tsx
│ │ │ │ └── RiskMonitor.tsx
│ │ │ ├── Components
│ │ │ │ ├── Index.tsx
│ │ │ │ ├── NoData.tsx
│ │ │ │ ├── ViewRenderer.tsx
│ │ │ │ └── StatFiltersDiv.tsx
│ │ │ ├── Stats.tsx
│ │ │ └── Stats.scss
│ │ ├── Settings
│ │ │ ├── Components
│ │ │ │ ├── Index.tsx
│ │ │ │ ├── FeedbackOrBugButton.tsx
│ │ │ │ ├── StartDatePicker.tsx
│ │ │ │ └── WriteModeSettings.tsx
│ │ │ ├── Redux
│ │ │ │ └── settingsSlice.ts
│ │ │ └── Settings.scss
│ │ ├── Index.ts
│ │ └── MainWindow.tsx
│ ├── Repositories
│ │ ├── Types
│ │ │ ├── Binance.ts
│ │ │ └── GithubRelease.ts
│ │ ├── interfaces
│ │ │ ├── Deals.ts
│ │ │ ├── Binance.ts
│ │ │ ├── General.ts
│ │ │ ├── API.ts
│ │ │ ├── Database.ts
│ │ │ ├── Config.ts
│ │ │ ├── Repository.ts
│ │ │ └── index.ts
│ │ └── Impl
│ │ │ ├── Binance.ts
│ │ │ ├── electron
│ │ │ ├── Base.ts
│ │ │ ├── Deals.ts
│ │ │ ├── index.ts
│ │ │ ├── API.ts
│ │ │ ├── Config.ts
│ │ │ └── Database.ts
│ │ │ └── General.ts
│ ├── Features
│ │ ├── Profiles
│ │ │ ├── Profiles.scss
│ │ │ └── Components
│ │ │ │ ├── Index.tsx
│ │ │ │ └── ProfileNameEditor.tsx
│ │ ├── Index.tsx
│ │ ├── UpdateBanner
│ │ │ ├── UpdateBanner.scss
│ │ │ ├── UpdateApiFetch.ts
│ │ │ ├── redux
│ │ │ │ └── bannerSlice.ts
│ │ │ └── UpdateBanner.tsx
│ │ ├── CoinPriceHeader
│ │ │ ├── BinanceApi.ts
│ │ │ └── CoinPriceHeader.scss
│ │ ├── 3Commas
│ │ │ ├── queryString.ts
│ │ │ ├── Type_3Commas.ts
│ │ │ ├── DataQueries
│ │ │ │ └── accounts.ts
│ │ │ └── 3Commas.ts
│ │ ├── ToastNotifications
│ │ │ └── ToastNotification.tsx
│ │ ├── Changelog
│ │ │ └── changelogModal.scss
│ │ └── LocalStorage
│ │ │ └── LocalStorage.ts
│ ├── redux
│ │ ├── hooks.ts
│ │ ├── store.ts
│ │ ├── globalFunctions.ts
│ │ └── threeCommas
│ │ │ └── initialState.ts
│ └── app.tsx
├── types
│ ├── Date.ts
│ ├── Charts.ts
│ ├── config.ts
│ └── preload.ts
├── index.html
├── main
│ ├── Database
│ │ └── helper.ts
│ ├── Config
│ │ └── migrations
│ │ │ └── 2.0.0.ts
│ ├── precheck.ts
│ └── 3Commas
│ │ └── types
│ │ ├── GridBots.ts
│ │ └── Bots.ts
├── utils
│ └── number_formatting.ts
└── renderer.tsx
├── .gitignore
├── nodemon.json
├── .vscode
└── settings.json
├── webpack.config.js
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── build.yml
├── webpack.electron.js
├── webpack.preload.js
├── scripts
├── notarize.js
└── getReleaseCounts.js
├── FORMULA_DESCRIPTIONS.md
└── webpack.react.js
/.gitbook.yaml:
--------------------------------------------------------------------------------
1 | root: ./docs/
--------------------------------------------------------------------------------
/docs/troubleshooting/error-syncing.md:
--------------------------------------------------------------------------------
1 | # Error Syncing
2 |
3 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/.DS_Store
--------------------------------------------------------------------------------
/assets/palette.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/palette.pdf
--------------------------------------------------------------------------------
/assets/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons/icon.ico
--------------------------------------------------------------------------------
/assets/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons/icon.png
--------------------------------------------------------------------------------
/assets/icons/icons.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons/icons.icns
--------------------------------------------------------------------------------
/assets/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons/256x256.png
--------------------------------------------------------------------------------
/assets/icons/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons/512x512.png
--------------------------------------------------------------------------------
/assets/icons/icon-old.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons/icon-old.icns
--------------------------------------------------------------------------------
/assets/icons.iconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons.iconset/icon_16x16.png
--------------------------------------------------------------------------------
/assets/icons.iconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons.iconset/icon_32x32.png
--------------------------------------------------------------------------------
/src/app/Components/Charts/Pie/index.ts:
--------------------------------------------------------------------------------
1 | import BalancePie from "./BalancePie";
2 |
3 | export {
4 | BalancePie
5 | }
--------------------------------------------------------------------------------
/src/app/Pages/DailyStats/Components/KPIs/index.ts:
--------------------------------------------------------------------------------
1 | import RoiMetrics from "./Roi";
2 |
3 | export {
4 | RoiMetrics
5 | }
--------------------------------------------------------------------------------
/assets/icons.iconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons.iconset/icon_128x128.png
--------------------------------------------------------------------------------
/assets/icons.iconset/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons.iconset/icon_16x16@2x.png
--------------------------------------------------------------------------------
/assets/icons.iconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons.iconset/icon_256x256.png
--------------------------------------------------------------------------------
/assets/icons.iconset/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons.iconset/icon_32x32@2x.png
--------------------------------------------------------------------------------
/assets/icons.iconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons.iconset/icon_512x512.png
--------------------------------------------------------------------------------
/assets/icons.iconset/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons.iconset/icon_128x128@2x.png
--------------------------------------------------------------------------------
/assets/icons.iconset/icon_256x256@2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons.iconset/icon_256x256@2.png
--------------------------------------------------------------------------------
/assets/icons.iconset/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/assets/icons.iconset/icon_512x512@2x.png
--------------------------------------------------------------------------------
/src/app/Repositories/Types/Binance.ts:
--------------------------------------------------------------------------------
1 |
2 | export type BinanceTicketPrice = {
3 | "symbol": string,
4 | "price": string
5 | }
--------------------------------------------------------------------------------
/src/app/Repositories/interfaces/Deals.ts:
--------------------------------------------------------------------------------
1 | export default interface DealsRepository {
2 | update(profileData: any, deal: any): any;
3 | }
--------------------------------------------------------------------------------
/src/app/Components/Charts/Line/index.ts:
--------------------------------------------------------------------------------
1 | import PairPerformanceByDate from "./PairByDay";
2 |
3 | export {
4 | PairPerformanceByDate
5 | }
--------------------------------------------------------------------------------
/src/app/Components/Charts/Area/index.ts:
--------------------------------------------------------------------------------
1 | import SummaryProfitByDay from "./SummaryProfitByDay";
2 |
3 | export {
4 | SummaryProfitByDay
5 | }
--------------------------------------------------------------------------------
/src/app/Components/Charts/Speedometer/index.ts:
--------------------------------------------------------------------------------
1 | import MaxRiskSpeedometer from "./MaxRiskPercent";
2 |
3 | export {
4 | MaxRiskSpeedometer
5 | };
--------------------------------------------------------------------------------
/src/app/Components/icons/Loading/Loading.scss:
--------------------------------------------------------------------------------
1 | #loadingIcon svg path,
2 | #loadingIcon svg rect{
3 | fill: var(--color-text-lightbackground)
4 | }
--------------------------------------------------------------------------------
/docs/.gitbook/assets/screen-shot-2021-09-20-at-11.03.05-am.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/docs/.gitbook/assets/screen-shot-2021-09-20-at-11.03.05-am.png
--------------------------------------------------------------------------------
/docs/.gitbook/assets/screen-shot-2021-09-20-at-5.02.15-pm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/docs/.gitbook/assets/screen-shot-2021-09-20-at-5.02.15-pm.png
--------------------------------------------------------------------------------
/docs/.gitbook/assets/screen-shot-2021-10-01-at-10.30.24-am.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coltoneshaw/3cpm/HEAD/docs/.gitbook/assets/screen-shot-2021-10-01-at-10.30.24-am.png
--------------------------------------------------------------------------------
/src/app/Features/Profiles/Profiles.scss:
--------------------------------------------------------------------------------
1 | .profileDiv{
2 |
3 | position: absolute;
4 | top: 2em;
5 | left: 5em;
6 | width: 250px;
7 | align-items: center;
8 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | node_modules/
3 | node/
4 | /node_modules
5 | src/main-app.js
6 | dist/*
7 | # build/*
8 | /src/main-app.js
9 | release/*
10 | commit.sh
11 | .env
--------------------------------------------------------------------------------
/src/app/Components/Sidebar/Components/index.ts:
--------------------------------------------------------------------------------
1 | import SidebarLink from "./SidebarLink";
2 | import SidebarNav from "./SidebarNav";
3 |
4 | export {
5 | SidebarLink,
6 | SidebarNav
7 | }
--------------------------------------------------------------------------------
/src/app/Repositories/interfaces/Binance.ts:
--------------------------------------------------------------------------------
1 | import type { binance } from "@/types/preload";
2 |
3 |
4 | export default interface BinanceRepository {
5 | coinData: binance['coinData']
6 | }
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/app/Components/Buttons/Index.tsx:
--------------------------------------------------------------------------------
1 | import UpdateDataButton from "./UpdateData";
2 | import ToggleRefreshButton from "./ToggleRefresh";
3 |
4 | export {
5 | UpdateDataButton,
6 | ToggleRefreshButton
7 | }
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": [
3 | "src/main.ts",
4 | "src/preload.ts",
5 | "src/electron/*"
6 | ],
7 | "exec": "webpack --config ./webpack.electron.js && electron .",
8 | "ext": "ts"
9 | }
--------------------------------------------------------------------------------
/src/app/Features/Profiles/Components/Index.tsx:
--------------------------------------------------------------------------------
1 | import ProfileNameEditor from "./ProfileNameEditor";
2 | import ProfileSwitcher from "./ProfileSwitcher";
3 |
4 | export {
5 | ProfileNameEditor,
6 | ProfileSwitcher
7 | }
--------------------------------------------------------------------------------
/src/app/Pages/BotPlanner/Components/index.ts:
--------------------------------------------------------------------------------
1 | import DataTable from "./DataTable";
2 | import Risk from "./Risk";
3 | import SaveButton from "./SaveButton";
4 | export {
5 | DataTable,
6 | Risk,
7 | SaveButton
8 | }
--------------------------------------------------------------------------------
/src/app/Components/Selectors/index.ts:
--------------------------------------------------------------------------------
1 | import AccountSelector from "./AccountSelector";
2 | import AllCurrencySelector from "./AllCurrencySelector";
3 |
4 | export {
5 | AccountSelector,
6 | AllCurrencySelector
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/src/app/Features/Index.tsx:
--------------------------------------------------------------------------------
1 | import ChangelogModal from "./Changelog/ChangelogModal";
2 | import ToastNotifcations from './ToastNotifications/ToastNotification'
3 |
4 | export {
5 | ChangelogModal,
6 | ToastNotifcations
7 | }
--------------------------------------------------------------------------------
/src/app/Pages/ActiveDeals/Components/SubrowTabs/Index.tsx:
--------------------------------------------------------------------------------
1 | import DCA from "./DCA";
2 | import Orders from "./Orders";
3 | import OrderTimeline from "./Timeline";
4 |
5 | export {
6 | DCA,
7 | OrderTimeline,
8 | Orders
9 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "git.ignoreLimitWarning": true,
3 | "liveServer.settings.port": 5501,
4 | "cSpell.words": [
5 | "preload",
6 | "uuidv"
7 | ],
8 | "typescript.tsdk": "node_modules/typescript/lib"
9 | }
--------------------------------------------------------------------------------
/src/app/Components/Charts/Scatter/index.ts:
--------------------------------------------------------------------------------
1 | import DealPerformanceBubble from './DealPerformanceBubble'
2 | import BotPerformanceBubble from './BotPerformanceBubble'
3 |
4 |
5 |
6 | export {
7 | DealPerformanceBubble,
8 | BotPerformanceBubble
9 | }
--------------------------------------------------------------------------------
/src/types/Date.ts:
--------------------------------------------------------------------------------
1 | class DateRange {
2 | public from: Date | null = null;
3 | public to: Date | null = null;
4 | }
5 |
6 | type utcDateRange = {
7 | utcEndDate: number;
8 | utcStartDate: number;
9 | }
10 |
11 | export {DateRange, utcDateRange}
--------------------------------------------------------------------------------
/src/app/Pages/Stats/Type_stats.tsx:
--------------------------------------------------------------------------------
1 | type pageIds = 'summary-stats' | 'risk-monitor' | 'performance-monitor' ;
2 | type pageNames = 'Summary Statistics' | 'Risk Monitor' | 'Performance Monitor';
3 |
4 | type buttonElements = {
5 | name: pageNames,
6 | id: pageIds
7 | }[]
--------------------------------------------------------------------------------
/src/app/Repositories/interfaces/General.ts:
--------------------------------------------------------------------------------
1 | import type { general, pm } from "@/types/preload";
2 |
3 |
4 | export interface GeneralRepository {
5 | openLink: general['openLink']
6 | }
7 |
8 | export interface PmRepository {
9 | versions: pm['versions']
10 | }
--------------------------------------------------------------------------------
/src/app/Pages/ActiveDeals/Components/index.ts:
--------------------------------------------------------------------------------
1 | import DealsTable from "./DealsTable";
2 | import NotificationsSettings from "./NotificationsSettings";
3 | import SubRowAsync from "./Subrow";
4 |
5 | export {
6 | NotificationsSettings,
7 | SubRowAsync,
8 | DealsTable
9 | }
--------------------------------------------------------------------------------
/src/app/Components/Buttons/ToggleRefresh.scss:
--------------------------------------------------------------------------------
1 | .ToggleRefreshButton .MuiLinearProgress-determinate {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | bottom: 0;
7 | width: 100%;
8 | height: 100%;
9 | opacity: 0.1;
10 | border-radius: 4px;
11 | }
--------------------------------------------------------------------------------
/src/app/Pages/Stats/Views/Index.ts:
--------------------------------------------------------------------------------
1 | import RiskMonitor from "./RiskMonitor";
2 | import SummaryStatistics from "./SummaryStatistics"
3 | import PerformanceMonitor from './PerformanceMonitor'
4 |
5 | export {
6 | RiskMonitor,
7 | SummaryStatistics,
8 | PerformanceMonitor
9 | }
--------------------------------------------------------------------------------
/src/app/Repositories/interfaces/API.ts:
--------------------------------------------------------------------------------
1 | import type {api} from '@/types/preload'
2 |
3 | export default interface APIRepository {
4 | update: api['update'];
5 | updateBots: api['updateBots'];
6 | getAccountData: api['getAccountData']
7 | getDealOrders: api['getDealOrders']
8 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const electronConfigs = require('./webpack.electron.js');
2 | const preloadConfig = require('./webpack.preload.js');
3 |
4 | const reactConfigs = require('./webpack.react.js');
5 |
6 | module.exports = [
7 | electronConfigs,
8 | preloadConfig,
9 | reactConfigs
10 | ];
11 |
--------------------------------------------------------------------------------
/docs/developers/setup-a-dev-build/packaging-the-application.md:
--------------------------------------------------------------------------------
1 | # Packaging the application
2 |
3 | 1. Run steps 1 - 5 of setting the dev server up.
4 | 2. Package the application
5 |
6 | ```text
7 | npm run build
8 | ```
9 |
10 | 1. The relevant build files will be located in `./release`
11 |
12 |
--------------------------------------------------------------------------------
/src/app/Repositories/interfaces/Database.ts:
--------------------------------------------------------------------------------
1 | import type {database, tableNames} from '@/types/preload';
2 |
3 | export default interface DBRepository {
4 | query: database['query'];
5 | update: database['update'];
6 | upsert: database['upsert'];
7 | run: database['run'];
8 | deleteAllData: database['deleteAllData'];
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/Components/DataTable/Index.tsx:
--------------------------------------------------------------------------------
1 | import formatDeals from "./FormatDeals";
2 | import CustomTable from "./Table";
3 | import {Bots_EditableCell, Settings_EditableCell, OpenIn3Commas} from './Components'
4 |
5 | export {
6 | formatDeals,
7 | CustomTable,
8 | Bots_EditableCell,
9 | Settings_EditableCell,
10 | OpenIn3Commas
11 | }
--------------------------------------------------------------------------------
/src/app/Repositories/Impl/Binance.ts:
--------------------------------------------------------------------------------
1 | import BaseElectronRepository from "@/app/Repositories/Impl/electron/Base";
2 | import { BinanceRepository } from '@/app/Repositories/interfaces';
3 |
4 | export default class BaseBinanceRepository extends BaseElectronRepository implements BinanceRepository{
5 | coinData = () => this.mainPreload.binance.coinData()
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/Repositories/Impl/electron/Base.ts:
--------------------------------------------------------------------------------
1 |
2 | // This gets pulled from the preload.ts file opening up the electron api to the app window.
3 |
4 | export default class BaseElectronRepository {
5 | protected mainPreload: Window["mainPreload"];
6 |
7 | constructor(mainPreload: Window["mainPreload"]) {
8 | this.mainPreload = mainPreload;
9 | }
10 | }
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 3C Portfolio Manager
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/app/Components/DataTable/Components/index.ts:
--------------------------------------------------------------------------------
1 | import OpenIn3Commas from "./OpenIn3Commas";
2 | import {Bots_EditableCell, Settings_EditableCell} from "./EditableCell";
3 | import { ColumnSelector, useColumnSelector } from "./ColumnSelector";
4 | export {
5 | OpenIn3Commas,
6 | Bots_EditableCell,
7 | Settings_EditableCell,
8 | ColumnSelector, useColumnSelector
9 | }
--------------------------------------------------------------------------------
/src/app/redux/hooks.ts:
--------------------------------------------------------------------------------
1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
2 | import type { RootState, AppDispatch } from './store'
3 |
4 | // Use throughout your app instead of plain `useDispatch` and `useSelector`
5 | export const useAppDispatch = () => useDispatch()
6 | export const useAppSelector: TypedUseSelectorHook = useSelector
--------------------------------------------------------------------------------
/src/app/Repositories/interfaces/Config.ts:
--------------------------------------------------------------------------------
1 | import type { config } from "@/types/preload";
2 |
3 |
4 | export default interface ConfigRepository {
5 | get: config['get'];
6 | profile: config['profile'];
7 | getProfile: config['getProfile']
8 | reset: config['reset']
9 | set: config['set']
10 | // setProfile: config['setProfile']
11 | bulk: config['bulk']
12 | }
13 |
--------------------------------------------------------------------------------
/docs/troubleshooting/README.md:
--------------------------------------------------------------------------------
1 | # Troubleshooting
2 |
3 | ## Logs
4 |
5 | ### Frontend
6 |
7 | * Menu Bar > View > Toogle Developer Tools > Console
8 |
9 | ### Backend
10 |
11 | * **Windows:** `%USERPROFILE%\AppData\Roaming\3C Portfolio Manager\logs\main.log`
12 | * **Mac**: `~/Library/Logs/3C Portfolio Manager/main.log`
13 | * **Linux**: `~/.config/3C Portfolio Manager/logs/main.log`
14 |
15 |
--------------------------------------------------------------------------------
/src/app/Pages/ActiveDeals/Components/EditDeal.scss:
--------------------------------------------------------------------------------
1 | div.editDealModal {
2 | background-color: var(--color-background-light);
3 | color: var(--color-text-lightbackground);
4 |
5 | .MuiDialogContentText-root{
6 | color: var(--color-text-lightbackground);
7 | padding: 1rem;
8 | }
9 |
10 | .MuiAutocomplete-input {
11 | color: var(--color-text-lightbackground)
12 | }
13 | }
--------------------------------------------------------------------------------
/assets/assets/backwardClock.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/assets/assets/coffee.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/Pages/Stats/Components/Index.tsx:
--------------------------------------------------------------------------------
1 | import NoData from "./NoData";
2 | import RoiCards from "./RoiCards";
3 | import SpeedometerDiv from "./SpeedometerDiv";
4 | import {ViewRenderer, useViewRenderer} from "./ViewRenderer";
5 | import StatFiltersDiv from './StatFiltersDiv'
6 | export {
7 | NoData,
8 | RoiCards,
9 | SpeedometerDiv,
10 | ViewRenderer,
11 | useViewRenderer,
12 | StatFiltersDiv
13 | }
--------------------------------------------------------------------------------
/src/app/Pages/Stats/Components/NoData.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NoData = () => {
4 |
5 | return (
6 | <>
7 | {/* Error loading chart */}
8 | It appears that no data was found. This most likely due to no data returned based on the selected filters. Adjust your currency, account, or start date.
9 | >
10 | )
11 | }
12 |
13 | export default NoData;
--------------------------------------------------------------------------------
/src/app/Components/Sidebar/svg/backwardClock.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/app/Components/Sidebar/svg/coffee.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/Pages/Settings/Components/Index.tsx:
--------------------------------------------------------------------------------
1 | import ApiSettings from "./ApiSettings";
2 | import ReservedBankroll from "./ReservedBankroll";
3 | import SaveDeleteButtons from "./SaveDeleteButtons";
4 | import StartDatePicker from "./StartDatePicker";
5 | import CurrencySelector from "./CurrencySelector";
6 |
7 | export {
8 | ApiSettings,
9 | CurrencySelector,
10 | ReservedBankroll,
11 | SaveDeleteButtons,
12 | StartDatePicker
13 | }
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/CustomToolTip.ts:
--------------------------------------------------------------------------------
1 | import withStyles from '@mui/styles/withStyles';
2 | import Tooltip from '@mui/material/Tooltip';
3 |
4 | const CardTooltip = withStyles(() => ({
5 | tooltip: {
6 | backgroundColor: '#f5f5f9',
7 | color: 'rgba(0, 0, 0, 0.87)',
8 | maxWidth: 220,
9 | fontSize: '.9em',
10 | fontWeight: 300,
11 | border: '1px solid #dadde9',
12 | },
13 | }))(Tooltip);
14 |
15 | export default CardTooltip
--------------------------------------------------------------------------------
/docs/frequently-asked-questions/metrics.md:
--------------------------------------------------------------------------------
1 | # Metrics
2 |
3 | ### Why does my profit or bankroll not match 3Commas?
4 |
5 | Usually, this has to do with filters. Within the 3C Portfolio Manager you're able to decide what accounts, currencies, and start dates for your data. This means there is the possibility you're filtering out data that would be shown on 3Commas. By default 3Commas will display 100% of your accounts and funds.
6 |
7 | You can adjust your filters within the 3C Portfolio Manager settings.
8 |
9 |
--------------------------------------------------------------------------------
/src/app/Features/UpdateBanner/UpdateBanner.scss:
--------------------------------------------------------------------------------
1 | .update-mainDiv {
2 | width: 100vw;
3 | position: absolute;
4 | top: 0;
5 | z-index: 1000;
6 | display: flex;
7 | flex-direction: row;
8 | justify-content: center;
9 | align-items: center;
10 |
11 | p {
12 | padding-right: 1em;
13 | }
14 |
15 | a {
16 | text-decoration: underline;
17 | cursor: pointer;
18 | }
19 |
20 | .closeIcon{
21 | cursor: pointer;
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/src/app/Pages/BotPlanner/BotPlanner.scss:
--------------------------------------------------------------------------------
1 | .button-botPlanner {
2 | width: 200px !important;
3 | // margin: 5px 5px 10px 5px !important;
4 | // align-self: flex-end !important;
5 | margin: 5px !important
6 | }
7 |
8 | .updatebutton {
9 | width: 40px !important;
10 | }
11 |
12 | .riskDiv {
13 | display: flex;
14 | flex-direction: row;
15 | justify-content: space-between;
16 | flex-wrap: wrap;
17 | width: 100%;
18 | gap: 2em;
19 | }
20 |
21 | #noBorder {
22 | border: none;
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/src/app/Pages/DailyStats/Components/index.ts:
--------------------------------------------------------------------------------
1 | import PairBar from "./Charts/PairBar";
2 | import { queryDealByPairByDay, queryDealByBotByDay, queryProfitDataByDay, getTotalProfit, getActiveDealsFunction } from "@/app/Pages/DailyStats/Components/3Commas/dailyDashboard";
3 | import CopyTodayStatsButton from "./CopyTodayStatsButton";
4 | export {
5 | PairBar,
6 | queryDealByPairByDay,
7 | queryDealByBotByDay,
8 | queryProfitDataByDay,
9 | getTotalProfit,
10 | getActiveDealsFunction,
11 | CopyTodayStatsButton
12 | }
--------------------------------------------------------------------------------
/src/app/Pages/Settings/Components/FeedbackOrBugButton.tsx:
--------------------------------------------------------------------------------
1 | import {Button} from "@mui/material";
2 | import React from "react";
3 |
4 | import { openLink } from "@/utils/helperFunctions";
5 |
6 | const FeedbackOrBugButton = () => {
7 |
8 | return (
9 | openLink('https://github.com/coltoneshaw/3c-portfolio-manager#feedback-or-bug-submission')} style={{ margin: '1em', borderRight: 'none' }} >Leave Feedback / Report a bug
10 | )
11 |
12 |
13 | }
14 |
15 | export default FeedbackOrBugButton;
16 |
--------------------------------------------------------------------------------
/src/app/Repositories/interfaces/Repository.ts:
--------------------------------------------------------------------------------
1 | import {
2 | APIRepository, DealsRepository,
3 | DBRepository, BinanceRepository, ConfigRepository, PmRepository, GeneralRepository} from '@/app/Repositories/interfaces';
4 |
5 | export default interface Repository {
6 | readonly Deals: DealsRepository;
7 | readonly API: APIRepository;
8 | readonly Database: DBRepository;
9 | readonly Binance: BinanceRepository;
10 | readonly Config: ConfigRepository;
11 | readonly General: GeneralRepository;
12 | readonly Pm: PmRepository;
13 | }
--------------------------------------------------------------------------------
/src/app/Repositories/Impl/electron/Deals.ts:
--------------------------------------------------------------------------------
1 | import BaseElectronRepository from "@/app/Repositories/Impl/electron/Base";
2 | import {UpdateDealRequest} from "@/main/3Commas/types/Deals";
3 | import {Type_Profile} from "@/types/config";
4 | import { DealsRepository } from '@/app/Repositories/interfaces';
5 |
6 | export default class ElectronDealsRepository extends BaseElectronRepository implements DealsRepository {
7 | update(profileData: Type_Profile, deal: UpdateDealRequest): any {
8 | return this.mainPreload.deals.update(profileData, deal)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/Repositories/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | import APIRepository from "./API";
2 | import BinanceRepository from "./Binance";
3 | import ConfigRepository from "./Config";
4 | import DBRepository from "./Database";
5 | import DealsRepository from "./Deals";
6 | import Repository from "./Repository";
7 | import {PmRepository, GeneralRepository} from "./General"
8 |
9 | export {
10 | APIRepository,
11 | ConfigRepository,
12 | DBRepository,
13 | DealsRepository,
14 | Repository,
15 | PmRepository,
16 | BinanceRepository,
17 | GeneralRepository
18 | }
--------------------------------------------------------------------------------
/src/app/Components/icons/BackwardClock.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const BackwardClock = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | export default BackwardClock;
--------------------------------------------------------------------------------
/src/app/Repositories/Impl/General.ts:
--------------------------------------------------------------------------------
1 | import BaseElectronRepository from "@/app/Repositories/Impl/electron/Base";
2 | import { GeneralRepository, PmRepository } from '@/app/Repositories/interfaces';
3 |
4 | export class BaseGeneralRepository extends BaseElectronRepository implements GeneralRepository {
5 | openLink = ( link:string) => {
6 | this.mainPreload.general.openLink(link)
7 | }
8 | }
9 |
10 | export class BasePmRepository extends BaseElectronRepository implements PmRepository {
11 | versions = () => {
12 | return this.mainPreload.pm.versions()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/assets/assets/tradingview-v2-svgrepo-com.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/app/Pages/Index.ts:
--------------------------------------------------------------------------------
1 | import BotPlannerPage from "@/app/Pages/BotPlanner/BotPlanner"
2 | import TradingViewPage from "@/app/Pages/TradingView/TradingView"
3 | import SettingsPage from "@/app/Pages/Settings/Settings"
4 | import StatsPage from "@/app/Pages/Stats/Stats"
5 | import ActiveDealsPage from "@/app/Pages/ActiveDeals/ActiveDeals";
6 | import MainWindow from "./MainWindow";
7 | import DailyStats from '@/app/Pages/DailyStats/DailyStats'
8 |
9 | export {
10 | BotPlannerPage,
11 | TradingViewPage,
12 | SettingsPage,
13 | StatsPage,
14 | ActiveDealsPage,
15 | MainWindow,
16 | DailyStats
17 | }
--------------------------------------------------------------------------------
/src/app/Repositories/Impl/electron/index.ts:
--------------------------------------------------------------------------------
1 | import ElectronAPIRepository from "@/app/Repositories/Impl/electron/API";
2 | import ElectronBinanceRepository from "@/app/Repositories/Impl/Binance";
3 | import ElectronConfigRepository from "@/app/Repositories/Impl/electron/Config";
4 | import ElectronDBRepository from "@/app/Repositories/Impl/electron/Database";
5 | import ElectronDealsRepository from "@/app/Repositories/Impl/electron/Deals";
6 |
7 | export {
8 | ElectronAPIRepository,
9 | ElectronDealsRepository,
10 | ElectronDBRepository,
11 | ElectronConfigRepository,
12 | ElectronBinanceRepository
13 | }
--------------------------------------------------------------------------------
/src/app/Components/Charts/Bar/index.ts:
--------------------------------------------------------------------------------
1 | import DealSoUtilizationBar from './DealSoUtilizationBar';
2 | import SoDistribution from './SoDistribution'
3 | import DealAllocationBar from './DealAllocation';
4 | import PairPerformanceBar from './PairPerformanceBar';
5 | import BotPerformanceBar from './BotPerformanceBar';
6 | import ProfitByDay from "./ProfitByDay";
7 | import SoDealDistribution from "./SoDealDistribution"
8 |
9 |
10 |
11 | export {
12 | DealSoUtilizationBar,
13 | SoDistribution,
14 | DealAllocationBar,
15 | PairPerformanceBar,
16 | BotPerformanceBar,
17 | ProfitByDay,
18 | SoDealDistribution
19 | }
--------------------------------------------------------------------------------
/src/app/Features/UpdateBanner/UpdateApiFetch.ts:
--------------------------------------------------------------------------------
1 | import fetch from 'electron-fetch'
2 |
3 | import type { Type_GithubRelease } from '@/app/Repositories/Types/GithubRelease'
4 |
5 |
6 | const fetchVersions = async () => {
7 | try {
8 | let response = await fetch('https://api.github.com/repos/coltoneshaw/3c-portfolio-manager/releases?per_page=5',
9 | {
10 | method: 'GET',
11 | timeout: 30000,
12 | })
13 |
14 | return await response.json()
15 | } catch (e) {
16 | console.log(e);
17 | return false
18 | }
19 |
20 | }
21 |
22 |
23 | export {
24 | fetchVersions
25 | }
--------------------------------------------------------------------------------
/src/app/Components/icons/Index.ts:
--------------------------------------------------------------------------------
1 | import BotPlannerIcon from "./BotPlanner";
2 | import Coffee from "./Coffee";
3 | import Cog from "./Cog";
4 | import Moon from "./Moon";
5 | import Sun from "./Sun";
6 | import PieChart from "./PieChart";
7 | import BackwardClock from './BackwardClock';
8 | import ActiveDealsIcon from "./ActiveDeals";
9 | import LoaderIcon from "./Loading/Loading";
10 | import TradingViewLogo from "./TradingViewLogo";
11 |
12 | export {
13 | BotPlannerIcon,
14 | Coffee,
15 | Cog,
16 | Moon,
17 | Sun,
18 | PieChart,
19 | BackwardClock,
20 | ActiveDealsIcon,
21 | LoaderIcon,
22 | TradingViewLogo
23 | }
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/Card.scss:
--------------------------------------------------------------------------------
1 | .dataCard {
2 | width: 125px;
3 | text-align: center;
4 | color: var(--color-text-lightbackground);
5 | padding: .5em !important;
6 | height: 65px;
7 | display: flex;
8 | flex-direction: column;
9 | justify-content: center;
10 | flex-grow: 1;
11 | }
12 |
13 | .dataCard h2 {
14 | font-size: 1.5em;
15 | margin: 0;
16 | font-weight: 700;
17 | opacity: .9;
18 |
19 | }
20 |
21 | .dataCard h4 {
22 | font-size: .65em;
23 | font-weight: 400;
24 | margin: 0;
25 | text-transform: capitalize;
26 | letter-spacing: .3px;
27 | padding-bottom: 2px;
28 | }
--------------------------------------------------------------------------------
/src/app/Components/icons/Coffee.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Coffee = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | )
14 | }
15 |
16 | export default Coffee;
--------------------------------------------------------------------------------
/src/app/Features/CoinPriceHeader/BinanceApi.ts:
--------------------------------------------------------------------------------
1 | import fetch from 'electron-fetch'
2 | import { BinanceTicketPrice } from '@/app/Repositories/Types/Binance'
3 |
4 |
5 | const fetchCoinPricesBinance = async () => {
6 |
7 |
8 | try {
9 | let response = await fetch('https://api.binance.com/api/v3/ticker/price',
10 | {
11 | method: 'GET',
12 | timeout: 30000,
13 | })
14 |
15 | return await response.json()
16 | } catch (e) {
17 | console.log(e);
18 | return false
19 | }
20 |
21 | }
22 |
23 |
24 | export {
25 | fetchCoinPricesBinance
26 | }
--------------------------------------------------------------------------------
/src/app/Components/Sidebar/Components/SidebarLink.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Tooltip from '@mui/material/Tooltip';
3 |
4 | interface Props {
5 | // In your case
6 | Icon: React.ComponentType,
7 | name: string,
8 | onClick: any
9 | }
10 |
11 | const SidebarLink = ({ Icon, name, onClick }: Props) => {
12 |
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 |
24 | }
25 |
26 | export default SidebarLink;
--------------------------------------------------------------------------------
/src/main/Database/helper.ts:
--------------------------------------------------------------------------------
1 | import { chooseDatabase } from './database'
2 |
3 | const query = async (profileId: string, query: string): Promise => {
4 | const db = chooseDatabase(profileId)
5 | const row = db.prepare(query)
6 | return row.all()
7 | }
8 |
9 |
10 | const run = (profileId: string, query: string): void => {
11 | const db = chooseDatabase(profileId)
12 | const stmt = db.prepare(query);
13 | stmt.run()
14 | }
15 |
16 | function normalizeData(data: any) {
17 | if (typeof data == 'string') return data.replaceAll('?', '')
18 | if (typeof data == 'boolean') return (data) ? 1 : 0;
19 |
20 | return data;
21 | }
22 |
23 |
24 | export { query, run, normalizeData }
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/ActiveDeals.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 |
6 |
7 | interface Type_Card {
8 | metric: number
9 | }
10 |
11 | /**
12 | *
13 | * @param metric - accepts the activeDealCount metric from the global data store.
14 | */
15 | const Card_ActiveDeals = ({metric}:Type_Card) => {
16 |
17 | const title = "Active Deals"
18 | const message = descriptions.calculations.activeDeals
19 | const key = title.replace(/\s/g, '')
20 | return ( )
21 |
22 | }
23 |
24 | export default Card_ActiveDeals;
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: coltoneshaw
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/webpack.electron.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | externals: {'better-sqlite3': 'commonjs2 better-sqlite3'},
5 | // Build Mode
6 | mode: 'development',
7 | // Electron Entrypoint
8 | entry: './src/main/main.ts',
9 | target: 'electron-main',
10 | resolve: {
11 | alias: {
12 | ['@']: path.resolve(__dirname, 'src'),
13 | ['#']: path.resolve(__dirname, '.')
14 |
15 | },
16 | extensions: ['.tsx', '.ts', '.js'],
17 | },
18 | module: {
19 | rules: [{
20 | test: /\.ts$/,
21 | include: /src/,
22 | use: [{ loader: 'ts-loader' }]
23 | }]
24 | },
25 | output: {
26 | path: __dirname + '/dist',
27 | filename: 'main.js'
28 | }
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/EnabledBots.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 |
6 |
7 | interface Type_Card {
8 | metric: number
9 | }
10 |
11 | /**
12 | *
13 | * @param metric - accepts the botCount metric. This is should be locally filtered and is the total number of enabled bots.
14 | */
15 | const Card_EnabledBots = ({ metric }: Type_Card) => {
16 |
17 | const title = "Enabled Bots"
18 | const message = descriptions.calculations.activeBots
19 | const key = title.replace(/\s/g, '')
20 | return ( )
21 | }
22 |
23 | export default Card_EnabledBots;
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/TotalDeals.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import { parseNumber } from "@/utils/number_formatting"
6 |
7 |
8 | interface Type_Card {
9 | metric: number
10 | }
11 |
12 | /**
13 | *
14 | * @param metric - accepts the totalDeals metric from the global data store.
15 | */
16 | const Card_TotalDeals = ({metric}:Type_Card) => {
17 |
18 | const title = "Total Deals"
19 | const message = descriptions.metrics.totalDeals
20 | const key = title.replace(/\s/g, '')
21 | return ( )
22 |
23 | }
24 |
25 | export default Card_TotalDeals;
--------------------------------------------------------------------------------
/src/app/Components/Sidebar/DisplaySwitcher.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { Moon, Sun } from '@/app/Components/icons/Index';
4 |
5 | import { useThemeProvidor } from '@/app/Context/ThemeEngine';
6 |
7 | const DisplaySwitcher = () => {
8 |
9 | const [ display, changeDisplay ] = useState(false)
10 | const { changeTheme } = useThemeProvidor()
11 |
12 |
13 | const displaySwitch = () => {
14 | changeDisplay(!display)
15 | changeTheme()
16 | }
17 |
18 | return (
19 |
20 | { (display) ? : }
21 |
22 | )
23 | }
24 |
25 | export default DisplaySwitcher;
--------------------------------------------------------------------------------
/src/app/Features/3Commas/queryString.ts:
--------------------------------------------------------------------------------
1 |
2 | import { Type_ReservedFunds, Type_Profile } from '@/types/config'
3 |
4 |
5 | export const getFiltersQueryString = (profileData: Type_Profile) => {
6 | const { general: { defaultCurrency }, statSettings: { reservedFunds, startDate }, id } = profileData
7 |
8 | const currencyString = (defaultCurrency) ? defaultCurrency.map((b: string) => "'" + b + "'") : ""
9 | const startString = startDate
10 | const accountIdString = reservedFunds.filter((account: Type_ReservedFunds) => account.is_enabled).map((account: Type_ReservedFunds) => account.id)
11 |
12 | return {
13 | currencyString,
14 | accountIdString,
15 | startString,
16 | currentProfileID: id
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/Components/Sidebar/Sidebar.scss:
--------------------------------------------------------------------------------
1 | .sidebarOption svg,
2 | #displaySwitcher svg,
3 | .icon {
4 | fill: var(--color-secondary-light50);
5 | max-height: 25px;
6 | max-width: 25px;
7 | }
8 |
9 | .sidebarOption {
10 | align-self: center;
11 | text-align: center;
12 | width: 100%;
13 | margin: 15px 0 15px 0;
14 | display: inline;
15 | }
16 |
17 |
18 | .sidebarOption .active svg,
19 | .sidebarOption:hover svg,
20 | #displaySwitcher:hover svg {
21 | fill: var(--color-secondary);
22 | }
23 |
24 |
25 | .sidebar-column{
26 | flex-basis: 50%;
27 | }
28 |
29 |
30 | #sidebar {
31 | z-index: 1;
32 | padding-top: 25px;
33 | display: flex;
34 | flex-direction: column;
35 | justify-content: flex-start;
36 | width: 60px;
37 | }
--------------------------------------------------------------------------------
/webpack.preload.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 |
4 | module.exports = {
5 | externals: {'better-sqlite3': 'commonjs2 better-sqlite3', '3commas-api-node': 'commonjs2 3commas-api-node' },
6 | // Build Mode
7 | mode: 'development',
8 | // Electron Entrypoint
9 | entry: './src/preload.ts',
10 | target: 'electron-main',
11 | resolve: {
12 | alias: {
13 | ['@']: path.resolve(__dirname, 'src'),
14 | ['#']: path.resolve(__dirname, '.')
15 |
16 | },
17 | extensions: ['.tsx', '.ts', '.js'],
18 | },
19 | module: {
20 | rules: [{
21 | test: /\.ts$/,
22 | include: /src/,
23 | use: [{ loader: 'ts-loader' }]
24 | }]
25 | },
26 | output: {
27 | path: __dirname + '/dist',
28 | filename: 'preload.js'
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/DropCoverage.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import { parseNumber } from "@/utils/number_formatting"
6 |
7 |
8 |
9 | interface Type_Card {
10 | metric: number
11 | }
12 |
13 | /**
14 | *
15 | * @param metric - accepts the dropCoverage metric calculated locally.
16 | */
17 | const Card_DropCoverage = ({metric}:Type_Card) => {
18 |
19 | const title = "Drop Coverage %"
20 | const message = descriptions.calculations.dropCoverage
21 | const key = title.replace(/\s/g, '')
22 | return ( )
23 | }
24 |
25 | export default Card_DropCoverage;
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/TotalBoughtVolume.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import { parseNumber } from "@/utils/number_formatting"
6 |
7 |
8 | interface Type_Card {
9 | metric: number
10 | }
11 |
12 | /**
13 | *
14 | * @param metric - accepts the boughtVolume metric from the global data store.
15 | */
16 | const Card_TotalBoughtVolume = ({metric}:Type_Card) => {
17 |
18 | const title = "Total Bought Volume"
19 | const message = descriptions.metrics.totalBoughtVolume
20 | const key = title.replace(/\s/g, '')
21 | return ( )
22 |
23 | }
24 |
25 | export default Card_TotalBoughtVolume;
--------------------------------------------------------------------------------
/docs/frequently-asked-questions/active-deals.md:
--------------------------------------------------------------------------------
1 | # Active Deals
2 |
3 | ### Why do my active SOs not match 3C?
4 |
5 | This is a fun one. It seems that how 3Commas handles Max Safety Trades is not how you'd expect. You can fill 5 SOs, have 1 active SO but manually set your MSTC to 0. This means that when attempting to calculate the max deal funds within the application it stops at the MSTC value, causing a mismatch in max deal funds on 3cpm and on 3commas. To mitigate this we manually update this value in the app to be the max of either MSTC or filled SOs + active SOs.
6 |
7 | ### What does auto sync do?
8 |
9 | Auto sync will update your deals and accounts every 15 seconds with information directly from 3Commas. Additionally, it turns on the ability to have push notifications about your deals closing!
10 |
11 |
--------------------------------------------------------------------------------
/assets/assets/moon.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/app/Features/UpdateBanner/redux/bannerSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | export type banner = 'updateVersion' | '' | 'apiError'
4 |
5 | const initialState = {
6 | show: false,
7 | message: '',
8 | type: ''
9 | }
10 |
11 |
12 | export const bannerSlice = createSlice({
13 | name: 'banner',
14 | initialState,
15 | reducers: {
16 | updateBannerData: (state, action: {payload: {show: boolean, message: string, type: banner}}) => {
17 | const {show, message, type} = action.payload
18 | state.show = show;
19 | state.message = message
20 | state.type = type
21 | }
22 | }
23 | })
24 |
25 | export const {
26 | updateBannerData
27 | } = bannerSlice.actions;
28 |
29 | export default bannerSlice.reducer
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/MaxDca.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import {formatCurrency, supportedCurrencies} from'@/utils/granularity'
6 |
7 | interface Type_Card {
8 | metric: number,
9 | currency: (keyof typeof supportedCurrencies)[]
10 | }
11 |
12 | /**
13 | *
14 | * @param metric - accepts the `totalMaxRisk` metric from the global data store.
15 | */
16 | const Card_MaxDca = ({metric, currency }:Type_Card) => {
17 |
18 | const title = "Max DCA"
19 | const message = descriptions.calculations.maxDca
20 | const key = title.replace(/\s/g, '')
21 |
22 | return ( )
23 |
24 | }
25 |
26 | export default Card_MaxDca;
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: coltoneshaw
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 | **Additional context**
31 | Add any other context about the problem here.
32 |
--------------------------------------------------------------------------------
/src/app/Components/Sidebar/svg/moon.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/TotalProfit.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import {formatCurrency, supportedCurrencies} from'@/utils/granularity'
6 |
7 | interface Type_Card {
8 | metric: number,
9 | currency: (keyof typeof supportedCurrencies)[]
10 | }
11 | /**
12 | *
13 | * @param metric - accepts the `totalProfit` metric from the global data store.
14 | */
15 | const Card_TotalProfit = ({metric, currency }:Type_Card) => {
16 |
17 | const title = "Total Profit"
18 | const message = descriptions.calculations.totalProfit
19 | const key = title.replace(/\s/g, '')
20 |
21 | return ( )
22 |
23 | }
24 |
25 | export default Card_TotalProfit;
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/Card.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement, ReactFragment } from 'react';
2 |
3 | import CardTooltip from './CustomToolTip';
4 |
5 | import './Card.scss';
6 | import { AnyStyledComponent } from 'styled-components';
7 | // " is calculated by taking your total DCA Max Risk of 35,746 and dividing it by your current bankroll of 14,644."
8 |
9 | const Card = ({ title = "", metric, message = "", SubMetric }: { title: string, metric: {metric: string | number, symbol: string}, message?: string, SubMetric?: AnyStyledComponent,}) => {
10 |
11 | return (
12 | {title} {message} > } >
13 |
14 |
{title}
15 | {metric.metric}
16 |
17 | )
18 |
19 | }
20 |
21 | export default Card;
--------------------------------------------------------------------------------
/src/app/Components/icons/TradingViewLogo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const TradingViewLogo = () => {
4 |
5 | return (
6 |
7 |
8 |
9 |
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | export default TradingViewLogo;
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/AverageDailyProfit.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import {formatCurrency, supportedCurrencies} from'@/utils/granularity'
6 |
7 |
8 | interface Type_Card {
9 | metric: number,
10 | currency: (keyof typeof supportedCurrencies)[]
11 | }
12 |
13 | /**
14 | *
15 | * @param metric - accepts the averageDailyProfit metric from the global data store.
16 | */
17 | const Card_AverageDailyProfit = ({metric, currency}:Type_Card) => {
18 |
19 | const title = "Average Daily Profit"
20 | const message = descriptions.metrics.averageDailyProfit
21 | const key = title.replace(/\s/g, '')
22 | return ( )
23 | }
24 |
25 | export default Card_AverageDailyProfit;
--------------------------------------------------------------------------------
/src/app/Pages/DailyStats/Components/3Commas/type_dailydashboard.ts:
--------------------------------------------------------------------------------
1 |
2 | type queryDealByPairByDayQuery = {
3 | pair: string[],
4 | averageHourlyProfitPercent: number,
5 | totalProfit: number,
6 | numberOfDeals: number,
7 | boughtVolume: number,
8 | averageDealHours: number
9 | }
10 |
11 |
12 | type queryDealByPairByDayReturn = queryDealByPairByDayQuery & {
13 | percentTotalVolume: number;
14 | percentTotalProfit: number;
15 | }
16 |
17 |
18 | type botQueryDealByDayQuery = {
19 | bot_name: string,
20 | bot_id: number,
21 | averageHourlyProfitPercent: number,
22 | totalProfit: number,
23 | numberOfDeals: number,
24 | boughtVolume: number,
25 | averageDealHours: number
26 | }
27 |
28 |
29 | type botQueryDealByDayReturn = botQueryDealByDayQuery & {
30 | percentTotalVolume: number;
31 | percentTotalProfit: number;
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/Components/icons/Moon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Moon = () => {
4 |
5 | return (
6 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default Moon;
--------------------------------------------------------------------------------
/src/app/redux/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit'
2 | import configSlice from '@/app/redux/config/configSlice'
3 | import threeCommasSlice from '@/app/redux/threeCommas/threeCommasSlice'
4 | import settingsSlice from '@/app/Pages/Settings/Redux/settingsSlice'
5 | import bannerSlice from '@/app/Features/UpdateBanner/redux/bannerSlice'
6 |
7 | const store = configureStore({
8 | reducer: {
9 | config: configSlice,
10 | threeCommas: threeCommasSlice,
11 | settings: settingsSlice,
12 | banner: bannerSlice
13 | },
14 | devTools: true
15 | })
16 |
17 | // Infer the `RootState` and `AppDispatch` types from the store itself
18 | export type RootState = ReturnType
19 |
20 | // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
21 | export type AppDispatch = typeof store.dispatch
22 |
23 | export default store;
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/TotalDayProfit.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import {formatCurrency, supportedCurrencies} from'@/utils/granularity'
6 |
7 | interface Type_Card {
8 | metric: number,
9 | currency: (keyof typeof supportedCurrencies)[]
10 | }
11 |
12 | /**
13 | *
14 | * @param metric - accepts today's profit which can be calculated with `profitData[profitData.length - 1].profit` from the global data store.
15 | */
16 | const Card_TotalDayProfit = ({metric, currency }:Type_Card) => {
17 |
18 | const title = "Today's Profit"
19 | const message = descriptions.metrics.todaysProfit
20 | const key = title.replace(/\s/g, '')
21 |
22 | return ( )
23 |
24 | }
25 |
26 | export default Card_TotalDayProfit;
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/TotalUnrealizedProfit.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import {formatCurrency, supportedCurrencies} from'@/utils/granularity'
6 |
7 | interface Type_Card {
8 | metric: number,
9 | currency: (keyof typeof supportedCurrencies)[]
10 | }
11 |
12 | /**
13 | *
14 | * @param metric - accepts the unrealized profit metric which is ( deal.take_profit / 100 ) * deal.bought_volume)
15 | */
16 | const Card_TotalUnrealizedProfit = ({metric, currency }:Type_Card) => {
17 |
18 | const title = "Unrealized Profit"
19 | const message = descriptions.metrics.totalUnrealizedProfit
20 | const key = title.replace(/\s/g, '')
21 |
22 | return ( )
23 |
24 | }
25 |
26 | export default Card_TotalUnrealizedProfit;
--------------------------------------------------------------------------------
/src/app/Components/Sidebar/Components/SidebarNav.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Tooltip from '@mui/material/Tooltip';
3 | import { NavLink } from 'react-router-dom'
4 | import { setStorageItem, storageItem } from '@/app/Features/LocalStorage/LocalStorage';
5 |
6 | interface Props {
7 | // In your case
8 | Icon: React.ComponentType,
9 | name: string,
10 | link: string,
11 | }
12 |
13 | const SidebarNav= ({ Icon, name, link }: Props) => {
14 |
15 |
16 | return (
17 | setStorageItem(storageItem.navigation.homePage, link)}>
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 |
28 | }
29 |
30 | export default SidebarNav;
--------------------------------------------------------------------------------
/docs/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 |
3 | * [3C Portfolio Manager](README.md)
4 | * [Frequently Asked Questions](frequently-asked-questions/README.md)
5 | * [Metrics](frequently-asked-questions/metrics.md)
6 | * [Active Deals](frequently-asked-questions/active-deals.md)
7 | * [Google Sheets](frequently-asked-questions/google-sheets.md)
8 |
9 | ## Configuration
10 |
11 | * [Initial Setup](configuration/initial-setup.md)
12 | * [Profiles](configuration/profiles.md)
13 |
14 | ## Developers
15 |
16 | * [3Commas API](developers/3commas-api.md)
17 | * [Setup a dev build](developers/setup-a-dev-build/README.md)
18 | * [Packaging the application](developers/setup-a-dev-build/packaging-the-application.md)
19 |
20 | ***
21 |
22 | * [Changelog](changelog.md)
23 | * [Troubleshooting](troubleshooting/README.md)
24 | * [Migrate to v1](troubleshooting/migrate-to-v1.md)
25 | * [Error Syncing](troubleshooting/error-syncing.md)
26 | * [Donate](https://www.buymeacoffee.com/ColtonS)
27 |
--------------------------------------------------------------------------------
/src/main/Config/migrations/2.0.0.ts:
--------------------------------------------------------------------------------
1 | import { TconfigValues, Type_Profile } from "@/types/config";
2 | import log from 'electron-log';
3 | import { checkOrMakeTables } from "@/main/Database/database";
4 | import fsExtra from 'fs-extra';
5 | import path from "path";
6 | import { app } from "electron";
7 |
8 | const appDataPath = app.getPath('userData');
9 |
10 |
11 |
12 |
13 | export const convertToProfileDatabases = async (profileIds: string[]) => {
14 | if (!profileIds || profileIds.length === 0) return
15 |
16 | await fsExtra.mkdir(path.join(appDataPath, 'databases'));
17 | for (let profileId of profileIds) {
18 | log.info(`Converting ${profileId} to it's own database`)
19 | checkOrMakeTables(profileId)
20 | }
21 | fsExtra.remove(path.join(appDataPath, 'db.sqlite3'), err => {
22 | if (err) return log.error('Unable to delete original database file', err)
23 | log.info('Deleted db.sqlite3 file from user directory')
24 | })
25 |
26 | }
--------------------------------------------------------------------------------
/assets/assets/pieChart.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/app/Repositories/Impl/electron/API.ts:
--------------------------------------------------------------------------------
1 | import { Type_UpdateFunction } from "@/types/3Commas";
2 | import { Type_Profile } from "@/types/config";
3 | import BaseElectronRepository from "@/app/Repositories/Impl/electron/Base";
4 | import { APIRepository } from '@/app/Repositories/interfaces';
5 |
6 | export default class ElectronAPIRepository extends BaseElectronRepository implements APIRepository {
7 | update = (type: string, options: Type_UpdateFunction, profileData: Type_Profile) => this.mainPreload.api.update(type, options, profileData)
8 | updateBots = (profileData: Type_Profile) => this.mainPreload.api.updateBots(profileData)
9 | getAccountData = (profileData?: Type_Profile, key?: string, secret?: string, mode?: string) => {
10 | return this.mainPreload.api.getAccountData(profileData, key, secret, mode)
11 | }
12 | getDealOrders = (profileData: Type_Profile, dealID: number) => {
13 | return this.mainPreload.api.getDealOrders(profileData, dealID)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/Components/Sidebar/svg/pieChart.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/app/Features/ToastNotifications/ToastNotification.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Snackbar, IconButton} from '@mui/material';
3 |
4 | import CloseIcon from '@mui/icons-material/Close';
5 |
6 | interface Type_Snack {
7 | open: boolean
8 | handleClose: any
9 | message: string
10 | }
11 |
12 | const ToastNotifcations = ({open, handleClose, message}: Type_Snack) => {
13 | return (
14 | <>
15 |
26 |
27 |
28 |
29 | >
30 | }
31 | />
32 | >
33 | );
34 | }
35 |
36 | export default ToastNotifcations;
37 |
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/ActiveDealReserve.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import {formatCurrency, supportedCurrencies} from'@/utils/granularity'
6 |
7 | interface Type_Card {
8 | metric: number,
9 | currency: (keyof typeof supportedCurrencies)[]
10 | }
11 |
12 | /**
13 | *
14 | * @param metric - accepts a sum of the active deal reserves. Which is just a total of `actual_usd_profit` together.
15 | */
16 | const Card_ActiveDealReserve = ({metric, currency }:Type_Card) => {
17 |
18 | const title = "Active Deal Reserve"
19 | const message = descriptions.metrics.activeDealReserves
20 | const key = title.replace(/\s/g, '')
21 |
22 | // overriding the currency here since the values are all currently in USD.
23 | currency = ['USD']
24 | return ( )
25 | }
26 |
27 | export default Card_ActiveDealReserve;
--------------------------------------------------------------------------------
/scripts/notarize.js:
--------------------------------------------------------------------------------
1 | // inspired by https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/
2 | require('dotenv').config();
3 | const {notarize} = require('electron-notarize');
4 |
5 |
6 | exports.default = async function notarizing(context) {
7 | const {electronPlatformName, appOutDir} = context;
8 | if (electronPlatformName !== 'darwin' || process.platform !== 'darwin') {
9 | return;
10 | }
11 |
12 | const appName = context.packager.appInfo.productFilename;
13 | if (typeof process.env.APPLEID === 'undefined') {
14 | console.log('skipping notarization, remember to setup environment variables for APPLEID and APPLEIDPASS if you want to notarize');
15 | return;
16 | }
17 | return await notarize({
18 | appBundleId: 'com.savvytoolbelt.3cportfoliomanager',
19 | appPath: `${appOutDir}/${appName}.app`,
20 | appleId: process.env.APPLEID,
21 | appleIdPassword: process.env.APPLEIDPASS,
22 | tool: 'notarytool',
23 | teamId: '4UHVHSRL22'
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/AverageDealHours.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import { parseNumber } from "@/utils/number_formatting"
6 |
7 |
8 | interface Type_Card {
9 | metric: number,
10 | additionalData: {
11 | totalClosedDeals: number
12 | totalDealHours: number
13 | }
14 | }
15 |
16 | /**
17 | *
18 | * @param metric - accepts the averageDailyProfit metric from the global data store.
19 | */
20 | const Card_AverageDealHours = ({metric, additionalData}:Type_Card) => {
21 |
22 | const {totalClosedDeals, totalDealHours} = additionalData
23 |
24 | const title = "Avg. Deal Hours"
25 | const message = descriptions.calculations.averageDealHours( totalClosedDeals, totalDealHours)
26 | const key = title.replace(/\s/g, '')
27 | return (
28 |
29 | )
30 | }
31 |
32 | export default Card_AverageDealHours;
--------------------------------------------------------------------------------
/src/app/Features/Profiles/Components/ProfileNameEditor.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { TextField } from '@mui/material';
3 | import { useAppSelector, useAppDispatch } from '@/app/redux/hooks';
4 | import { configPaths } from "@/app/redux/globalFunctions";
5 | import { updateEditProfileByPath } from "@/app/Pages/Settings/Redux/settingsSlice";
6 |
7 | const ProfileNameEditor = () => {
8 | const { name } = useAppSelector(state => state.settings.editingProfile);
9 | const dispatch = useAppDispatch()
10 | const handleChange = (e: any) => {
11 | dispatch(updateEditProfileByPath({ data: e.target.value, path: configPaths.name }))
12 | }
13 |
14 | return (
15 |
27 | )
28 | }
29 |
30 | export default ProfileNameEditor;
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/TotalInDeals.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import {formatCurrency, supportedCurrencies} from'@/utils/granularity'
6 |
7 | interface Type_Card {
8 | metric: number
9 | additionalData: { on_orders: number, totalBoughtVolume:number }
10 | currency: (keyof typeof supportedCurrencies)[]
11 |
12 | }
13 |
14 | /**
15 | *
16 | * @param metric - accepts the activeDealCount metric from the global data store.
17 | * @param additionalData - requires on_orders and totalBoughtVolume to be passed in.
18 | */
19 | const Card_totalInDeals = ({metric, currency, additionalData: { on_orders, totalBoughtVolume }}:Type_Card) => {
20 |
21 | const title = "In deals"
22 | const message = descriptions.calculations.totalInDeals(on_orders, totalBoughtVolume, currency)
23 | const key = title.replace(/\s/g, '')
24 |
25 | return (
26 |
27 | )
28 | }
29 |
30 | export default Card_totalInDeals;
--------------------------------------------------------------------------------
/assets/assets/activeDeals.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
12 |
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/TotalBankRoll.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import {formatCurrency, supportedCurrencies} from'@/utils/granularity'
6 |
7 | interface Type_Card {
8 | metric: number
9 | currency: (keyof typeof supportedCurrencies)[]
10 | additionalData: { position: number, totalBoughtVolume: number, reservedFundsTotal: number}
11 | }
12 |
13 | /**
14 | *
15 | * @param metric - accepts the `totalBankroll` metric from the global data store.
16 | * @param
17 | */
18 | const Card_TotalBankRoll = ({metric, additionalData, currency }:Type_Card) => {
19 |
20 | const { position, totalBoughtVolume, reservedFundsTotal } = additionalData
21 |
22 | const title = "Total bankroll"
23 | const message = descriptions.calculations.totalBankRoll(position, totalBoughtVolume, reservedFundsTotal, currency)
24 | const key = title.replace(/\s/g, '')
25 |
26 | return ( )
27 |
28 | }
29 |
30 | export default Card_TotalBankRoll;
--------------------------------------------------------------------------------
/src/app/Pages/BotPlanner/Components/SaveButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ToastNotifcations } from '@/app/Features/Index'
3 | import { Button } from '@mui/material';
4 | import SaveIcon from '@mui/icons-material/Save';
5 |
6 | interface Type_SaveButton {
7 | saveFunction: any
8 | className: string
9 | }
10 |
11 | const SaveButton = ({saveFunction, className} : Type_SaveButton ) => {
12 |
13 | const [open, setOpen] = React.useState(false);
14 |
15 | const handleClose = (event: any, reason: string) => {
16 | if (reason === 'clickaway') {
17 | return;
18 | }
19 |
20 | setOpen(false);
21 | };
22 |
23 |
24 | return (
25 | <>
26 | }
28 | onClick={() => {
29 | saveFunction()
30 | setOpen(true)
31 | }}
32 |
33 | className={className}
34 |
35 | >
36 | Save table data
37 |
38 |
39 |
40 | >
41 | )
42 | }
43 |
44 | export default SaveButton;
--------------------------------------------------------------------------------
/src/app/Repositories/Impl/electron/Config.ts:
--------------------------------------------------------------------------------
1 | import BaseElectronRepository from "@/app/Repositories/Impl/electron/Base";
2 | import { ConfigRepository } from '@/app/Repositories/interfaces';
3 | import { Type_Profile } from "@/types/config";
4 | import type { defaultConfig } from "@/utils/defaultConfig";
5 |
6 | export default class ElectronConfigRepository extends BaseElectronRepository implements ConfigRepository {
7 | get = (value: 'all' | string) => {
8 | return this.mainPreload.config.get(value)
9 | }
10 | profile = (type: 'create', profileData: Type_Profile, profileId: string) => {
11 | return this.mainPreload.config.profile(type, profileData, profileId)
12 | }
13 | getProfile = (value: string, profileId: string) => {
14 | return this.mainPreload.config.getProfile(value, profileId)
15 | }
16 | reset = async () => {
17 | await this.mainPreload.config.reset()
18 | }
19 | set = async (key: string, value: any) => {
20 | await this.mainPreload.config.set(key, value)
21 | }
22 | bulk = async (changes: typeof defaultConfig) => {
23 | await this.mainPreload.config.bulk(changes)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/assets/assets/cog.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/MaxRiskPercent.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import { parseNumber } from "@/utils/number_formatting"
6 | import type {supportedCurrencies} from'@/utils/granularity'
7 |
8 | interface Type_Card {
9 | metric: number,
10 | additionalData: { maxDCA:number , totalBankroll:number, inactiveBotFunds:number }
11 | currency: (keyof typeof supportedCurrencies)[]
12 |
13 | }
14 |
15 | /**
16 | *
17 | * @param metric - accepts the risk metric calculated locally.
18 | * @param additionalData - accepts totalBankroll, maxDCA
19 | */
20 | const Card_MaxRiskPercent = ({metric, additionalData, currency}:Type_Card) => {
21 |
22 | const { totalBankroll, maxDCA, inactiveBotFunds } = additionalData;
23 |
24 | const title = "Risk %"
25 | const message = descriptions.calculations.risk(maxDCA, totalBankroll, inactiveBotFunds, currency)
26 | const key = title.replace(/\s/g, '')
27 | return ( )
28 |
29 | }
30 |
31 | export default Card_MaxRiskPercent;
--------------------------------------------------------------------------------
/src/app/Components/Sidebar/svg/cog.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/app/Components/icons/Loading/Loading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import './Loading.scss'
4 |
5 | const LoaderIcon = () => {
6 |
7 | return (
8 |
10 |
11 |
15 |
16 |
17 |
21 |
22 |
23 |
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | export default LoaderIcon;
--------------------------------------------------------------------------------
/FORMULA_DESCRIPTIONS.md:
--------------------------------------------------------------------------------
1 | ## Bank Roll Calculations
2 |
3 | ### Total Bankroll
4 | Formula:
5 | `Total Bankroll = Position ( Funds in that currency ) + funds currently in deals`
6 |
7 | Additional information:
8 | - What is in position?
9 | - Position is a sum of what you have in active deals + what you have in available funds. This
10 | - Why doesn't it match exactly to my crypto account?
11 | - Your crypto account also takes into account any coins that you hold that are not a part of your DCA bots. For example, if you've made a smart trade, holding coins, etc.
12 | - Additionally, the data from your crypto account to 3Commas is not always up to date, there may be slight variances in the numbers. But you should see within 1-4% the number is right.
13 |
14 | ### Bankroll Available:
15 | Formula:
16 | ` ( 1 - ( ( funds currently in deals ) / ( Total Bankroll )) ) * 100 = remaining bankroll percent`
17 |
18 |
19 | Additional information:
20 | - This takes into account all the bankroll you have for the selected currency and gives the percent remaining after you remove what's on an order plus funds in a deal.
21 | - This is calculated within the `calculateMetrics` inside `DataContext.js`
22 |
--------------------------------------------------------------------------------
/assets/assets/power.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
7 |
9 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/metrics/TotalRoi.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Card from "../Card";
4 | import descriptions from "@/descriptions";
5 | import { parseNumber } from '@/utils/number_formatting'
6 | import type {supportedCurrencies} from'@/utils/granularity'
7 |
8 | interface Type_Card {
9 | additionalData: { totalProfit: number, totalBankroll: number}
10 | currency: (keyof typeof supportedCurrencies)[]
11 | title: string
12 |
13 | }
14 |
15 | /**
16 | *
17 | * @param additionalData - accepts the `totalProfit_perf` and `boughtVolume` metric from the global data store.
18 | * @param
19 | */
20 | const Card_TotalRoi = ({additionalData, currency, title}:Type_Card) => {
21 |
22 | const { totalProfit, totalBankroll } = additionalData
23 |
24 | //
25 |
26 | // const title = "Total ROI"
27 | const message = descriptions.calculations.totalRoi(totalProfit, totalBankroll, currency)
28 | const key = title.replace(/\s/g, '')
29 | const metric = parseNumber( ( ( totalProfit / ( totalBankroll - totalProfit)) * 100 ), 2) + "%"
30 |
31 | return ( )
32 | }
33 |
34 | export default Card_TotalRoi;
--------------------------------------------------------------------------------
/src/app/Features/3Commas/Type_3Commas.ts:
--------------------------------------------------------------------------------
1 |
2 | type fetchDealDataFunctionQuery = {
3 | closed_at_str: string,
4 | final_profit: number
5 | deal_hours: number
6 | total_deals: number
7 | }
8 |
9 | type fetchPerformanceData = {
10 | performance_id: string,
11 | bot_name: string,
12 | pair: string[],
13 | averageHourlyProfitPercent: number,
14 | total_profit: number,
15 | number_of_deals: number,
16 | bought_volume: number,
17 | averageDealHours: number
18 | }
19 |
20 | type fetchBotPerformanceMetrics = {
21 | bot_id: number,
22 | pairs: string,
23 | total_profit: string,
24 | avg_profit: number,
25 | number_of_deals : number,
26 | bought_volume : number,
27 | avg_deal_hours : number,
28 | avg_completed_so: number,
29 | bot_name : string,
30 | type: 'Bot::SingleBot' | 'Bot::MultiBot'
31 | }
32 |
33 |
34 | type fetchPairPerformanceMetrics = {
35 | pair:string
36 | avg_completed_so: number
37 | total_profit: number
38 | number_of_deals: number
39 | avg_profit: number
40 | bought_volume: number
41 | avg_deal_hours: number
42 | }
43 |
44 | type fetchSoData = {
45 | completed_safety_orders_count : number,
46 | total_profit: number,
47 | total_deals : number
48 | }
--------------------------------------------------------------------------------
/src/app/Components/icons/PieChart.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const PieChart = () => {
4 | return (
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | export default PieChart;
--------------------------------------------------------------------------------
/docs/troubleshooting/migrate-to-v1.md:
--------------------------------------------------------------------------------
1 | # Migrate to v1
2 |
3 | Migrating to v1.0.0 from any prior release can contain a bug or two due to the level of backend changes that have occurred. If you cannot sync, are missing data, or something doesn't feel right try the below troubleshooting steps.
4 |
5 | 1. Reset your profile
6 | * Menu Bar > Help > Reset profile
7 | * This will refresh the page and delete all the data for the profile. Once it's reset you can click the refresh button. If this doesn't resolve the profile try the below.
8 | 2. Delete the application contents, **not** the application.
9 | 1. Based on your operating system navigate to the folder where the application contents are stored.
10 | * **Windows:** `%USERPROFILE%\AppData\Roaming\3C Portfolio Manager`
11 | * **Mac**: `~/Library/Application Support/3C Portfolio Manager`
12 | * **Linux**: `~/.config/3C Portfolio Manager`
13 | 2. Close the 3C Portfolio Manager application
14 | 3. You can delete the **entire** folder titled `3C Portfolio Manager`
15 | 4. Re-open the application
16 | 5. Follow the setup steps outlined under [Creating a Profile](../configuration/profiles.md#creating)
17 | 3. If you continue to have an [issue open a ticket or reach out to me ](../#feedback-or-bug-submission)
18 |
19 |
--------------------------------------------------------------------------------
/src/app/Components/DataTable/Components/OpenIn3Commas.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import LaunchIcon from '@mui/icons-material/Launch';
4 |
5 | import {openLink} from '@/utils/helperFunctions'
6 | import { textAlign } from "@mui/system";
7 |
8 | const OpenIn3Commas = ({ cell, bot_id, className }: { cell: any, bot_id: string, className?: string }) => {
9 |
10 |
11 | const url = (bot_id) ? `https://3commas.io/bots/${bot_id}/edit` : `https://3commas.io/bots`
12 |
13 |
14 | return (
15 | <>
16 |
17 |
18 |
28 | {cell.value}
29 | openLink(url)}
37 | />
38 |
39 | >
40 | )
41 | }
42 |
43 | export default OpenIn3Commas;
--------------------------------------------------------------------------------
/src/app/Repositories/Impl/electron/Database.ts:
--------------------------------------------------------------------------------
1 | import BaseElectronRepository from "@/app/Repositories/Impl/electron/Base";
2 | import { DBRepository } from '@/app/Repositories/interfaces';
3 | import { tableNames } from "@/types/preload";
4 |
5 | export default class ElectronDBRepository extends BaseElectronRepository implements DBRepository {
6 | query = async (profileId:string, queryString:string) => await this.mainPreload.database.query(profileId, queryString);
7 | update = (profileId:string, table:tableNames, data:object[]) => {
8 | if(!data || data.length === 0){
9 | console.log('no data to update');
10 | return
11 | }
12 | this.mainPreload.database.update(profileId, table, data);
13 | }
14 | upsert = (profileId:string, table:tableNames, data:any[], id:string, updateColumn:string) => {
15 | if(!data || data.length === 0){
16 | console.log('no data to update');
17 | return
18 | }
19 | this.mainPreload.database.upsert(profileId, table, data, id, updateColumn);
20 | }
21 | run = (profileId:string, query:string) => this.mainPreload.database.run(profileId, query);
22 | deleteAllData = async (profileID?: string) => await this.mainPreload.database.deleteAllData(profileID);
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/Pages/Settings/Redux/settingsSlice.ts:
--------------------------------------------------------------------------------
1 | import { Type_Profile } from '@/types/config';
2 | import { defaultProfile } from '@/utils/defaultConfig';
3 | import { createSlice } from '@reduxjs/toolkit';
4 | import { v4 as uuidv4 } from 'uuid';
5 | import { updateProfileByPath } from '@/app/redux/globalFunctions';
6 | const initialState = {
7 | editingProfile: defaultProfile
8 | }
9 |
10 | export const settingsSlice = createSlice({
11 | name: 'settings',
12 | initialState,
13 | reducers: {
14 | setEditingProfile: (state, action) => {
15 | state.editingProfile = action.payload
16 | },
17 | updateEditProfileByPath: (state, action: { payload: { data: string | {} | [], path: any}}) => {
18 | const { data, path } = action.payload
19 | const newProfile = updateProfileByPath(data, Object.assign({}, { ...state.editingProfile }), path)
20 | state.editingProfile = newProfile
21 | },
22 | addEditingProfile: state => {
23 | state.editingProfile = { ...defaultProfile, id: uuidv4() }
24 | },
25 |
26 | }
27 | })
28 |
29 | export const {
30 | addEditingProfile,
31 | updateEditProfileByPath,
32 | setEditingProfile
33 | } = settingsSlice.actions;
34 |
35 | export default settingsSlice.reducer
--------------------------------------------------------------------------------
/src/app/Components/icons/Cog.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Cog = () => {
4 | return (
5 |
6 |
8 |
9 | )
10 | }
11 |
12 | export default Cog;
--------------------------------------------------------------------------------
/src/app/Components/icons/ActiveDeals.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const ActiveDealsIcon = () => {
4 | return (
5 |
7 |
8 |
10 |
12 |
16 |
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default ActiveDealsIcon;
--------------------------------------------------------------------------------
/docs/configuration/initial-setup.md:
--------------------------------------------------------------------------------
1 | # Initial Setup
2 |
3 | 1. Download the latest release from [GitHub](https://github.com/coltoneshaw/3c-portfolio-manager/releases/)
4 | * Depending on how you're downloading this file you could get an unsafe file warning. For more information read [Why should I trust this?](../#why-should-i-trust-this) and [Unsafe File Warning](../#unsafe-file-warning).
5 | * You'll find the download links under `Assets`.
6 | 2. Run the installer on your computer \(or a local VM\)
7 | 3. Generate API keys from 3Commas. **These are not the same as your exchange keys**
8 | 1. Go to [3commas.io](www.3commas.io)
9 | 2. Top right click your email > API Keys 
10 | 3. Click "New API Access Token"
11 | 4. Select all three **read-only** properties.
12 | 5. Save and use in the next step
13 | 4. Add your API keys to the application, run `Test API keys`
14 | * This will download your accounts and confirm the API keys are valid.
15 | 5. Choose a filter currency, the start date, and enable an account.
16 | * If you have funds in your account that you **do not** want to be used in calculations add that to the `Reserved Bankroll` field
17 | 6. Click Save.
18 | * This will download the deal data, bots, and account information from 3commas. This can take anywhere from 15 seconds to 4 minutes.
19 | 7. Start to profit
20 |
21 |
--------------------------------------------------------------------------------
/src/app/Components/Sidebar/svg/botmanager.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
8 |
9 |
10 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
31 |
32 |
33 |
34 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/app/Components/Charts/Speedometer/MaxRiskPercent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactSpeedometer from "react-d3-speedometer"
3 |
4 | interface Type_Speedometer {
5 | metric: number
6 | min: number
7 | max: number
8 | colorArray?: string[]
9 | labelArray: object[]
10 | title:string
11 | }
12 |
13 | const MaxRiskSpeedometer = ({ metric, min, max, colorArray, labelArray, title }:Type_Speedometer) => {
14 |
15 | return (
16 |
21 |
25 |
{title}
26 | max) ? max : metric}
31 | currentValueText={`${metric}%`}
32 | needleColor="steelblue"
33 | segments={5}
34 | segmentColors={colorArray}
35 | customSegmentLabels={labelArray}
36 | />
37 |
38 |
39 |
40 |
41 | )
42 |
43 | }
44 |
45 |
46 |
47 | export default MaxRiskSpeedometer;
--------------------------------------------------------------------------------
/assets/assets/botmanager.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
8 |
9 |
10 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
31 |
32 |
33 |
34 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/app/Pages/Settings/Components/StartDatePicker.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { isValid } from 'date-fns'
3 | import moment from "moment";
4 |
5 | import DateTimePicker from '@mui/lab/DateTimePicker';
6 | import { TextField, FormControl } from '@mui/material';
7 |
8 | import { useAppSelector, useAppDispatch } from '@/app/redux/hooks';
9 | import { configPaths } from "@/app/redux/globalFunctions";
10 | import { updateEditProfileByPath } from "@/app/Pages/Settings/Redux/settingsSlice";
11 |
12 | export default function StartDatePicker() {
13 |
14 | const startDate = useAppSelector(state => state.settings.editingProfile.statSettings.startDate);
15 | const dispatch = useAppDispatch()
16 |
17 | const returnTodayUtcEnd = (date: Date) => moment.utc(date).endOf("day").valueOf();
18 |
19 | const handleDateChange = (date: Date | null) => {
20 | if (date != undefined && isValid(date))dispatch(updateEditProfileByPath({ data: moment(date).valueOf(), path: configPaths.statSettings.startDate }))
21 | };
22 |
23 | return (
24 |
25 | }
31 | className="desktopPicker"
32 | />
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/Components/Charts/DataCards/index.ts:
--------------------------------------------------------------------------------
1 | import Card_ActiveDeals from "./metrics/ActiveDeals";
2 | import Card_totalInDeals from "./metrics/TotalInDeals";
3 | import Card_MaxDca from "./metrics/MaxDca";
4 | import Card_TotalBankRoll from "./metrics/TotalBankRoll";
5 | import Card_TotalProfit from "./metrics/TotalProfit";
6 | import Card_EnabledBots from "./metrics/EnabledBots";
7 | import Card_DropCoverage from "./metrics/DropCoverage";
8 | import Card_MaxRiskPercent from "./metrics/MaxRiskPercent";
9 | import Card_TotalBoughtVolume from "./metrics/TotalBoughtVolume";
10 | import Card_TotalDeals from "./metrics/TotalDeals";
11 | import Card_TotalRoi from "./metrics/TotalRoi";
12 | import Card_AverageDailyProfit from "./metrics/AverageDailyProfit";
13 | import Card_AverageDealHours from "./metrics/AverageDealHours";
14 | import Card_TotalDayProfit from './metrics/TotalDayProfit'
15 | import Card_ActiveDealReserve from "./metrics/ActiveDealReserve";
16 | import Card_TotalUnrealizedProfit from "./metrics/TotalUnrealizedProfit";
17 |
18 |
19 | export {
20 | Card_ActiveDeals,
21 | Card_totalInDeals,
22 | Card_MaxDca,
23 | Card_TotalBankRoll,
24 | Card_TotalProfit,
25 | Card_EnabledBots,
26 | Card_DropCoverage,
27 | Card_MaxRiskPercent,
28 | Card_TotalBoughtVolume,
29 | Card_TotalDeals,
30 | Card_TotalRoi,
31 | Card_AverageDailyProfit,
32 | Card_AverageDealHours,
33 | Card_TotalDayProfit,
34 | Card_ActiveDealReserve,
35 | Card_TotalUnrealizedProfit
36 | }
--------------------------------------------------------------------------------
/src/types/Charts.ts:
--------------------------------------------------------------------------------
1 | import { Type_Profit, Type_Query_PerfArray, Type_ActiveDeals, Type_MetricData, Type_Bot_Performance_Metrics, Type_Pair_Performance_Metrics, Type_SoDistributionArray } from '@/types/3Commas';
2 |
3 | import type {defaultCurrency} from '@/types/config'
4 | export interface Type_SoDistribution {
5 | data: Type_ActiveDeals[]
6 | metrics: Type_MetricData
7 | defaultCurrency: defaultCurrency
8 | }
9 |
10 | export type Type_SoDealDis = {
11 | defaultCurrency: defaultCurrency
12 | data: Type_SoDistributionArray[] | undefined
13 | }
14 |
15 |
16 | export interface Type_ProfitChart {
17 | data: Type_Profit[]
18 | X: string
19 | defaultCurrency: defaultCurrency
20 | }
21 |
22 | export interface Type_Pair_Performance {
23 | data: Type_Pair_Performance_Metrics[] | undefined | []
24 | defaultCurrency: defaultCurrency
25 |
26 | }
27 |
28 |
29 | export interface Type_Tooltip {
30 | active: boolean
31 | payload: any[]
32 | label: string,
33 | formatter: Function
34 | }
35 |
36 | export interface Type_DealPerformanceCharts{
37 | data: Type_Query_PerfArray[] | undefined | []
38 | defaultCurrency: defaultCurrency
39 |
40 | }
41 |
42 | export interface Type_BotPerformanceCharts{
43 | data: Type_Bot_Performance_Metrics[] | undefined | []
44 | defaultCurrency: defaultCurrency
45 |
46 | }
47 |
48 | export interface Type_ActiveDealCharts{
49 | // title: string
50 | data: Type_ActiveDeals[]
51 | defaultCurrency: defaultCurrency
52 |
53 | }
--------------------------------------------------------------------------------
/src/app/Pages/DailyStats/DailyStats.scss:
--------------------------------------------------------------------------------
1 |
2 | .green-text {
3 | color: var(--color-green)
4 | }
5 |
6 | .red-text {
7 | color: var(--color-red)
8 | }
9 |
10 | .kpiMetricsContainer {
11 | display: flex;
12 | flex-direction: column;
13 | justify-content: center;
14 |
15 |
16 | .metrics {
17 | flex: 1;
18 | display: flex;
19 | flex-direction: column;
20 | justify-content: center;
21 |
22 | p, .roi-percent {
23 | margin: 0;
24 | padding: .2em 0;
25 | font-size: 1.1em;
26 | white-space: nowrap;
27 | }
28 |
29 | strong {
30 | margin: 0 .4em 0 0;
31 | font-size: 1.1em;
32 | font-weight: 700;
33 | letter-spacing: .5px;
34 | padding: .2em 0;
35 | }
36 |
37 | @media only screen and (max-width: 1550px){
38 | p, .roi-percent {
39 | font-size: 1em;
40 | }
41 |
42 | strong {
43 | font-size: 1em;
44 | }
45 | }
46 | }
47 | }
48 |
49 | .roiSpan {
50 | display: flex;
51 | flex-direction: row;
52 | // flex-wrap: wrap;
53 | justify-content: center;
54 |
55 | strong {
56 | text-align: right;
57 | white-space: nowrap;
58 | align-self: center;
59 | }
60 |
61 | p {
62 | align-self: center;
63 | .roi-percent {
64 | white-space: nowrap;
65 | }
66 | }
67 |
68 |
69 |
70 | }
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build/release
2 |
3 | on: push
4 |
5 | jobs:
6 | release:
7 | runs-on: ${{ matrix.os }}
8 |
9 | strategy:
10 | matrix:
11 | os: [macos-latest, windows-latest]
12 |
13 | steps:
14 | - name: Check out Git repository
15 | uses: actions/checkout@v1
16 |
17 | - name: Install Node.js, NPM and Yarn
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: 16
21 |
22 | - name: Install Snapcraft
23 | uses: samuelmeuli/action-snapcraft@v1
24 | # Only install Snapcraft on Ubuntu
25 | if: startsWith(matrix.os, 'ubuntu')
26 | with:
27 | # Log in to Snap Store
28 | snapcraft_token: ${{ secrets.snapcraft_token }}
29 |
30 | - name: Build/release Electron app
31 | uses: samuelmeuli/action-electron-builder@v1
32 | with:
33 | # GitHub token, automatically provided to the action
34 | # (No need to define this secret in the repo settings)
35 | github_token: ${{ secrets.github_token }}
36 |
37 | # If the commit is tagged with a version (e.g. "v1.0.0"),
38 | # release the app after building
39 | release: ${{ startsWith(github.ref, 'refs/tags/v') }}
40 | mac_certs: ${{ secrets.mac_certs }}
41 | mac_certs_password: ${{ secrets.mac_certs_password }}
42 | env:
43 | # macOS notarization API key
44 | APPLEID: ${{ secrets.APPLEID }}
45 | APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
--------------------------------------------------------------------------------
/src/app/Pages/Stats/Views/SummaryStatistics.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useAppSelector } from '@/app/redux/hooks';
3 |
4 | // material UI components
5 | import {Grid} from '@mui/material';
6 |
7 | // custom charts
8 | import { SummaryProfitByDay } from '@/app/Components/Charts/Area'
9 | import { PairPerformanceBar, BotPerformanceBar, ProfitByDay } from '@/app/Components/Charts/Bar';
10 |
11 | const SummaryStatistics = () => {
12 |
13 | const { profitData, performanceData } = useAppSelector(state => state.threeCommas);
14 | const defaultCurrency = useAppSelector(state => state.config.currentProfile.general.defaultCurrency);
15 |
16 |
17 | return (
18 | <>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | >
34 |
35 | )
36 | }
37 |
38 | export default SummaryStatistics
--------------------------------------------------------------------------------
/webpack.react.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const path = require('path');
3 |
4 | module.exports = {
5 | mode: 'development',
6 | entry: './src/renderer.tsx',
7 | target: 'electron-renderer',
8 | devtool: 'source-map',
9 | devServer: {
10 | contentBase: path.join(__dirname, 'dist/renderer.js'),
11 | compress: true,
12 | port: 9000
13 | },
14 | resolve: {
15 | alias: {
16 | ['@']: path.resolve(__dirname, 'src'),
17 | ['#']: path.resolve(__dirname, '.')
18 | },
19 | extensions: ['.tsx', '.ts', '.js'],
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.ts(x?)$/,
25 | include: /src/,
26 | use: [{ loader: 'ts-loader' }]
27 | },
28 | {
29 | test: /\.js$/,
30 | exclude: /node_modules/,
31 | use: {
32 | loader: 'babel-loader',
33 | options: {
34 | presets: [[
35 | '@babel/preset-env', {
36 | targets: {
37 | esmodules: true
38 | }
39 | }],
40 | '@babel/preset-react']
41 | }
42 | }
43 | },
44 | {
45 | test: /\.s[ac]ss$/i,
46 | use: [
47 | 'style-loader',
48 | 'css-loader',
49 | 'sass-loader',
50 | ],
51 | }
52 | ]
53 | },
54 | output: {
55 | path: __dirname + '/dist',
56 | filename: 'renderer.js'
57 | },
58 | plugins: [
59 | new HtmlWebpackPlugin({
60 | template: './src/index.html'
61 | })
62 | ]
63 | };
64 |
--------------------------------------------------------------------------------
/src/app/Pages/Settings/Settings.scss:
--------------------------------------------------------------------------------
1 | .settings-div{
2 | margin-left: auto;
3 | margin-right: auto;
4 |
5 | width: 75%;
6 | min-width: 200px;
7 |
8 | padding: 5em;
9 | justify-content: space-between;
10 | text-align: left;
11 |
12 | // relative for the add profile dropdown.
13 | position: relative;
14 |
15 | }
16 |
17 | .settings-child {
18 | margin: 0 0 40px 0;
19 | }
20 |
21 | .subText{
22 | font-weight: 300;
23 | font-size: .8em;
24 | margin: 0 0 1em 0;
25 | }
26 |
27 | .settingsButtonDiv{
28 | justify-content: center;
29 |
30 | .deleteProfile{
31 | background-color: var(--color-red);
32 | color: black !important;
33 | opacity: .9;
34 | }
35 |
36 | .deleteProfile:hover {
37 | background-color: var(--color-red);
38 | opacity: .8;
39 | }
40 | }
41 |
42 | .settingsButtonDiv button{
43 | width: 200px;
44 | }
45 |
46 | .settings-right {
47 | margin-left: 15px;
48 | flex-basis: 50%;
49 | }
50 |
51 | .settings-left {
52 | margin-right: 15px;
53 | flex-basis: 50%;
54 | }
55 |
56 | #date-picker-inline {
57 | margin: 10px
58 | }
59 |
60 | .desktopPicker {
61 | margin: 10px;
62 | width: 100%;
63 | }
64 |
65 | .settings-dataGrid {
66 | flex-grow: 1;
67 | text-align: center;
68 | border: none;
69 | width: 50%;
70 | align-self: center;
71 | width: 60%;
72 | }
73 |
74 | .versionNumber {
75 | align-self: center;
76 | }
77 |
78 | .settings-datePicker{
79 | .MuiSvgIcon-root {
80 | color: var(--color-text-lightbackground) !important;
81 | }
82 | }
--------------------------------------------------------------------------------
/src/app/Features/CoinPriceHeader/CoinPriceHeader.scss:
--------------------------------------------------------------------------------
1 | .BtcPriceSpan {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | padding: .5em 0;
6 | margin-left: 60px;
7 | width: calc(100vw - 60px);
8 | display: flex;
9 | flex-direction: row;
10 | justify-content: center;
11 | font-size: .95em !important;
12 | background-color: var(--color-background);
13 | z-index: 100;
14 | padding-right: 1em;
15 | box-shadow: 0 2px 4px -2px rgba(0,0,0,.2);
16 |
17 | .coinDiv {
18 | flex: 1;
19 | text-align: center;
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | }
24 |
25 | .coinHeaderButton {
26 | font-size: .95em !important;
27 | padding: 0;
28 | margin: 0;
29 | line-height: 0;
30 |
31 | .MuiButton-label {
32 | font-size: .9em !important;
33 |
34 | }
35 | }
36 |
37 |
38 | // add shadow
39 | }
40 |
41 | div.addCoinModal {
42 | background-color: var(--color-background);
43 | height: 350px;
44 | width: 300px;
45 | padding: 1em;
46 | position: relative;
47 |
48 | .closeIcon{
49 | position: absolute;
50 | right: 2%;
51 | top: 2%;
52 | cursor: pointer;
53 | width: 25px;
54 | height: 25px;
55 | }
56 |
57 | .closeIcon:hover{
58 | fill: var(--color-secondary-light25)
59 | }
60 |
61 | .selectedCoinDiv{
62 | align-items: center;
63 | width: 100%;
64 | }
65 |
66 | .addCoinDiv {
67 | align-items: center;
68 | width: 100%;
69 | }
70 |
71 |
72 | .MuiAutocomplete-input {
73 | color: var(--color-text-lightbackground)
74 | }
75 | }
--------------------------------------------------------------------------------
/src/app/Pages/Stats/Components/ViewRenderer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { RiskMonitor, SummaryStatistics, PerformanceMonitor } from '../Views/Index';
3 | import { setStorageItem, getStorageItem, storageItem } from '@/app/Features/LocalStorage/LocalStorage';
4 | const defaultNav = 'summary-stats';
5 | const localStorageSortName = storageItem.navigation.statsPage
6 |
7 | const useViewRenderer = () => {
8 |
9 | const [currentView, changeView] = useState(defaultNav)
10 |
11 | const viewChanger = (newView: pageIds) => {
12 |
13 | const selectedNav = (newView != undefined) ? newView : defaultNav;
14 | changeView(selectedNav);
15 | setStorageItem(localStorageSortName, selectedNav)
16 | }
17 |
18 | useEffect(() => {
19 | const getSortFromStorage = getStorageItem(localStorageSortName);
20 | changeView(getSortFromStorage ?? defaultNav);
21 | }, [])
22 |
23 | return {
24 | currentView,
25 | viewChanger
26 | }
27 |
28 |
29 | }
30 |
31 | const ViewRenderer = ({ currentView }: { currentView: pageIds }) => {
32 | const currentViewRender = () => {
33 | let view =
34 | switch (currentView) {
35 | case 'risk-monitor':
36 | view =
37 | break;
38 | case 'performance-monitor':
39 | view =
40 | break;
41 | default:
42 | break;
43 | }
44 |
45 | return view;
46 | }
47 | return currentViewRender()
48 | }
49 |
50 | export {
51 | ViewRenderer,
52 | useViewRenderer
53 | }
--------------------------------------------------------------------------------
/docs/developers/setup-a-dev-build/README.md:
--------------------------------------------------------------------------------
1 | # Setup a dev build
2 |
3 | A developers build will enable you to contribute to the project locally, make changes in real time, and test the code for yourself. Just follow the below steps.
4 |
5 | 1. Download the project locally
6 |
7 | ```text
8 | git clone https://github.com/coltoneshaw/3c-portfolio-manager.git
9 | ```
10 |
11 | 2. Navigate into the folder you downloaded
12 |
13 | ```bash
14 | cd 3c-portfolio-manager
15 | ```
16 |
17 | 3. Download the project dependencies.
18 |
19 | ```bash
20 | npm i --include=dev
21 | ```
22 |
23 |
24 | If you experience issues with `node-gyp` when installing the dependencies run `pwd` or equivalent and ensure that you **do not** have any spaces in your path names.
25 |
26 | Invalid path name example - `/Desktop/my folder/3c-portfolio-manager`
27 |
28 | Valid path name example - `/Desktop/my_folder/3c-portfolio-manager`
29 |
30 | 4. Build webpack and sqlite3
31 |
32 | ```text
33 | npm run webpack
34 | npm run rebuild
35 | ```
36 |
37 | These commands will take a few minutes as they build the webpack config and rebuild sqlite locally.
38 |
39 | 5. Start the dev server
40 |
41 | ```text
42 | npm run dev
43 | ```
44 |
45 | This will start the development version of 3C portfolio manager in a new window for you to test with. As you make changes to the code the application will refresh. You may see errors for dev tools. This is expected until the full build is complete. Give it about a minute to finish.
46 |
47 | Note: If you make changes to the Electron main.ts / preload.ts file you may need to cancel the dev server, rebuild with `npm run rebuild`, and start up the dev server again.
48 |
49 | ```bash
50 | npm i --include=dev
51 | ```
52 |
53 |
--------------------------------------------------------------------------------
/src/app/Features/Changelog/changelogModal.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | div.changelogModal {
4 | background-color: var(--color-background);
5 | height: 60vh;
6 | width: 60vw;
7 | position: relative;
8 |
9 | .closeIcon{
10 | position: absolute;
11 | right: 2%;
12 | top: 2%;
13 | cursor: pointer;
14 | width: 25px;
15 | height: 25px;
16 | }
17 |
18 | .closeIcon:hover{
19 | fill: var(--color-secondary-light25)
20 | }
21 | h3{
22 | margin: 0;
23 | padding: 1em;
24 | letter-spacing: .5px;
25 | text-transform: uppercase;
26 | }
27 |
28 | div.versionDiv {
29 | flex-basis: 25%;
30 | background-color: var(--color-primary);
31 | text-align: center;
32 | overflow-y: scroll;
33 | overflow-x: hidden;
34 |
35 |
36 |
37 | span.version {
38 | margin: 0 ;
39 | padding: 1em;
40 | letter-spacing: 1px;
41 | }
42 |
43 | span.version:hover {
44 | background-color: var(--color-primary-dark25)
45 | }
46 |
47 | span.version.active {
48 | background-color: var(--color-background);
49 | }
50 |
51 |
52 |
53 | }
54 |
55 | .changesDiv {
56 | flex-basis: 75%;
57 | padding-left: 1em;
58 | overflow-x: auto;
59 |
60 |
61 | h3{
62 | padding-left: 0;
63 | }
64 |
65 | h4 {
66 | margin: 0
67 | }
68 |
69 | li {
70 | font-weight: 400;
71 | letter-spacing: .2px;
72 | }
73 |
74 | a {
75 | padding-bottom: .5em;
76 | color: var(--color-text-light)
77 | }
78 | }
79 | }
80 |
81 |
82 |
--------------------------------------------------------------------------------
/src/main/precheck.ts:
--------------------------------------------------------------------------------
1 | import { config } from "@/main/Config/config";
2 | import { TconfigValues } from "@/types/config";
3 | import { checkOrMakeTables } from "./Database/initializeDatabase";
4 | import log from 'electron-log';
5 | import path from "path";
6 | import fsExtra from 'fs-extra';
7 | import { app } from "electron";
8 |
9 | const appDataPath = app.getPath('userData');
10 | const checkInvalidConfig = async (currentProfile: string | undefined | 'default', loadedConfig: TconfigValues) => {
11 |
12 | if(!currentProfile || currentProfile === 'default'){
13 | try {
14 | const profileIds = Object.keys(loadedConfig.profiles);
15 | config.set('current', profileIds[0])
16 | log.debug('Primary profile was undefined / default. Switching to ' + profileIds[0])
17 | } catch (err) {
18 | log.error('Unable to convert config to use a new primary profile.', err)
19 | }
20 |
21 | }
22 | }
23 |
24 | const checkProfileDatabase = async (currentProfile: string) => {
25 | await checkOrMakeTables(currentProfile)
26 | }
27 |
28 | const checkDatabaseDirectory = async () => {
29 |
30 | const databaseDirExists = await fsExtra.pathExists(path.join(appDataPath, 'databases'))
31 | if(!databaseDirExists) {
32 | await fsExtra.mkdir(path.join(appDataPath, 'databases'));
33 | log.debug('Created the database directory')
34 | return;
35 | }
36 | log.debug('Database directory exists')
37 | }
38 |
39 | export const preloadCheck = async () => {
40 | const loadedConfig = config.store;
41 | const currentProfile = loadedConfig?.current;
42 |
43 | await checkInvalidConfig(currentProfile, loadedConfig)
44 | await checkDatabaseDirectory()
45 | await checkProfileDatabase(currentProfile)
46 | }
--------------------------------------------------------------------------------
/src/app/Features/UpdateBanner/UpdateBanner.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 |
3 |
4 | import { openLink } from '@/utils/helperFunctions';
5 | import { useAppDispatch, useAppSelector } from '@/app/redux/hooks';
6 | import { updateBannerData, banner } from './redux/bannerSlice'
7 | import './UpdateBanner.scss';
8 |
9 | import Close from '@mui/icons-material/Close';
10 | let latestLink = 'https://github.com/coltoneshaw/3c-portfolio-manager/releases'
11 |
12 | const UpdateBanner = () => {
13 |
14 | const { show, message, type } = useAppSelector(state => state.banner)
15 | const dispatch = useAppDispatch()
16 |
17 | const returnBannerElement = (type: banner, message: string) => {
18 | if (type == 'updateVersion') {
19 | return ( There is a new update available! Click openLink(latestLink + '/tag/' + message)}>here to download {message}.
)
20 | }
21 |
22 | return {message}
23 | }
24 |
25 | const renderBanner = () => {
26 | if (show) {
27 | return (
28 |
35 | {returnBannerElement(type, message)}
36 | dispatch(updateBannerData({ show: false, message: '', type: '' }))} />
37 |
38 | )
39 | }
40 | }
41 |
42 | return (
43 | <>
44 | {renderBanner()}
45 | >
46 | )
47 | }
48 |
49 | export default UpdateBanner;
--------------------------------------------------------------------------------
/src/app/Pages/Stats/Views/RiskMonitor.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useAppSelector } from '@/app/redux/hooks';
3 |
4 | // material UI components
5 | import { Grid } from '@mui/material';
6 |
7 | // custom charts
8 | import { DealSoUtilizationBar, SoDistribution } from '@/app/Components/Charts/Bar/index'
9 | import SpeedometerDiv from '@/app/Pages/Stats/Components/SpeedometerDiv';
10 |
11 |
12 |
13 | const RiskMonitor = () => {
14 |
15 | const { activeDeals, metricsData } = useAppSelector(state => state.threeCommas);
16 | const defaultCurrency = useAppSelector(state => state.config.currentProfile.general.defaultCurrency);
17 |
18 | return (
19 | <>
20 |
21 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {/*
33 |
34 |
35 |
36 |
37 |
38 |
39 | */}
40 |
41 |
42 |
43 |
44 |
45 | >
46 |
47 | )
48 | }
49 |
50 | export default RiskMonitor
--------------------------------------------------------------------------------
/docs/frequently-asked-questions/google-sheets.md:
--------------------------------------------------------------------------------
1 | # Google Sheets
2 |
3 | ### Why did we change from Google Sheets?
4 |
5 | If you find yourself asking "Why did you go from Google Sheets to an Electron app? I love sheets!" then read below!
6 |
7 | There were two reasons in the consideration of switching, the limitations/performance of sheet storage and the pain of end-users updating the sheet.
8 |
9 | #### Performance and storage limitations
10 |
11 | As you might know, Google Sheets limits your entire sheet to be about five million cells which means you cannot have all of your transaction data and for some people, you can only have a small subset of your recent deals. We went through a long process of pruning the data not needed and becoming critical of each column that we added. We even took the step of limiting the sheet to only about 5000 deals, however, this was still about 300k columns and a 2-6 minute data refresh the experience.
12 |
13 | **The updates...**
14 |
15 | How long have you used the sheet? Was it during the period when we had a new update every hour it seemed? So you may have experienced the pain updating was. It meant you had to...
16 |
17 | 1. Go copy the new sheet
18 | 2. Delete your old sheet
19 | 3. Regenerate your API keys
20 | 4. Put them in the new sheet
21 | 5. Hope the sheet took them without hitting an error that required private browsing...
22 | 6. Resync the entire sheet
23 | 7. Now you have your data again.
24 |
25 | Did that feel like a lot to you? Imagine doing it every time we release a new version. It's a pain, sometimes _even I_ wouldn't update to the new version since it wasn't worth it. Now with it being local, that means all you need to do is download the latest version, run the installer and you're done! Everything under the hood is stored directly on your computer. Which means it's also far safer.
26 |
27 |
--------------------------------------------------------------------------------
/scripts/getReleaseCounts.js:
--------------------------------------------------------------------------------
1 | const fetch = require('electron-fetch').default
2 |
3 |
4 | const fetchVersions = async () => {
5 | let response = await fetch('https://api.github.com/repos/coltoneshaw/3c-portfolio-manager/releases',
6 | {
7 | method: 'GET',
8 | timeout: 30000,
9 | });
10 |
11 | const data = await response.json()
12 | const downloadArray = {};
13 | // const downloadsByRelease = []
14 | let downloadsByOS = {
15 | 'Windows' : 0,
16 | 'Linux' : 0,
17 | 'Mac': 0,
18 | 'Total' : 0
19 | }
20 |
21 | for (release of data) {
22 |
23 | const {tag_name, assets} = release
24 |
25 | const downloads = {};
26 | let releaseDownloads = 0;
27 |
28 | for (download of assets) {
29 | const {name, download_count} = download;
30 | if(name.includes('.exe')){
31 | downloads['Windows'] = download_count
32 | downloadsByOS.Windows += download_count
33 | } else if(name.includes('.AppImage' || name.includes('.snap'))){
34 | downloads['Linux'] = download_count
35 | downloadsByOS.Linux += download_count
36 | } else if(name.includes('.dmg')){
37 | downloads['Mac'] = download_count
38 | downloadsByOS.Mac += download_count
39 | }
40 |
41 | releaseDownloads += +download_count
42 | }
43 |
44 | downloads['Total'] = releaseDownloads;
45 | downloadArray[tag_name] = downloads;
46 | downloadsByOS.Total += releaseDownloads
47 | // downloadArray.push({tag_name: downloads})
48 | }
49 |
50 | console.log(downloadArray)
51 | // console.log(downloadsByRelease)
52 | console.log(downloadsByOS)
53 |
54 | }
55 |
56 | console.log(fetchVersions())
--------------------------------------------------------------------------------
/src/app/Components/Buttons/UpdateData.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | import { Button } from '@mui/material';
4 | import SyncIcon from '@mui/icons-material/Sync';
5 |
6 | import { useAppSelector } from '@/app/redux/hooks';
7 | import {updateAllData} from '@/app/redux/threeCommas/Actions'
8 |
9 | import { ToastNotifcations } from '@/app/Features/Index'
10 |
11 | interface Type_ButtonProps {
12 | style?: object,
13 | className?: string
14 | disabled?: boolean
15 | }
16 | const UpdateDataButton = ({ style, className}: Type_ButtonProps) => {
17 | const { threeCommas: {isSyncing}, config: {currentProfile}} = useAppSelector(state => state);
18 |
19 | const [spinning, updateSpinning] = useState(false)
20 | useEffect(() => updateSpinning(isSyncing), [isSyncing])
21 |
22 |
23 | const [open, setOpen] = React.useState(false);
24 |
25 | const handleClick = () => {
26 | setOpen(true);
27 | };
28 |
29 | const handleClose = (event: any, reason: string) => {
30 | if (reason === 'clickaway') {
31 | return;
32 | }
33 |
34 | setOpen(false);
35 | };
36 |
37 |
38 | return (
39 | <>
40 | updateAllData(1000, currentProfile, 'fullSync', handleClick)}
46 | disableElevation
47 | // startIcon={}
48 | style={style}
49 | >
50 |
51 |
52 | >
53 | )
54 | }
55 |
56 | export default UpdateDataButton;
--------------------------------------------------------------------------------
/src/app/Pages/Settings/Components/WriteModeSettings.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {FormControlLabel, FormGroup, Switch} from "@mui/material";
3 |
4 | import { useAppSelector, useAppDispatch } from '@/app/redux/hooks';
5 | import { configPaths } from "@/app/redux/globalFunctions";
6 | import { updateEditProfileByPath } from "@/app/Pages/Settings/Redux/settingsSlice";
7 |
8 | function WriteModeSettings () {
9 |
10 | const writeEnabled = useAppSelector(state => state.settings.editingProfile.writeEnabled);
11 | const dispatch = useAppDispatch()
12 | function toggleWriteEnabled() {
13 | dispatch(updateEditProfileByPath({ data: !writeEnabled, path: configPaths.writeEnabled }))
14 |
15 | }
16 |
17 | return (
18 |
Write mode:
19 |
20 |
21 |
22 | By activating the write mode, you will allow 3CPM to perform write operations on your 3CPM account. 3CPM will never perform any action without asking confirmation.
23 |
24 |
25 |
26 | } label="Enable write mode" />
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | )
42 | }
43 |
44 |
45 | export default WriteModeSettings
--------------------------------------------------------------------------------
/src/app/Components/DataTable/Table.scss:
--------------------------------------------------------------------------------
1 | .dataTableBase {
2 | overflow: auto;
3 | .table {
4 | border-spacing: 0;
5 | background-color: var(--color-background-light);
6 | color: var(--color-text-lightbackground);
7 | font-size: 0.9em;
8 | // display: block;
9 | .th,
10 | .td {
11 | margin: 0;
12 | // padding: 0.3rem 0.2rem 0.3rem 0.2rem;
13 | }
14 |
15 | .thead {
16 | position: sticky;
17 | top: 0;
18 | z-index: 100;
19 | padding: 0;
20 | // text-align: center !important;
21 | .th {
22 | font-weight: 700;
23 | }
24 | }
25 |
26 | .tbody {
27 | .dataTableInput {
28 | font-size: 0.875rem;
29 | padding: 0;
30 | margin: 0;
31 | border: 0;
32 | background-color: var(--color-background-light);
33 | color: var(--color-text-lightbackground);
34 | }
35 |
36 | .tr:nth-child(2n + 2) .td {
37 | background-color: var(--color-secondary-light87);
38 |
39 | .dataTableInput {
40 | background-color: var(--color-secondary-light87);
41 | }
42 | }
43 | .tr:hover .td {
44 | .dataTableInput {
45 | background-color: var(--color-secondary-light25);
46 | color: var(--color-text-darkbackground);
47 | }
48 |
49 | .MuiSwitch-thumb {
50 | background-color: darkgrey !important;
51 | }
52 | background-color: var(--color-secondary-light25);
53 | color: var(--color-text-darkbackground);
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/app/Pages/Stats/Components/StatFiltersDiv.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Type_Profile } from '@/types/config';
3 | import { getLang } from '@/utils/helperFunctions';
4 | const lang = getLang()
5 |
6 | const dateString = (currentProfile: Type_Profile) => {
7 | const date: undefined | number = currentProfile?.statSettings?.startDate;
8 |
9 | if (date != undefined) {
10 | const adjustedTime = date + ((new Date()).getTimezoneOffset() * 60000)
11 | const dateString = new Date(adjustedTime).toUTCString()
12 | return new Date(dateString).toLocaleString(lang, { month: '2-digit', day: '2-digit', year: 'numeric' })
13 | }
14 | return ""
15 | }
16 |
17 | const returnAccountNames = (currentProfile: Type_Profile) => {
18 |
19 | const reservedFunds = currentProfile.statSettings.reservedFunds;
20 | return reservedFunds.length > 0 ?
21 | currentProfile.statSettings.reservedFunds.filter(account => account.is_enabled).map(account => account.account_name).join(', ')
22 | :
23 | "n/a";
24 | }
25 |
26 | const returnCurrencyValues = (currentProfile: Type_Profile) => {
27 | const currencyValues: string[] | undefined = currentProfile.general.defaultCurrency
28 | return currencyValues != undefined && currencyValues.length > 0 ?
29 | currencyValues.join(', ')
30 | :
31 | "n/a";
32 | }
33 |
34 |
35 | const StatFiltersDiv = ({ currentProfile }: { currentProfile: Type_Profile }) => {
36 |
37 | return (
38 |
39 |
Account: {returnAccountNames(currentProfile)}
40 |
Start Date: {dateString(currentProfile)}
41 |
Filtered Currency: {returnCurrencyValues(currentProfile)}
42 |
43 | )
44 | }
45 |
46 | export default StatFiltersDiv;
--------------------------------------------------------------------------------
/src/app/Components/icons/BotPlanner.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const BotPlannerIcon = () => {
4 | return (
5 |
6 |
7 |
8 |
10 |
11 |
12 |
13 |
14 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
32 |
33 |
34 |
35 |
36 |
38 |
39 |
40 |
41 |
42 | )
43 | }
44 |
45 |
46 | export default BotPlannerIcon;
--------------------------------------------------------------------------------
/src/app/Features/3Commas/DataQueries/accounts.ts:
--------------------------------------------------------------------------------
1 | import { Type_Query_Accounts } from '@/types/3Commas'
2 | import { getFiltersQueryString } from '@/app/Features/3Commas/queryString';
3 | import { Type_Profile } from '@/types/config'
4 |
5 | /**
6 | *
7 | * @param {string} defaultCurrency This is the default currency configured in settings and used as a filter
8 | * @returns
9 | */
10 | const getAccountDataFunction = async (profileData: Type_Profile) => {
11 | const filtersQueryString = await getFiltersQueryString(profileData);
12 | const { currencyString, accountIdString, currentProfileID } = filtersQueryString;
13 |
14 | const query = `
15 | SELECT
16 | *
17 | FROM
18 | accountData
19 | WHERE
20 | account_id IN ( ${accountIdString} )
21 | and currency_code IN ( ${currencyString} )
22 | `
23 | let accountData: Type_Query_Accounts[] | [] = await window.ThreeCPM.Repository.Database.query(currentProfileID, query)
24 |
25 | // removed this since it seems redundant to the above query
26 | // .then((data: Type_Query_Accounts[]) => data.filter(row => defaultCurrency.includes(row.currency_code)))
27 |
28 | if (accountData == null || accountData.length > 0) {
29 | let on_ordersTotal = 0;
30 | let positionTotal = 0;
31 |
32 | for (const account of accountData) {
33 | const { on_orders, position } = account
34 | on_ordersTotal += on_orders;
35 | positionTotal += position;
36 |
37 | }
38 | return {
39 | accountData,
40 | balance: {
41 | on_orders: on_ordersTotal,
42 | position: positionTotal,
43 | }
44 | }
45 | }
46 |
47 | return {
48 | accountData: [],
49 | balance: {
50 | on_orders: 0,
51 | position: 0,
52 | }
53 | }
54 | }
55 |
56 | export {
57 | getAccountDataFunction
58 | }
59 |
--------------------------------------------------------------------------------
/src/utils/number_formatting.ts:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | *
4 | * @param number Accepts a number or string, parses and returns
5 | * @param digits number of trailing digits to return.
6 | * @returns returns a number to 0 decimals and comma seperated
7 | */
8 | const parseNumber = (number: number | string, digits:number = 0, activeDeals?: boolean) => {
9 | switch (typeof number) {
10 | case "number": // do nothing
11 | break
12 | case "string":
13 | number = parseInt(number)
14 | break
15 | default:
16 | number = 0
17 | }
18 |
19 | let numberFormatter:any = {'minimumFractionDigits': (digits > 4) ? 4 : digits, 'maximumFractionDigits': digits}
20 |
21 | if(activeDeals){
22 | if(number >= 1000) numberFormatter = { 'minimumFractionDigits': 0, 'maximumFractionDigits': 0, "useGrouping": false}
23 | if(number >= 10) numberFormatter = { 'minimumFractionDigits': digits, 'maximumFractionDigits': digits, "useGrouping": false}
24 | if(number < 10) numberFormatter = { 'minimumFractionDigits': (digits > 4) ? digits : 4, 'maximumFractionDigits' : 8}
25 | }
26 | // console.log(digits)
27 | // if(maxSize && number >= 1) numberFormatter = { 'minimumSignificantDigits': digits , 'maximumSignificantDigits': digits, "useGrouping": false}
28 | // if(number < 1) numberFormatter = { 'minimumFractionDigits': (digits > 6) ? digits : 6 , 'maximumFractionDigits': (digits > 6) ? digits : 6 , "useGrouping": false}
29 | // if(maxSize && number < 1) numberFormatter = { 'minimumSignificantDigits': maxSize , 'maximumSignificantDigits': maxSize}
30 |
31 | return number.toLocaleString(undefined, numberFormatter)
32 | }
33 |
34 | /**
35 | *
36 | * @param num1 top number of fraction
37 | * @param num2 divisor - bottom number of the fraction
38 | * @returns a parsed string to fixed 0 with %
39 | */
40 | const formatPercent = (num1:number , num2:number) => {
41 | return parseNumber ( ( (num1 / num2) * 100 ) , 0 ) + "%"
42 | }
43 |
44 | export {
45 | parseNumber,
46 | formatPercent
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/Components/Sidebar/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import './Sidebar.scss';
3 |
4 |
5 | import { ActiveDealsIcon, TradingViewLogo, BotPlannerIcon, Coffee, Cog, PieChart } from '@/app/Components/icons/Index';
6 | import MenuBookIcon from '@mui/icons-material/MenuBook';
7 | import { SidebarNav, SidebarLink } from './Components';
8 | import { ProfileSwitcher } from '@/app/Features/Profiles/Components/Index'
9 |
10 | import DisplaySwitcher from './DisplaySwitcher';
11 |
12 | import {openLink} from '@/utils/helperFunctions'
13 | import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
14 |
15 |
16 | /**
17 | * TODO:
18 | * - Move the settings / coffee cog into the display switcher.
19 | */
20 | class Sidebar extends Component {
21 |
22 | render() {
23 | return (
24 |
44 | )
45 | }
46 | }
47 |
48 | export default Sidebar;
--------------------------------------------------------------------------------
/src/main/3Commas/types/GridBots.ts:
--------------------------------------------------------------------------------
1 | export type GridBots = {
2 | "id": number,
3 | "account_id": number,
4 | "account_name": string,
5 | "is_enabled": boolean,
6 | "grids_quantity": string,
7 | "created_at": string //"2021-06-17T16:58:44.450Z",
8 | "updated_at": string //"2021-06-24T19:52:56.525Z",
9 | "strategy_type": "manual" | 'ai',
10 | "lower_price": string,
11 | "upper_price": string,
12 | "quantity_per_grid": string,
13 | "leverage_type": null | string,
14 | "leverage_custom_value": null | string,
15 | "name": string,
16 | "pair": string,
17 | "start_price": string,
18 | "grid_price_step": string,
19 | "current_profit": string,
20 | "current_profit_usd": string,
21 | "total_profits_count": string,
22 | "bought_volume": string,
23 | "sold_volume": string,
24 | "profit_percentage": string,
25 | "current_price": string,
26 | "investment_base_currency": string,
27 | "investment_quote_currency": string,
28 | "grid_lines": {
29 | "price": string,
30 | "side": null | string,
31 | "order_placed": boolean
32 | }[]
33 | }
34 |
35 | export type GridBotShow = GridBots & {
36 | editable: boolean
37 | }
38 |
39 | export type GridMarketOrders = {
40 | "grid_lines_orders":
41 | {
42 | "order_id": string,
43 | "order_type": "SELL" | 'BUY',
44 | "status_string": "Filled" | string,
45 | "created_at": string,
46 | "updated_at": string,
47 | "quantity": string,
48 | "quantity_remaining": string,
49 | "total": string,
50 | "rate": string,
51 | "average_price": string
52 | }[],
53 | 'balancing_orders' : any[]
54 | }
55 |
56 | export type GridBotProfits = {
57 | "grid_line_id": number,
58 | "profit": string,
59 | "usd_profit": string,
60 | "created_at": string
61 | }
62 |
63 | export type GridRequiredBalance = {
64 | "need_balancing": boolean,
65 | "necessary_quantities": {
66 | "quantity": string,
67 | "currency": string
68 | }
69 | }
--------------------------------------------------------------------------------
/assets/assets/sun.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
10 |
11 |
12 |
14 |
15 |
16 |
18 |
19 |
20 |
22 |
23 |
24 |
26 |
27 |
28 |
30 |
31 |
32 |
34 |
35 |
36 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/app/Components/Charts/formatting.tsx:
--------------------------------------------------------------------------------
1 | import { formatCurrency, supportedCurrencies } from '@/utils/granularity'
2 | import type {defaultCurrency} from '@/types/config'
3 | import { dynamicSort } from '@/utils/helperFunctions';
4 | import { Type_Bot_Performance_Metrics, Type_Pair_Performance_Metrics } from '@/types/3Commas';
5 |
6 |
7 | const yAxisWidth = (defaultCurrency: defaultCurrency) => {
8 |
9 | const firstCurrency = defaultCurrency[0] ?? ['USD'];
10 | const yWidth = supportedCurrencies[firstCurrency].rounding * 10 ?? undefined
11 | return (yWidth < 50) ? undefined : yWidth
12 | }
13 |
14 | const currencyTickFormatter = (value: any, defaultCurrency: defaultCurrency) => {
15 | if(value === 0) return value
16 | return String(formatCurrency([defaultCurrency[0]], value).metric)
17 | }
18 |
19 | const currencyTooltipFormatter = (value: any, defaultCurrency: defaultCurrency) => {
20 | if(value === 0) return value
21 |
22 | const {metric, symbol} = formatCurrency([defaultCurrency[0]], value);
23 | return String(symbol + ' ' + metric)
24 | }
25 |
26 | const filterData = (data: Type_Bot_Performance_Metrics[] | Type_Pair_Performance_Metrics[], filter:string ) => {
27 | let newData = [...data]
28 | newData = newData.sort(dynamicSort('-total_profit'));
29 | const length = data.length;
30 | const fiftyPercent = length / 2
31 | const twentyPercent = length / 5
32 |
33 | if (filter === 'top20') {
34 | newData = newData.sort(dynamicSort('-total_profit'));
35 | return newData.filter( (bot, index) => index < twentyPercent)
36 | } else if (filter === 'top50') {
37 | newData = newData.sort(dynamicSort('-total_profit'));
38 | return newData.filter( (bot, index) => index < fiftyPercent)
39 | } else if (filter === 'bottom50') {
40 | newData = newData.sort(dynamicSort('total_profit'));
41 | return newData.filter( (bot, index) => index < fiftyPercent)
42 | } else if (filter === 'bottom20') {
43 | newData = newData.sort(dynamicSort('total_profit'));
44 | return newData.filter( (bot, index) => index < twentyPercent)
45 | }
46 |
47 | return newData;
48 | }
49 |
50 | export {
51 | yAxisWidth,
52 | currencyTickFormatter,
53 | currencyTooltipFormatter,
54 | filterData
55 | };
--------------------------------------------------------------------------------
/src/app/Components/Sidebar/svg/sun.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
10 |
11 |
12 |
14 |
15 |
16 |
18 |
19 |
20 |
22 |
23 |
24 |
26 |
27 |
28 |
30 |
31 |
32 |
34 |
35 |
36 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/app/app.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useLayoutEffect, useState } from 'react';
2 |
3 | import './App.global.scss';
4 | import Sidebar from './Components/Sidebar/Sidebar';
5 | import { HashRouter } from 'react-router-dom'
6 | import { MainWindow } from "@/app/Pages/Index"
7 |
8 | import { useThemeProvidor } from './Context/ThemeEngine';
9 |
10 | import UpdateBanner from './Features/UpdateBanner/UpdateBanner';
11 |
12 |
13 | import { useAppSelector, useAppDispatch } from '@/app/redux/hooks';
14 | import { updateConfig } from '@/app/redux/config/configActions';
15 | import { updateAllDataQuery } from './redux/threeCommas/Actions';
16 |
17 | // @ts-ignore
18 | import { version } from '#/package.json';
19 | import { updateBannerData } from '@/app/Features/UpdateBanner/redux/bannerSlice';
20 |
21 | const App = () => {
22 |
23 | const themeEngine = useThemeProvidor();
24 | const currentProfile = useAppSelector(state => state.config.currentProfile)
25 | const dispatch = useAppDispatch()
26 | const [profile, updateLocalProfile] = useState(() => currentProfile)
27 | const { styles } = themeEngine
28 |
29 | useEffect(() => {
30 | updateConfig();
31 | }, []);
32 |
33 | useEffect(() => {
34 | window.ThreeCPM.Repository.Pm.versions()
35 | .then(versionData => {
36 | if (!versionData || !versionData[0]) return
37 | const currentVersion = versionData.filter((release: any) => !release.prerelease)[0]
38 | if ("v" + version != currentVersion.tag_name) {
39 | dispatch(updateBannerData({ show: true, message: currentVersion.tag_name, type: 'updateVersion' }))
40 | }
41 | })
42 | }, [])
43 |
44 | useLayoutEffect(() => {
45 | if (currentProfile.id == profile.id) return
46 | if (currentProfile && currentProfile?.statSettings?.reservedFunds.filter(a => a.is_enabled).length > 0) {
47 | updateAllDataQuery(currentProfile, 'fullSync');
48 | console.log('Changing to a new profile')
49 | updateLocalProfile(currentProfile)
50 | }
51 |
52 | }, [currentProfile])
53 |
54 |
55 | return (
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | )
64 | }
65 |
66 | export default App;
67 |
--------------------------------------------------------------------------------
/src/types/config.ts:
--------------------------------------------------------------------------------
1 | import type { supportedCurrencies } from '@/utils/granularity'
2 |
3 |
4 |
5 | export type defaultCurrency = (keyof typeof supportedCurrencies)[] | []
6 |
7 | export interface Type_Profile {
8 | id: string
9 | name: string,
10 | apis: {
11 | threeC: {
12 | key: string,
13 | secret: string,
14 | mode: string,
15 | }
16 | },
17 | general: {
18 | defaultCurrency: defaultCurrency
19 | globalLimit: number
20 | updated: boolean
21 | },
22 | syncStatus: {
23 | deals: {
24 | lastSyncTime: number
25 | }
26 | },
27 | statSettings: {
28 | startDate: number
29 | account_id: number[],
30 | reservedFunds: Type_ReservedFunds[]
31 | },
32 | writeEnabled: boolean,
33 | }
34 |
35 | export type Type_NotificationsSettings = {
36 | enabled: boolean,
37 | summary: boolean,
38 | }
39 |
40 | export type Type_GlobalSettings = {
41 | notifications: Type_NotificationsSettings
42 | }
43 |
44 |
45 |
46 |
47 | export interface Type_ReservedFunds {
48 | id: number
49 | account_name: string
50 | reserved_funds: string | number
51 | is_enabled: boolean
52 | }
53 |
54 | export interface Type_ApiKeys {
55 | key: string
56 | secret: string
57 | }
58 |
59 | export type TconfigValues = {
60 | profiles: Record,
61 | current: string | 'default',
62 | globalSettings: Type_GlobalSettings,
63 | general: {
64 | version: string
65 | },
66 | }
67 |
68 | export interface Type_ConfigContext {
69 | config: TconfigValues
70 | currentProfile: Type_Profile
71 | updateConfig: any
72 | setConfigBulk: any
73 | reset: any
74 | state: {
75 | accountID: number[]
76 | updateAccountID: any
77 | date: number
78 | updateDate: any
79 | currency: string[]
80 | updateCurrency: any
81 | updateApiData: any
82 | apiData: { key: string, secret: string, mode: string }
83 | reservedFunds: Type_ReservedFunds[],
84 | updateReservedFunds: any
85 | currentProfileId: string
86 | updateCurrentProfileId: any
87 | },
88 | actions: {
89 | fetchAccountsForRequiredFunds: any
90 | }
91 | }
--------------------------------------------------------------------------------
/src/renderer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import App from '@/app/app';
4 | import { ThemeProvider } from '@material-ui/core/styles';
5 | import { ThemeEngine } from '@/app/Context/ThemeEngine'
6 | import DateAdapter from '@mui/lab/AdapterDateFns';
7 | import LocalizationProvider from '@mui/lab/LocalizationProvider';
8 |
9 | import store from '@/app/redux/store'
10 | import { Provider } from 'react-redux'
11 |
12 | import {
13 | ElectronAPIRepository,
14 | ElectronDealsRepository,
15 | ElectronDBRepository,
16 | ElectronConfigRepository,
17 | } from "@/app/Repositories/Impl/electron";
18 |
19 | import BaseBinanceRepository from '@/app/Repositories/Impl/Binance';
20 | import {BaseGeneralRepository, BasePmRepository} from '@/app/Repositories/Impl/General'
21 |
22 | import {Repository} from '@/app/Repositories/interfaces'
23 |
24 |
25 | interface ThreeCPMNS {
26 | Repository: Repository
27 | }
28 |
29 | declare global {
30 | interface Window {
31 | ThreeCPM: ThreeCPMNS;
32 | }
33 | }
34 |
35 | window.ThreeCPM = window.ThreeCPM || {};
36 | const mainPreload = window.mainPreload
37 |
38 | let repo: Repository = {
39 | Deals: new ElectronDealsRepository(mainPreload),
40 | API: new ElectronAPIRepository(mainPreload),
41 | Config: new ElectronConfigRepository(mainPreload),
42 | Database: new ElectronDBRepository(mainPreload),
43 | Binance: new BaseBinanceRepository(mainPreload),
44 | General: new BaseGeneralRepository(mainPreload),
45 | Pm: new BasePmRepository(mainPreload)
46 | };
47 |
48 |
49 | /*
50 | * For the future we could have something like this:
51 | */
52 | // if (electrn) {
53 | // repo = {
54 | // API: new ElectronAPIRepository(electrn),
55 | // ...
56 | // }
57 | // }
58 | // ...
59 |
60 |
61 | // TODO: find a more react friendly way of making Repository accessible
62 | window.ThreeCPM.Repository = repo
63 |
64 | render(
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | ,
78 | document.getElementById('root')
79 |
80 | );
81 |
82 |
83 |
--------------------------------------------------------------------------------
/src/app/Components/DataTable/FormatDeals.tsx:
--------------------------------------------------------------------------------
1 | import { Type_ActiveDeals } from "@/types/3Commas"
2 | import { calc_deviation } from '@/utils/formulas'
3 | import { parseNumber } from '@/utils/number_formatting';
4 |
5 |
6 | const formatDeals = (activeDeals: Type_ActiveDeals[]) => {
7 | return activeDeals.map(deal => {
8 | const { max_safety_orders,
9 | active_manual_safety_orders, max_deal_funds, actual_usd_profit,
10 | actual_profit_percentage, pair, currency,
11 | safety_order_step_percentage, martingale_step_coefficient,
12 | current_price, take_profit_price,
13 | take_profit, base_order_volume, safety_order_volume, martingale_volume_coefficient,
14 | bought_volume, bought_amount, completed_safety_orders_count, completed_manual_safety_orders_count, trailing_deviation, trailing_enabled
15 |
16 | } = deal
17 |
18 |
19 | const safetyOrderString = (completed_manual_safety_orders_count > 0 || active_manual_safety_orders > 0) ? `${completed_safety_orders_count} + ${completed_manual_safety_orders_count} / ${max_safety_orders}` : `${completed_safety_orders_count} / ${max_safety_orders}`
20 |
21 |
22 | const ttp = (trailing_enabled) ? `(${trailing_deviation})` : ''
23 |
24 | return {
25 | ...deal,
26 | actual_usd_profit,
27 | actual_profit_percentage,
28 | current_price,
29 | take_profit_price,
30 | safetyOrderString,
31 | pair: pair + " / " + currency,
32 | // the below values need to be formatted to the same length across all the data
33 | max_deviation: calc_deviation(max_safety_orders, safety_order_step_percentage, martingale_step_coefficient),
34 | // in_profit: actual_usd_profit > 0,
35 | bot_settings: `TP: ${take_profit} ${ttp}, BO: ${base_order_volume}, SO: ${safety_order_volume}, SOS: ${safety_order_step_percentage}%, OS: ${martingale_volume_coefficient}, SS: ${martingale_step_coefficient}, MSTC: ${max_safety_orders}`,
36 | bought_volume: bought_volume ?? 0,
37 | // bought_amount: parseNumber( bought_amount, 5, true) + ' ' + pair,
38 | unrealized_profit: ( take_profit / 100 ) * bought_volume
39 | }
40 | })
41 | }
42 |
43 | export default formatDeals;
--------------------------------------------------------------------------------
/src/app/redux/globalFunctions.ts:
--------------------------------------------------------------------------------
1 | import { Type_Profile } from "@/types/config"
2 |
3 | export const configPaths = {
4 | apis: {
5 | threeC: {
6 | main: 'apis.threeC',
7 | key: 'apis.threeC.key',
8 | secret: 'apis.threeC.secret',
9 | mode: 'apis.threeC.mode'
10 | }
11 | },
12 | syncStatus: {
13 | deals: {
14 | lastSyncTime: 'syncStatus.deals.lastSyncTime'
15 | }
16 | },
17 | statSettings: {
18 | reservedFunds: 'statSettings.reservedFunds',
19 | startDate: 'statSettings.startDate',
20 | account_id: 'statSettings.account_id',
21 | },
22 | name: 'name',
23 | writeEnabled: 'writeEnabled',
24 | general: {
25 | defaultCurrency: 'general.defaultCurrency'
26 | },
27 | globalSettings: {
28 | notifications: {
29 | enabled: 'globalSettings.notifications.enabled',
30 | summary: 'globalSettings.notifications.summary',
31 | }
32 | }
33 | }
34 |
35 |
36 | export const updateProfileByPath = (data: any, profileData: Type_Profile, path: any) => {
37 |
38 | // let newProfile = Object.assign({}, { ...state.currentProfile })
39 | switch (path) {
40 | case configPaths.apis.threeC.main: // update all the api data.
41 | profileData.apis.threeC = data
42 | break
43 | case configPaths.statSettings.reservedFunds: // update all the api data.
44 | profileData.statSettings.reservedFunds = data
45 | break
46 | case configPaths.name: // update all the api data.
47 | profileData.name = data
48 | break
49 | case configPaths.writeEnabled: // update all the api data.
50 | profileData.writeEnabled = data
51 | break
52 | case configPaths.general.defaultCurrency: // update all the currency data
53 | profileData.general.defaultCurrency = data
54 | break
55 | case configPaths.statSettings.startDate: // update all the api data.
56 | profileData.statSettings.startDate = data
57 | break
58 | case configPaths.syncStatus.deals.lastSyncTime:
59 | profileData.statSettings.startDate = data
60 | break
61 | default:
62 | break;
63 | }
64 |
65 | return profileData
66 |
67 | }
--------------------------------------------------------------------------------
/src/app/Features/3Commas/3Commas.ts:
--------------------------------------------------------------------------------
1 | import moment from "moment";
2 |
3 |
4 | import {
5 | Type_Pair_By_Date,
6 | Type_UpdateFunction,
7 | } from '@/types/3Commas'
8 | import { Type_Profile } from '@/types/config'
9 | import { DateRange } from "@/types/Date";
10 |
11 | import { getFiltersQueryString } from './queryString';
12 | import { fetchPerformanceDataFunction, fetchDealDataFunction, getActiveDealsFunction, fetchSoData } from "./DataQueries/deals";
13 | import { fetchBotPerformanceMetrics, botQuery } from "./DataQueries/bots";
14 | import { getAccountDataFunction } from "./DataQueries/accounts";
15 |
16 | // these queries use the deals database but are pairs only. Can probably combine this with the deals queries or create a new folder.
17 | import { fetchPairPerformanceMetrics, getSelectPairDataByDate } from "./DataQueries/pairs";
18 |
19 | /**
20 | * @description This kicks off the update process that updates all 3Commas data within the database.
21 | *
22 | * @params - type 'autoSync'
23 | * @params {options} - option string
24 | */
25 | const updateThreeCData = async (type: string, options: Type_UpdateFunction, profileData: Type_Profile) => {
26 | console.info({ options })
27 | return await window.ThreeCPM.Repository.API.update(type, options, profileData);
28 | }
29 |
30 | export const initDate = (startString: number, oDate?: DateRange) => {
31 | let date = new DateRange()
32 | if (oDate) {
33 | date = { ...oDate }
34 | }
35 |
36 | if (date.from == null) {
37 | date.from = moment(startString).startOf("day").toDate()
38 |
39 | }
40 |
41 | if (date.to == null) {
42 | date.to = moment().endOf("day").toDate()
43 | }
44 | return date;
45 | }
46 |
47 | export const DateRangeToSQLString = (d: DateRange) => {
48 | let fromDateStr = moment.utc(d.from)
49 | .subtract(d.from?.getTimezoneOffset(), "minutes")
50 | .startOf("day")
51 | .toISOString()
52 |
53 |
54 | let toDateStr = moment.utc(d.to)
55 | .subtract(d.to?.getTimezoneOffset(), "minutes")
56 | .add(1, "days")
57 | .startOf("day")
58 | .toISOString()
59 |
60 | return [fromDateStr, toDateStr]
61 | }
62 |
63 |
64 |
65 |
66 | export {
67 | fetchDealDataFunction,
68 | fetchPerformanceDataFunction,
69 | getActiveDealsFunction,
70 | updateThreeCData,
71 | getAccountDataFunction,
72 | fetchBotPerformanceMetrics,
73 | fetchPairPerformanceMetrics,
74 | botQuery,
75 | getSelectPairDataByDate,
76 | getFiltersQueryString,
77 | fetchSoData
78 | }
79 |
80 |
81 |
--------------------------------------------------------------------------------
/src/app/Pages/ActiveDeals/Components/SubrowTabs/Orders.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { formatCurrency } from '@/utils/granularity'
3 |
4 | const dateFormatter = (dateString: string) => new Date(dateString).toLocaleDateString(undefined, { month: '2-digit', day: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' })
5 |
6 | function Orders({ row, ordersData }: any) {
7 |
8 | const formatCurrencyLocally = (value: number) => formatCurrency([row.original.from_currency], value).metric
9 |
10 |
11 | return (
12 |
13 |
14 |
15 |
16 | Side
17 | Order Type
18 | Status
19 | Rate ({row.original.from_currency})
20 | Amount ({row.original.to_currency})
21 | Volume ({row.original.from_currency})
22 | Created
23 | Updated
24 |
25 |
26 |
27 | {ordersData.map((r: any) => (
28 |
29 | {r.order_type}
30 | {r.deal_order_type}
31 | {r.status_string}
32 |
33 | {r.order_type == "BUY" && (<>Desired: {formatCurrencyLocally(r.rate)} Real: {formatCurrencyLocally(r.average_price)}>)}
34 | {r.order_type == "SELL" && (<>{formatCurrencyLocally(r.rate)}>)}
35 |
36 | {formatCurrencyLocally(+r.quantity)}
37 | {(r.total) ? formatCurrencyLocally(r.total) : '-'}
38 | {dateFormatter(r.created_at)}
39 | {dateFormatter(r.updated_at)}
40 | )
41 | )}
42 |
43 |
44 |
)
45 | }
46 |
47 | export default Orders
--------------------------------------------------------------------------------
/src/app/Pages/ActiveDeals/Components/NotificationsSettings.tsx:
--------------------------------------------------------------------------------
1 | import { useAppSelector } from '@/app/redux/hooks';
2 | import { Checkbox, FormControlLabel } from '@mui/material';
3 | import React, { useEffect, useState } from "react";
4 | import { updateNotificationsSettingsGlobal } from '@/app/redux/config/configActions';
5 |
6 | /**
7 | *
8 | * @returns Checkboxes for configuring the state of auto sync.
9 | */
10 | const NotificationsSettings = () => {
11 |
12 | const { enabled: storeEnabled, summary: storeSummary } = useAppSelector(state => state.config.config.globalSettings.notifications);
13 |
14 | const [summary, setSummary] = useState(() => storeSummary)
15 | const [enabled, setEnabled] = useState(() => storeEnabled)
16 |
17 | useEffect(() => {
18 | setSummary(storeSummary)
19 | setEnabled(storeEnabled)
20 | }, [storeEnabled, storeSummary]);
21 |
22 | const changeSummary = (event: React.ChangeEvent) => {
23 | const checked = event.target.checked
24 | updateNotificationsSettingsGlobal({ summary: checked })
25 | setSummary(checked)
26 | }
27 |
28 | const changeEnabled = (event: React.ChangeEvent) => {
29 | const checked = event.target.checked
30 | if(!checked) {
31 | updateNotificationsSettingsGlobal({ summary: false })
32 | setSummary(false)
33 | }
34 | updateNotificationsSettingsGlobal({ enabled: checked })
35 | setEnabled(checked)
36 | }
37 |
38 | return (
39 |
40 |
50 | }
51 | label="Enable Notifications"
52 |
53 | />
54 |
63 | }
64 | label="Summarize Notifications"
65 | />
66 |
67 |
68 |
69 | )
70 | }
71 |
72 | export default NotificationsSettings;
73 |
--------------------------------------------------------------------------------
/src/app/redux/threeCommas/initialState.ts:
--------------------------------------------------------------------------------
1 | import { Type_ReservedFunds, Type_Profile } from '@/types/config'
2 |
3 |
4 | import type {
5 | Type_Query_PerfArray,
6 | Type_Query_bots,
7 | Type_ActiveDeals,
8 | Type_Query_Accounts,
9 | Type_MetricData,
10 | Type_Profit,
11 | Type_Bot_Performance_Metrics,
12 | Type_Performance_Metrics,
13 | Type_Pair_Performance_Metrics,
14 | Type_SyncOptions
15 | } from '@/types/3Commas'
16 |
17 | export {Type_MetricData, Type_Performance_Metrics, Type_ReservedFunds}
18 | // Define the initial state using that type
19 | export const initialState = {
20 | botData: [],
21 | profitData: [],
22 | activeDeals: [],
23 | performanceData: { pair_bot: [], bot: [], safety_order: [] },
24 | balanceData: { on_orders: 0, position: 0 },
25 | accountData: [],
26 | metricsData: {
27 | activeDealCount: 0,
28 | totalProfit_perf: 0,
29 | totalDeals: 0,
30 | boughtVolume: 0,
31 | averageDealHours: 0,
32 | averageDailyProfit: 0,
33 | totalBoughtVolume: 0,
34 | maxRisk: 0,
35 | totalProfit: 0,
36 | maxRiskPercent: 0,
37 | bankrollAvailable: 0,
38 | totalBankroll: 0,
39 | position: 0,
40 | on_orders: 0,
41 | totalInDeals: 0,
42 | availableBankroll: 0,
43 | reservedFundsTotal: 0,
44 | totalClosedDeals: 0,
45 | totalDealHours: 0,
46 | inactiveBotFunds: 0,
47 | totalMaxRisk: 0
48 | },
49 | additionalData: [],
50 | isSyncing: false,
51 | isSyncingTime: 0,
52 | syncOptions: {
53 | time: 0,
54 | syncCount: 0
55 | },
56 | autoRefresh: false,
57 | }
58 |
59 | export type typeString = 'botData' | 'profitData' | 'activeDeals' | 'performanceData' | 'metricsData' | 'accountData' | 'balanceData'
60 |
61 | export type Type_SyncData = {
62 | time: number,
63 | syncCount: number
64 | }
65 |
66 | export type setDataType =
67 | { type: 'botData', data: typeof initialState.botData } |
68 | { type: 'profitData', data: typeof initialState.profitData } |
69 | { type: 'activeDeals', data: typeof initialState.activeDeals } |
70 | { type: 'performanceData', data: typeof initialState.performanceData } |
71 | { type: 'balanceData', data: typeof initialState.balanceData } |
72 | { type: 'accountData', data: typeof initialState.accountData } |
73 | { type: 'metricsData', data: Type_MetricData }
--------------------------------------------------------------------------------
/src/app/Pages/BotPlanner/Components/Risk.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 |
4 | import { useAppSelector } from '@/app/redux/hooks';
5 | import { Type_Query_bots } from '@/types/3Commas';
6 |
7 |
8 | import {
9 | Card_EnabledBots,
10 | Card_DropCoverage,
11 | Card_MaxDca,
12 | Card_TotalBankRoll,
13 | Card_MaxRiskPercent
14 | } from '@/app/Components/Charts/DataCards';
15 |
16 |
17 | // Need to import metric contexts here
18 | const Risk = ({ localBotData }: { localBotData: Type_Query_bots[] }) => {
19 | const { defaultCurrency } = useAppSelector(state => state.config.currentProfile.general);
20 | const { metricsData: {totalBankroll, totalBoughtVolume, position, reservedFundsTotal}} = useAppSelector(state => state.threeCommas);
21 | /**
22 | * Bankroll - sum, on_orders, position all added together. Needs to come from global state most likely.
23 | * risk - bank roll / total DCA risk
24 | * active bots - count of bots with enabled flagged.
25 | * DCA Max risk - sum of the max_bot_usage.
26 | */
27 |
28 | const enabledDeals = localBotData.filter(bot => bot.is_enabled && !bot.hide)
29 |
30 | /**
31 | * TODO
32 | * - Can move these calculations on to the data card itself to clean up these functions. That would mean not every card gets a metric since most are calculated.
33 | */
34 | let maxDCA = (enabledDeals.length > 0) ? enabledDeals.map(deal => deal.max_funds).reduce((sum, max) => sum + max) : 0;
35 | // const inactiveBotFunds = result.map(r => r.enabled_inactive_funds).reduce((sum, funds) => sum + funds ) ?? 0;
36 |
37 | let risk = (maxDCA / totalBankroll) * 100
38 | let botCount = localBotData.filter(deal => deal.is_enabled).length
39 |
40 | const sumDropCoverage = (enabledDeals.length > 0) ? enabledDeals.map(deal => (deal.maxCoveragePercent) ? deal.maxCoveragePercent : 0).reduce((sum, max) => sum + max) : 0;
41 | let dropCoverage = sumDropCoverage / enabledDeals.length
42 |
43 |
44 |
45 | return (
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | )
55 | }
56 |
57 | export default Risk;
--------------------------------------------------------------------------------
/src/app/Features/LocalStorage/LocalStorage.ts:
--------------------------------------------------------------------------------
1 | import { tryParseJSON_ } from "@/utils/helperFunctions"
2 |
3 |
4 | const storageItem = {
5 | navigation: {
6 | homePage: 'homePage', // the home page the application navigates to
7 | statsPage: 'nav-statsPage'
8 | },
9 | settings: {
10 | displayMode: 'displayMode', // the dark mode switcher. Values are 'lightMode' and 'darkMode',
11 | coinPriceArray: 'coinPriceArray'
12 | },
13 | charts:{
14 | pairByDateFilter: 'pairByDateFilter',
15 | BotPerformanceBubble:{
16 | filter: 'filter-botPerformanceBubble'// filter for the bot bubble - values are 'all' , top20, top50, bottom50, bottom20
17 | },
18 | DealPerformanceBubble:{
19 | sort: 'sort-dealPerformanceBubble', // percentTotalProfit , number_of_deals , percentTotalVolume
20 | filter: 'filter-dealPerformanceBubble'// filter for the bot bubble - values are 'all' , top20, top50, bottom50, bottom20
21 | },
22 | PairPerformanceBar: {
23 | sort: 'sort-pairPerformanceBar', // -total_profit, -bought_volume, -avg_deal_hours
24 | filter: 'filter-pairPerformanceBar', // all, top20, top50, bottom50, bottom20
25 | },
26 | BotPerformanceBar: {
27 | sort: 'sort-BotPerformanceBar', // -total_profit, -bought_volume, -avg_deal_hours
28 | filter: 'filter-BotPerformanceBar', // all, top20, top50, bottom50, bottom20
29 | },
30 | ProfitByDay: {
31 | sort: 'sort-ProfitByDay' //day , month, year
32 | }
33 | },
34 | tables: {
35 | DealsTable: {
36 | sort: 'sort-DealsTable', // [ {id: 'value', desc: boolean}],
37 | columns: 'columns-DealsTable' // array of accessor ids from react-table
38 | },
39 | BotPlanner: {
40 | sort: 'sort-BotPlanner', // [ {id: 'value', desc: boolean}],
41 | columns: 'columns-DealsTable'// array of accessor ids from react-table
42 | }
43 | }
44 | }
45 |
46 | const setStorageItem = (id:string, value:string | [] | object) => {
47 |
48 | if(typeof value === 'object') value = JSON.stringify(value)
49 |
50 |
51 | localStorage.setItem(id, value)
52 |
53 | }
54 |
55 | const getStorageItem = (id:string) => {
56 |
57 | const storageItem = localStorage.getItem(id)
58 |
59 | const parsed = (storageItem != undefined) ? tryParseJSON_(storageItem) : undefined
60 |
61 | if(parsed){
62 | return parsed
63 | }
64 |
65 | return storageItem
66 | }
67 |
68 |
69 |
70 | export {
71 | storageItem,
72 | setStorageItem,
73 | getStorageItem
74 | }
--------------------------------------------------------------------------------
/src/app/Components/Selectors/AccountSelector.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Type_ReservedFunds } from '@/types/config';
3 |
4 | import {
5 | FormControl,
6 | InputLabel,
7 | MenuItem,
8 | Select,
9 | ListItemText,
10 | Checkbox,
11 | } from '@mui/material';
12 |
13 | type AccountSelector = {
14 | reservedFunds: Type_ReservedFunds[],
15 | updateAccounts: CallableFunction
16 | }
17 | const AccountSelector = ({reservedFunds, updateAccounts}:AccountSelector) => {
18 | const [selectedAccounts, updateSelectedAccounts] = useState(reservedFunds);
19 | const [selectedAccountIds, updateSelectedAccountIds] = useState(() => reservedFunds.filter(a => a.is_enabled).map(a => a.id));
20 | const updateTempAccounts = (newAccounts: number[]) => {
21 | const selected = reservedFunds.filter(a => newAccounts.includes(a.id))
22 | updateAccounts(selected)
23 | updateSelectedAccounts(selected)
24 | updateSelectedAccountIds(newAccounts)
25 | }
26 |
27 | const onChange = (e: any) => {
28 | updateTempAccounts([...e.target.value])
29 | }
30 |
31 |
32 | const [open, setOpen] = useState(false);
33 | const handleClose = () => setOpen(false);
34 | const handleOpen = () => setOpen(true);
35 |
36 | return (
37 |
38 | Accounts
39 | (selectedAccounts.length > 0) ? selectedAccounts.map(a => a.account_name).join(', ') : ""}
48 | style={{
49 | marginRight: '15px',
50 | width: '100%'
51 | }}
52 | open={open}
53 | onClose={handleClose}
54 | onOpen={handleOpen}
55 | >
56 | {reservedFunds.map(c => {
57 | return (
58 |
59 | - 1} />
60 |
61 |
62 | )
63 | })}
64 |
65 |
66 |
67 | )
68 | }
69 |
70 | export default AccountSelector;
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # 3C Portfolio Manager
2 |
3 | The 3C Portfolio Manager is an essential add-on to your 3Commas experience. It enables you to manage your DCA bots with greater analytics, real-time alerting, and tons of additional features. It's a downloadable desktop application that's supported across Mac OS, Windows, and Linux operating systems. We are always expanding and adding new features! You may have seen this before as the 3C Portfolio Manager. That Google Sheet was the project that paved the way for the success of this application.
4 |
5 | ### Supported Operating Systems
6 |
7 | * Debian:
8 | * 10 - Seems to have a bug when attempting to run an Electron application
9 | * 11 - Works with the `.Appimage`
10 | * Chrome OS - Works with the `.Appimage`
11 | * macOS - M1 and Intel are fully supported
12 | * Windows
13 | * <10 - Untested, if it works let me know!
14 | * 10 - Fully supported
15 | * 11 - Fully supported - Thanks @Karizma!
16 | * Linux
17 | * Ubuntu - Fully Supported
18 | * Other distros are currently untested.
19 |
20 | ## Feedback or Bug Submission
21 |
22 | We welcome all feedback and bug reports as it helps us improve the project for everyone. You can submit these reports in two ways:
23 |
24 | 1. \(Preferred\) If you have a Github account do so on [the issues page](https://github.com/coltoneshaw/3c-portfolio-manager/issues) and select the right type of report.
25 | 2. If you do not have a GitHub account you can use our Google Form [here](https://forms.gle/EZeXuLcR8eosikkAA).
26 |
27 | If you have any issues don't hesitate to reach out to me on Discord @the\_okayest\_human\#1680
28 |
29 | ## Screenshots
30 |
31 | ### Dark Mode!
32 |
33 | 
34 |
35 | ### Active Deals
36 |
37 | 
38 |
39 | ### Bot Planner
40 |
41 | 
42 |
43 | ### Stats - Performance Monitor
44 |
45 | 
46 |
47 | ### Stats - Risk Monitor
48 |
49 | 
50 |
51 | ### Stats - Summary
52 |
53 | 
54 |
55 | ### Settings
56 |
57 | 
58 |
59 |
--------------------------------------------------------------------------------
/src/app/Pages/Stats/Stats.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useAppSelector } from '@/app/redux/hooks';
3 |
4 | import './Stats.scss'
5 | import { Button, ButtonGroup } from '@mui/material';
6 |
7 | import { UpdateDataButton } from '@/app/Components/Buttons/Index'
8 | import { RoiCards, ViewRenderer, useViewRenderer, StatFiltersDiv } from './Components/Index'
9 |
10 |
11 |
12 | const buttonElements:buttonElements = [
13 | {
14 | name: 'Summary Statistics',
15 | id: 'summary-stats'
16 | },
17 | {
18 | name: 'Risk Monitor',
19 | id: 'risk-monitor'
20 | },
21 | {
22 | name: 'Performance Monitor',
23 | id: 'performance-monitor'
24 | }
25 | ]
26 |
27 |
28 | const StatsPage = () => {
29 | const { currentProfile } = useAppSelector(state => state.config);
30 | const { metricsData } = useAppSelector(state => state.threeCommas);
31 |
32 | const {currentView, viewChanger } = useViewRenderer()
33 |
34 | // // this feels redundant
35 | // const [reservedFunds, updateReservedFunds] = useState(() => currentProfile.statSettings.reservedFunds)
36 | // useEffect(() => {
37 | // if (currentProfile.statSettings.reservedFunds.length > 0) updateReservedFunds(currentProfile.statSettings.reservedFunds)
38 | // }, [currentProfile.statSettings.reservedFunds])
39 |
40 | return (
41 | <>
42 |
43 |
44 | {/* This needs to be it's own div to prevent the buttons from taking on the flex properties. */}
45 |
46 |
47 | {
48 | buttonElements.map(button => {
49 | if (button.id === currentView) return viewChanger(button.id)} className="primaryButton-outline">{button.name}
50 | return viewChanger(button.id)} >{button.name}
51 | })
52 | }
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | >
62 |
63 | )
64 | }
65 |
66 |
67 |
68 | export default StatsPage;
--------------------------------------------------------------------------------
/src/types/preload.ts:
--------------------------------------------------------------------------------
1 | import { defaultCurrency, Type_Profile } from '@/types/config';
2 | import { defaultConfig } from "@/utils/defaultConfig";
3 | import type { UpdateDealRequest } from "@/main/3Commas/types/Deals";
4 | import { Type_UpdateFunction } from '@/types/3Commas'
5 | import type { getDealOrders } from '@/main/3Commas/index';
6 | import type {Type_GithubRelease} from '@/app/Repositories/Types/GithubRelease'
7 | import type {BinanceTicketPrice} from '@/app/Repositories/Types/Binance';
8 |
9 | declare global {
10 | interface Window {
11 | mainPreload: mainPreload
12 | }
13 | }
14 |
15 | export interface config {
16 | get: (value: T) => Promise,
17 | profile: (type: 'create', newProfile: Type_Profile, profileId: string) => Promise,
18 | getProfile: (value: string, profileId: string) => Promise< Type_Profile | undefined >,
19 | reset: () => Promise,
20 | set: (key: string, value: any) => Promise,
21 | // setProfile: (key: string, value: any) => Promise,
22 | bulk: (changes: typeof defaultConfig) => Promise
23 | }
24 |
25 |
26 | export type tableNames = 'deals' | 'bots' | 'accountData'
27 | export interface database {
28 | query: (profileId:string, queryString: string) => Promise,
29 | update: (profileId:string, table: tableNames, updateData: object[]) => void,
30 | upsert: (profileId:string, table: tableNames, data: any[], id: string, updateColumn: string) => void,
31 | run: (profileId:string, query: string) => void,
32 | deleteAllData: (profileID?: string) => Promise
33 | }
34 |
35 | export interface api {
36 | update: (type: string, options: Type_UpdateFunction, profileData: Type_Profile) => Promise,
37 | updateBots: (profileData: Type_Profile) => Promise,
38 | getAccountData: (profileData?: Type_Profile, key?: string, secret?: string, mode?: string) => Promise<{ id: number, name: string }[]>,
39 | getDealOrders: (profileData: Type_Profile, dealID: number) => ReturnType,
40 | }
41 |
42 | export interface general {
43 | openLink: (link: string) => void
44 | }
45 |
46 | export interface binance {
47 | coinData: () => Promise< BinanceTicketPrice | false >
48 | }
49 | export interface pm {
50 | versions: () => Promise
51 | }
52 |
53 | interface mainPreload {
54 | deals: {
55 | update: (profileData: Type_Profile, deal: UpdateDealRequest) => Promise
56 | },
57 | api: api,
58 | config: config,
59 | database: database,
60 | general: general,
61 | binance: binance,
62 | pm: pm
63 | };
64 |
65 | export {
66 | mainPreload,
67 | Type_Profile,
68 | defaultConfig,
69 | UpdateDealRequest,
70 | Type_UpdateFunction,
71 | getDealOrders
72 | }
--------------------------------------------------------------------------------
/src/app/Components/icons/Sun.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Sun = () => {
4 |
5 | return (
6 |
7 |
8 |
9 |
11 |
12 |
13 |
15 |
16 |
17 |
19 |
20 |
21 |
23 |
24 |
25 |
27 |
28 |
29 |
31 |
32 |
33 |
35 |
36 |
37 |
39 |
40 |
41 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | )
52 | }
53 |
54 | export default Sun;
--------------------------------------------------------------------------------
/src/main/3Commas/types/Bots.ts:
--------------------------------------------------------------------------------
1 | import {threeCommas_Api_Deals} from './Deals'
2 |
3 | export type Bots = {
4 | id: number,
5 | account_id: number,
6 | is_enabled: boolean,
7 | max_safety_orders: number,
8 | active_safety_orders_count: number,
9 | pairs: string[],
10 | strategy_list: {strategy: string}[],
11 | max_active_deals: number,
12 | active_deals_count: number,
13 | deletable?: boolean,
14 | created_at: string,
15 | updated_at: string,
16 | trailing_enabled: boolean,
17 | tsl_enabled: boolean,
18 | deal_start_delay_seconds: null | string,
19 | stop_loss_timeout_enabled: boolean,
20 | stop_loss_timeout_in_seconds: number,
21 | disable_after_deals_count: null | string,
22 | deals_counter: null | string,
23 | allowed_deals_on_same_pair: null | boolean,
24 | easy_form_supported: boolean,
25 | close_deals_timeout: null | string,
26 | url_secret: string,
27 | name: string,
28 | take_profit: string,
29 | base_order_volume: string,
30 | safety_order_volume: string,
31 | safety_order_step_percentage: string,
32 | take_profit_type: 'total',
33 | type: 'Bot::SingleBot' | 'Bot::MultiBot',
34 | martingale_volume_coefficient: string,
35 | martingale_step_coefficient: string,
36 | stop_loss_percentage: string,
37 | cooldown: string,
38 | btc_price_limit: string,
39 | strategy: 'long' | 'short',
40 | min_volume_btc_24h: string,
41 | profit_currency: 'quote_currency' | 'base_currency',
42 | min_price: null | 'quote_currency',
43 | max_price: null | 'quote_currency',
44 | stop_loss_type: 'stop_loss' | 'stop_loss_and_disable_bot'
45 | safety_order_volume_type: 'quote_currency' | 'percent',
46 | base_order_volume_type: 'quote_currency' | 'percent',
47 | account_name: string,
48 | trailing_deviation: string,
49 | finished_deals_profit_usd: string,
50 | finished_deals_count: string,
51 | leverage_type: 'not_specified' | string,
52 | leverage_custom_value: null,
53 | start_order_type: 'limit' | 'market',
54 | active_deals_usd_profit: string
55 | }
56 |
57 | export type ShowBot = Bots & {
58 | active_deals: threeCommas_Api_Deals[],
59 | bot_events?: {message: string, created_at:string}[]
60 | }
61 |
62 |
63 | export type GetBotsStats = {
64 | overall_stats: {
65 | USD?: string,
66 | BUSD?: string,
67 | USDT?: string,
68 | },
69 | today_stats: {
70 | USD?: string,
71 | BUSD?: string,
72 | USDT?: string,
73 | },
74 | profits_in_usd: {
75 | overall_usd_profit: number,
76 | today_usd_profit: number,
77 | active_deals_usd_profit: number,
78 | funds_locked_in_active_deals: number
79 | }
80 | }
--------------------------------------------------------------------------------
/src/app/Components/Charts/Line/Components/PairSelector.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useAppSelector } from '@/app/redux/hooks';
3 |
4 | import { Select, InputLabel, FormControl, MenuItem, Checkbox, ListItemText, Input, SelectChangeEvent } from '@mui/material';
5 |
6 | import { setStorageItem, getStorageItem, storageItem } from '@/app/Features/LocalStorage/LocalStorage';
7 |
8 |
9 | type PairSelector = {
10 | pairs: { pair: string, opacity: number }[]
11 | pairFilters: string[]
12 | updatePairFilters: any
13 | }
14 |
15 | const PairSelector = ({ pairFilters, updatePairFilters, pairs }: PairSelector) => {
16 | const { currentProfile } = useAppSelector(state => state.config);
17 |
18 | // if any part of the current profile reserved funds changes then we clear the pairs
19 | useEffect(() => {
20 | updatePairFilters([]);
21 | setStorageItem(storageItem.charts.pairByDateFilter, [])
22 |
23 | }, [currentProfile.statSettings.reservedFunds])
24 |
25 | const handleChange = (event: any) => {
26 |
27 | let filter = event.target.value;
28 | // preventing more than 8 items from showing at any given time.
29 | if (filter.length > 8) filter = filter.filter((pair: string, index: number) => index > 0)
30 |
31 | updatePairFilters([...filter]);
32 | setStorageItem(storageItem.charts.pairByDateFilter, [...filter])
33 | };
34 |
35 | const grid = (pairs.length <= 5) ? '1fr' : '1fr 1fr 1fr 1fr 1fr'
36 |
37 |
38 |
39 | return (
40 |
41 |
42 | Show
43 |
44 | }
50 | // @ts-ignore
51 | renderValue={() => (pairFilters.length > 0) ? pairFilters.join() : ""}
52 | style={{ width: "150px" }}
53 |
54 | MenuProps={{
55 | MenuListProps: {
56 | style: {
57 | display: 'grid',
58 | gridTemplateColumns: grid,
59 | }
60 | }
61 | }}
62 |
63 |
64 | >
65 | {pairs.map(pair => (
66 |
67 | - 1} />
68 |
69 |
70 | ))}
71 |
72 |
73 |
74 |
75 |
76 | )
77 | }
78 |
79 | export default PairSelector
--------------------------------------------------------------------------------
/src/app/Pages/MainWindow.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState , useLayoutEffect} from 'react';
2 | import { Route, Redirect } from 'react-router-dom'
3 | import {
4 | BotPlannerPage,
5 | TradingViewPage,
6 | SettingsPage,
7 | StatsPage,
8 | ActiveDealsPage,
9 | DailyStats
10 | } from '@/app/Pages/Index'
11 |
12 | // @ts-ignore
13 | import { version } from '#/package.json';
14 |
15 | import CoinPriceHeader from '@/app/Features/CoinPriceHeader/CoinPriceHeader';
16 | import { useAppSelector } from '@/app/redux/hooks';
17 | import { ChangelogModal } from '@/app/Features/Index';
18 | import { getStorageItem, storageItem } from '@/app/Features/LocalStorage/LocalStorage';
19 |
20 | const MainWindow = () => {
21 |
22 | const { currentProfile } = useAppSelector(state => state.config);
23 |
24 |
25 | const [homePage, updateHomePage] = useState('/activeDeals')
26 |
27 | useLayoutEffect(() => {
28 | if (currentProfile.apis.threeC.key !== "" && currentProfile.apis.threeC.secret !== "") {
29 | updateHomePage(getStorageItem(storageItem.navigation.homePage) ?? '/activeDeals')
30 | return
31 | }
32 |
33 | updateHomePage('/settings')
34 | }, [currentProfile.apis.threeC])
35 |
36 |
37 | // changelog state responsible for opening / closing the changelog
38 | const [openChangelog, setOpenChangelog] = useState(false);
39 |
40 | const handleOpenChangelog = () => {
41 | setOpenChangelog(true);
42 | };
43 |
44 | useEffect(() => {
45 | window.ThreeCPM.Repository.Config.get('general.version')
46 | .then((versionData: string) => {
47 | if (versionData == undefined || versionData != version) {
48 | handleOpenChangelog()
49 |
50 | // setting to false so this does not open again
51 | window.ThreeCPM.Repository.Config.set('general.version', version)
52 | }
53 | })
54 |
55 | }, [])
56 |
57 |
58 |
59 | return (
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
} />
69 | } />
70 | } />
71 | } />
72 | } />
73 | } />
74 |
75 |
76 |
77 | )
78 | }
79 |
80 |
81 | export default MainWindow;
82 |
83 |
--------------------------------------------------------------------------------
/src/app/Repositories/Types/GithubRelease.ts:
--------------------------------------------------------------------------------
1 | type githubAuthor = {
2 | "login": string //"coltoneshaw",
3 | "id": number // 46071821,
4 | "node_id": string //"MDQ6VXNlcjQ2MDcxODIx",
5 | "avatar_url": number //"https://avatars.githubusercontent.com/u/46071821?v=4",
6 | "gravatar_id": string,
7 | "url": string // "https://api.github.com/users/coltoneshaw",
8 | "html_url": string //"https://github.com/coltoneshaw",
9 | "followers_url": string //"https://api.github.com/users/coltoneshaw/followers",
10 | "following_url": string // "https://api.github.com/users/coltoneshaw/following{/other_user}",
11 | "gists_url": string // "https://api.github.com/users/coltoneshaw/gists{/gist_id}",
12 | "starred_url": string // "https://api.github.com/users/coltoneshaw/starred{/owner}{/repo}",
13 | "subscriptions_url": string //"https://api.github.com/users/coltoneshaw/subscriptions",
14 | "organizations_url": string //"https://api.github.com/users/coltoneshaw/orgs",
15 | "repos_url": string // "https://api.github.com/users/coltoneshaw/repos",
16 | "events_url": string //"https://api.github.com/users/coltoneshaw/events{/privacy}",
17 | "received_events_url": string //"https://api.github.com/users/coltoneshaw/received_events",
18 | "type": 'user' // "User",
19 | "site_admin": boolean
20 | }
21 |
22 | type assets = {
23 | "url": string // "https://api.github.com/repos/coltoneshaw/3c-portfolio-manager/releases/assets/46530447",
24 | "id": number // 46530447,
25 | "node_id": string //"RA_kwDOFv81Ic4Cxf-P",
26 | "name": string // "3c-portfolio-manager-1.0.0-linux-x86_64.AppImage",
27 | "label": null | string,
28 | "uploader": githubAuthor,
29 | "content_type": string// "application/octet-stream",
30 | "state": string //"uploaded",
31 | "size": number // 93781957,
32 | "download_count": number // 34,
33 | "created_at": string //"2021-10-08T15:47:44Z",
34 | "updated_at": string //"2021-10-08T15:48:43Z",
35 | "browser_download_url": string //"https://github.com/coltoneshaw/3c-portfolio-manager/releases/download/v1.0.0/3c-portfolio-manager-1.0.0-linux-x86_64.AppImage"
36 | }
37 |
38 | export type Type_GithubRelease = {
39 | "url": string,
40 | "assets_url": string,
41 | "upload_url":string,
42 | "html_url": string,
43 | "id": number,
44 | "author": githubAuthor,
45 | "node_id": string // "RE_kwDOFv81Ic4DClhw",
46 | "tag_name": string // "v1.0.0",
47 | "target_commitish": string // "main",
48 | "name": string //"v1.0.0",
49 | "draft": false,
50 | "prerelease": false,
51 | "created_at": string //"2021-10-08T14:39:46Z",
52 | "published_at": string // "2021-10-08T14:40:27Z",
53 | "assets": assets[],
54 | "tarball_url": string // "https://api.github.com/repos/coltoneshaw/3c-portfolio-manager/tarball/v1.0.0",
55 | "zipball_url": string // "https://api.github.com/repos/coltoneshaw/3c-portfolio-manager/zipball/v1.0.0",
56 | "body": string // it's long.
57 | }
--------------------------------------------------------------------------------
/src/app/Pages/ActiveDeals/Components/Subrow.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | import { useAppSelector } from "@/app/redux/hooks";
4 | import Box from '@mui/material/Box';
5 | import Tab from '@mui/material/Tab';
6 | import TabContext from '@mui/lab/TabContext';
7 | import TabList from '@mui/lab/TabList';
8 | import TabPanel from '@mui/lab/TabPanel';
9 | import { OrderTimeline, DCA, Orders } from './SubrowTabs/Index'
10 | import type { Type_MarketOrders } from '@/types/3Commas'
11 |
12 |
13 | function SubRows({ row, visibleColumns, ordersData, loading }: any) {
14 | if (loading) {
15 | return (
16 |
17 |
18 |
19 | Loading...
20 |
21 |
22 | );
23 | }
24 |
25 | const [activeTab, setActiveTab] = React.useState('timeline');
26 | const handleChange = (event: any, newValue: string) => {
27 | setActiveTab(newValue);
28 | };
29 |
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | function SubRowAsync({ row, visibleColumns }: any) {
59 | const [loading, setLoading] = useState(true);
60 | const [ordersData, setOrdersData] = useState([]);
61 |
62 | const { currentProfile } = useAppSelector(state => state.config);
63 |
64 | useEffect(() => {
65 | const getDealOrdersPromise = window.ThreeCPM.Repository.API.getDealOrders(currentProfile, row.original.id)
66 |
67 | Promise.all([getDealOrdersPromise])
68 | .then(([getDealOrdersResult]) => {
69 | setOrdersData(getDealOrdersResult.reverse());
70 | setLoading(false);
71 | })
72 |
73 | }, []);
74 |
75 | return (
76 |
82 | );
83 | }
84 |
85 | export default SubRowAsync
86 |
--------------------------------------------------------------------------------
/src/app/Pages/Stats/Stats.scss:
--------------------------------------------------------------------------------
1 | .tooltip {
2 | border-radius: .25em;
3 | background-color: var(--color-background);
4 | opacity: .9;
5 | color: var(--color-text-lightbackground);
6 | padding: 1em;
7 | text-align: left;
8 | margin-left: 2em;
9 | margin-right: 2em;
10 | }
11 |
12 | .tooltip h4, .tooltip p {
13 | padding: 0;
14 | margin: 0;
15 | }
16 |
17 | .tooltip h4{
18 | text-align: center;
19 | padding-bottom: 1em;
20 | }
21 |
22 | .chartTitle{
23 | text-align: center;
24 | padding: 0;
25 | margin: 0;
26 | font-weight: 300;
27 | }
28 |
29 | .speedDialContainer{
30 | width: 100%;
31 | }
32 |
33 | .speedDials{
34 | margin: auto;
35 | min-height: 300px;
36 | height: 400px;
37 | width: 30%;
38 | padding: 5%
39 | }
40 |
41 |
42 | .statHeaderRow{
43 | padding-bottom: 15px;
44 | }
45 |
46 | .filters {
47 | font-weight: 400;
48 | font-size: .9em;
49 | flex-basis: 30%;
50 | align-items: center;
51 | justify-content: flex-end;
52 |
53 | }
54 |
55 | .menuButtons{
56 | margin: auto;
57 | flex-basis: 70%;
58 | min-width: 730px;
59 | }
60 |
61 |
62 | @media only screen and (max-width: 1500px) {
63 |
64 | .statHeaderRow{
65 | flex-wrap: wrap;
66 | }
67 | .filters{
68 | flex-basis: 100%;
69 | justify-content: center;
70 | }
71 |
72 | .menuButtons{
73 | flex-basis: 100%;
74 | justify-content: center;
75 | align-self: center;
76 | }
77 |
78 | }
79 | .filters p {
80 | padding: 0px 10px 0px 10px
81 | }
82 |
83 | .stat-chart {
84 | margin: 0;
85 | }
86 |
87 | .riskMonitorDiv {
88 | display: flex;
89 | flex-direction: column;
90 | width: 100%;
91 | height: 100%;
92 |
93 | .speedometerDiv {
94 | display: flex;
95 | flex-direction: row;
96 | justify-content: space-between;
97 | padding-bottom: 32px;
98 | width: 100%;
99 | }
100 |
101 | .chartDiv{
102 | display: flex;
103 | flex-direction: column;
104 | width: 100%;
105 | }
106 |
107 | @media only screen and (min-width: 1550px) {
108 | flex-direction: row;
109 | flex-wrap: nowrap;
110 |
111 | .boxData {
112 | margin-bottom: 32px;
113 | }
114 |
115 |
116 | .speedometerDiv {
117 | flex-direction: column;
118 | width: 400px;
119 | padding-bottom: 0;
120 |
121 | .boxData:last-child {
122 | margin-bottom: 0;
123 | }
124 | }
125 | .chartDiv{
126 | flex: 1;
127 | justify-content: space-between;
128 |
129 | .boxData{
130 | height: 50%;
131 | margin-left: 32px;
132 | }
133 |
134 | .boxData:last-child {
135 | margin-bottom: 0;
136 | // margin-top: 32px;
137 | }
138 |
139 | }
140 |
141 | }
142 | }
--------------------------------------------------------------------------------