diff --git a/.gitignore b/.gitignore index 215d2c5..7dbbbf3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Local files +/data +/mlruns + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/models/backtest.py b/app/models/backtest.py index 4d66ff8..cd0c5f7 100644 --- a/app/models/backtest.py +++ b/app/models/backtest.py @@ -8,7 +8,7 @@ class Backtest(db.Model): start_date = db.Column(db.Date) end_date = db.Column(db.Date) inital_cash = db.Column(db.Integer) - fee = db.Column(db.Integer) + fee = db.Column(db.Float) # status = db.Column(db.String(50)) created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) @@ -29,12 +29,14 @@ class Result(db.Model): __tablename__ = 'results' id = db.Column(db.Integer, primary_key=True) backtest_id = db.Column(db.Integer, db.ForeignKey('backtests.id'), nullable=False) + strategy = db.Column(db.String(255)) total_return = db.Column(db.Numeric(10, 2)) number_of_trades = db.Column(db.Integer) winning_trades = db.Column(db.Integer) losing_trades = db.Column(db.Integer) max_drawdown = db.Column(db.Numeric(10, 2)) sharpe_ratio = db.Column(db.Numeric(10, 2)) + is_best = db.Column(db.Boolean, default=False, nullable=True) class Metric(db.Model): __tablename__ = 'metrics' diff --git a/app/routes/backtest.py b/app/routes/backtest.py index 64a03e7..15457c7 100644 --- a/app/routes/backtest.py +++ b/app/routes/backtest.py @@ -3,7 +3,7 @@ from app.models.backtest import Backtest, Result from app import db from flask_jwt_extended import jwt_required -from app.services.backtest_service import run_backtest_by_id +from app.services.backtest_service import run_backtest_by_id, run_and_evaluate_backtest from app.services.kafka_service import kafka_service from flask_cors import CORS, cross_origin @@ -34,10 +34,11 @@ def run_backtest(): db.session.commit() - # Publish backtest to Kafka for processing - kafka_service.produce('backtest_scenes', { - "backtest_id": new_backtest.id - }) + # # Publish backtest to Kafka for processing + # kafka_service.produce('backtest_scenes', { + # "backtest_id": new_backtest.id + # }) + run_and_evaluate_backtest(backtest_id=new_backtest.id, symbol=symbol, initial_cash= inital_cash, fee=fee, start_date=start_date,end_date= end_date) return jsonify({"msg": "Backtest created and published to Kafka", "backtest_id": new_backtest.id}), 201 @@ -55,7 +56,7 @@ def get_backtests(): 'symbol': backtest.symbol, 'start_date': backtest.start_date.strftime('%Y-%m-%d'), 'end_date': backtest.end_date.strftime('%Y-%m-%d'), - 'initial_cash': backtest.initial_cash, + 'inital_cash': backtest.inital_cash, 'fee': backtest.fee, 'created_at': backtest.created_at.strftime('%Y-%m-%d %H:%M:%S') }) @@ -63,7 +64,7 @@ def get_backtests(): @bp.route('/backtests//results', methods=['GET']) @jwt_required() -@cross_origin(origin='*') +@cross_origin(origins='*') def get_backtest_results(backtest_id): results = Result.query.filter_by(backtest_id=backtest_id).all() if not results: @@ -73,12 +74,14 @@ def get_backtest_results(backtest_id): for result in results: result_list.append({ 'id': result.id, + 'strategy': result.strategy, 'total_return': float(result.total_return), 'number_of_trades': result.number_of_trades, 'winning_trades': result.winning_trades, 'losing_trades': result.losing_trades, 'max_drawdown': float(result.max_drawdown), - 'sharpe_ratio': float(result.sharpe_ratio) + 'sharpe_ratio': float(result.sharpe_ratio), + 'is_best': result.is_best }) return jsonify({'results': result_list}), 200 diff --git a/app/services/backtest_service.py b/app/services/backtest_service.py index 771c505..abe2cca 100644 --- a/app/services/backtest_service.py +++ b/app/services/backtest_service.py @@ -13,28 +13,25 @@ def run_backtest_by_id(backtest_id): run_and_evaluate_backtest(backtest_id=backtest_id, symbol=backtest.symbol, initial_cash=backtest.inital_cash, fee=backtest.fee, start_date=backtest.start_date, end_date = backtest.end_date) - # for res in results: - # result = Result(**res) - # db.session.add(result) - # # Log metrics to MLflow - # metrics = { - # "total_return": res.total_return, - # "number_of_trades": res.number_of_trades, - # "winning_trades": res.winning_trades, - # "losing_trades": res.losing_trades, - # "max_drawdown": res.max_drawdown, - # "sharpe_ratio": res.sharpe_ratio - # } - # mlflow_service.log_metrics(run_name=f"Backtest_{backtest_id}", metrics=metrics) - - # Publish result to Kafka - # kafka_service.produce('backtest_results', { - # "backtest_id": backtest_id, - # "metrics": metrics - # }) - # db.session.commit() +def score_backtest(result, min_return, max_return, min_sharpe, max_sharpe, min_drawdown, max_drawdown): + weights = { + 'total_return': 0.4, + 'sharpe_ratio': 0.4, + 'max_drawdown': 0.2, + } + + normalized_return = (result['total_return'] - min_return) / (max_return - min_return) + normalized_sharpe = (result['sharpe_ratio'] - min_sharpe) / (max_sharpe - min_sharpe) + normalized_drawdown = (max_drawdown - result['max_drawdown']) / (max_drawdown - min_drawdown) + + score = ( + weights['total_return'] * normalized_return + + weights['sharpe_ratio'] * normalized_sharpe + + weights['max_drawdown'] * normalized_drawdown + ) + return score def run_and_evaluate_backtest(backtest_id, symbol, initial_cash, fee, start_date, end_date): strategies = [ @@ -44,33 +41,36 @@ def run_and_evaluate_backtest(backtest_id, symbol, initial_cash, fee, start_date ] results = [] + result_objects = [] for strategy in strategies: + print(strategy, symbol, initial_cash, fee, start_date, end_date) result = run_backtest(strategy, symbol, initial_cash, fee, start_date, end_date) - result.backtest_id = backtest_id - result = Result(**result) - db.session.add(result) + result['backtest_id'] = backtest_id + result['strategy'] = strategy.__name__ + + result_obj = Result(**result) + db.session.add(result_obj) + result_objects.append(result_obj) - # Log metrics to MLflow metrics = { - "total_return": result.total_return, - "number_of_trades": result.number_of_trades, - "winning_trades": result.winning_trades, - "losing_trades": result.losing_trades, - "max_drawdown": result.max_drawdown, - "sharpe_ratio": result.sharpe_ratio + "total_return": result['total_return'], + "number_of_trades": result['number_of_trades'], + "winning_trades": result['winning_trades'], + "losing_trades": result['losing_trades'], + "max_drawdown": result['max_drawdown'], + "sharpe_ratio": result['sharpe_ratio'] } mlflow_service.log_metrics(run_name=f"Backtest_{backtest_id}", metrics=metrics) - - # Publish result to Kafka - # kafka_service.produce('backtest_results', { - # "backtest_id": backtest_id, - # "metrics": metrics - # }) + + # publish results to Kafka + kafka_service.produce('backtest_results', { + "backtest_id": backtest_id, + "metrics": metrics + }) + db.session.commit() - results.append(result) - # Determine the min and max values for normalization min_return = min(result['total_return'] for result in results) max_return = max(result['total_return'] for result in results) min_sharpe = min(result['sharpe_ratio'] for result in results) @@ -78,17 +78,21 @@ def run_and_evaluate_backtest(backtest_id, symbol, initial_cash, fee, start_date min_drawdown = min(result['max_drawdown'] for result in results) max_drawdown = max(result['max_drawdown'] for result in results) - # Score each strategy - scores = [score_backtest(result) for result in results] + scores = [score_backtest(result, min_return, max_return, min_sharpe, max_sharpe, min_drawdown, max_drawdown) for result in results] - # Select the best strategy best_strategy_index = scores.index(max(scores)) - best_strategy = strategies[best_strategy_index] + for idx, result_obj in enumerate(result_objects): + result_obj.is_best = (idx == best_strategy_index) + + db.session.commit() + print("Best Strategy:") - print(best_strategy.__name__) + print(strategies[best_strategy_index].__name__) print("Score:") print(scores[best_strategy_index]) print("Metrics:") print(results[best_strategy_index]) - return results \ No newline at end of file + + return results + diff --git a/app/services/mlflow_service.py b/app/services/mlflow_service.py index ed1d7fb..2427956 100644 --- a/app/services/mlflow_service.py +++ b/app/services/mlflow_service.py @@ -2,12 +2,24 @@ import mlflow.sklearn class MLflowService: - def __init__(self, tracking_uri): + def __init__(self, tracking_uri, experiment_name): mlflow.set_tracking_uri(tracking_uri) + self.experiment_name = experiment_name + self.experiment_id = self.get_or_create_experiment_id(experiment_name) + + def get_or_create_experiment_id(self, experiment_name): + experiment = mlflow.get_experiment_by_name(experiment_name) + if experiment is None: + experiment_id = mlflow.create_experiment(experiment_name) + else: + experiment_id = experiment.experiment_id + return experiment_id def log_metrics(self, run_name, metrics): - with mlflow.start_run(run_name=run_name): + with mlflow.start_run(experiment_id=self.experiment_id, run_name=run_name): for key, value in metrics.items(): + print(key, value) mlflow.log_metric(key, value) -mlflow_service = MLflowService(tracking_uri='http://localhost:5050') +# Initialize the MLflowService with the desired experiment name +mlflow_service = MLflowService(tracking_uri='http://localhost:5050', experiment_name='Backtest_Results') diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5397955..18e35e1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,13 +8,19 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@mui/icons-material": "^5.15.20", + "@mui/material": "^5.15.20", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.7.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.2.1", "react-scripts": "5.0.1", + "react-toastify": "^10.0.5", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -2461,6 +2467,179 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.11.5", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz", + "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.2", + "@emotion/serialize": "^1.1.4", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2562,6 +2741,44 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.3.tgz", + "integrity": "sha512-1ZpCvYf788/ZXOhRQGFxnYQOVgeU+pi0i+d0Ow34La7qjIXETi6RNswGVKkA6KcDO8/+Ysu2E/CeUmmeEBDvTg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.3" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.6.tgz", + "integrity": "sha512-qiTYajAnh3P+38kECeffMSQgbvXty2VB6rS+42iWR4FPIlZjLK84E9qtLnMTLIpPz2znD/TaFqaiavMUrS+Hcw==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.3" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", + "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.3.tgz", + "integrity": "sha512-XGndio0l5/Gvd6CLIABvsav9HHezgDFFhDfHk1bvLfr9ni8dojqLSvBbotJEjmIwNHL7vK4QzBJTdBRoB+c1ww==", + "license": "MIT" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3441,6 +3658,272 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "license": "MIT" }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.15.20", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.20.tgz", + "integrity": "sha512-DoL2ppgldL16utL8nNyj/P12f8mCNdx/Hb/AJnX9rLY4b52hCMIx1kH83pbXQ6uMy6n54M3StmEbvSGoj2OFuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.15.20", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.20.tgz", + "integrity": "sha512-oGcKmCuHaYbAAoLN67WKSXtHmEgyWcJToT1uRtmPyxMj9N5uqwc/mRtEnst4Wj/eGr+zYH2FiZQ79v9k7kSk1Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.15.20", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.20.tgz", + "integrity": "sha512-tVq3l4qoXx/NxUgIx/x3lZiPn/5xDbdTE8VrLczNpfblLYZzlrbxA7kb9mI8NoBF6+w9WE9IrxWnKK5KlPI2bg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/core-downloads-tracker": "^5.15.20", + "@mui/system": "^5.15.20", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.20", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/@mui/private-theming": { + "version": "5.15.20", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.20.tgz", + "integrity": "sha512-BK8F94AIqSrnaPYXf2KAOjGZJgWfvqAVQ2gVR3EryvQFtuBnG6RwodxrCvd3B48VuMy6Wsk897+lQMUxJyk+6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.15.20", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz", + "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.15.20", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.20.tgz", + "integrity": "sha512-LoMq4IlAAhxzL2VNUDBTQxAb4chnBe8JvRINVNDiMtHE2PiPOoHlhOPutSxEbaL5mkECPVWSv6p8JEV+uykwIA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.15.20", + "@mui/styled-engine": "^5.15.14", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.20", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.14", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", + "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.15.20", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.20.tgz", + "integrity": "sha512-mAbYx0sovrnpAu1zHc3MDIhPqL8RPVC5W5xcO1b7PiSCJPtckIZmBkp8hefamAvUiAV8gpfMOM6Zb+eSisbI2A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.11", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -3565,6 +4048,16 @@ } } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4780,6 +5273,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -6641,6 +7143,15 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -7668,6 +8179,16 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -9200,6 +9721,12 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -9922,6 +10449,21 @@ "he": "bin/he" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -16276,6 +16818,15 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "license": "MIT" }, + "node_modules/react-icons": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", + "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -16364,6 +16915,35 @@ } } }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -17850,6 +18430,12 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9c9fc77..0380667 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,13 +3,19 @@ "version": "0.1.0", "private": true, "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@mui/icons-material": "^5.15.20", + "@mui/material": "^5.15.20", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.7.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.2.1", "react-scripts": "5.0.1", + "react-toastify": "^10.0.5", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/frontend/src/App.js b/frontend/src/App.js index 383d1b6..4d3076e 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -3,9 +3,11 @@ import React, { useState, useEffect } from 'react'; import Login from './Login'; import BacktestForm from './BacktestForm'; +import BacktestResultsTable from './BacktestResultsTable'; function App() { const [token, setToken] = useState(''); + const [currentView, setCurrentView] = useState('form'); // New state to track the current view // Function to check if user is authenticated useEffect(() => { @@ -13,9 +15,9 @@ function App() { if (storedToken) { setToken(storedToken); } - }, [token]); + }, []); - // Function to handle logout (optional) + // Function to handle logout const handleLogout = () => { localStorage.removeItem('token'); setToken(''); @@ -28,16 +30,38 @@ function App() {
- + {currentView === 'form' ? ( + + ) : ( + + )}
) : ( diff --git a/frontend/src/BacktestForm.js b/frontend/src/BacktestForm.js index 88155a8..caff4fc 100644 --- a/frontend/src/BacktestForm.js +++ b/frontend/src/BacktestForm.js @@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react'; import axios from 'axios'; +import { toast, ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; const API_BASE_URL = 'http://localhost:5000'; @@ -64,7 +66,10 @@ const BacktestForm = ({ token }) => { // Handle successful response console.log('Backtest created:', response.data); - // Reset form fields after successful submission (optional) + toast.success('Backtest created successfully! Please check the results table!'); + + + // Reset form fields after successful submission setFormData({ coin: '', name: '', @@ -76,6 +81,7 @@ const BacktestForm = ({ token }) => { } catch (error) { console.error('Error creating backtest:', error); // Handle error, show error message to user + toast.error('Something went wrong!'); } }; @@ -171,6 +177,7 @@ const BacktestForm = ({ token }) => { + ); }; diff --git a/frontend/src/BacktestResultModal.js b/frontend/src/BacktestResultModal.js index 19222f2..50c3888 100644 --- a/frontend/src/BacktestResultModal.js +++ b/frontend/src/BacktestResultModal.js @@ -1,5 +1,8 @@ import React, { useState, useEffect } from 'react'; +import { FaCheckCircle, FaTimesCircle } from 'react-icons/fa'; import axios from 'axios'; +import { Modal, Box, Typography, IconButton, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@mui/material'; +import { Close as CloseIcon } from '@mui/icons-material'; const API_BASE_URL = 'http://localhost:5000'; @@ -24,41 +27,80 @@ const BacktestResultModal = ({ backtest, onClose, token }) => { }, [backtest.id, token]); return ( -
-
-

{backtest.name} Details

- - - - - - - - - - - - - - {results.map((result) => ( - - - - - - - - - ))} - -
Total ReturnNumber of TradesWinning TradesLosing TradesMax DrawdownSharpe Ratio
{result.total_return}{result.number_of_trades}{result.winning_trades}{result.losing_trades}{result.max_drawdown}{result.sharpe_ratio}
-
-
+ + + + {backtest.name} Details + theme.palette.grey[500], + }} + > + + + + + + + + Strategy + Total Return + Number of Trades + Winning Trades + Losing Trades + Max Drawdown + Sharpe Ratio + Is Best + + + + {results.map((result) => ( + + {result.strategy} + {result.total_return} + {result.number_of_trades} + {result.winning_trades} + {result.losing_trades} + {result.max_drawdown} + {result.sharpe_ratio} + + {result.is_best ? ( + + + Best + + ) : ( + + + Not the Best + + )} + + + ))} + +
+
+
+
); }; diff --git a/frontend/src/BacktestResultsTable.js b/frontend/src/BacktestResultsTable.js index 8ab1ff2..4ae0b18 100644 --- a/frontend/src/BacktestResultsTable.js +++ b/frontend/src/BacktestResultsTable.js @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import axios from 'axios'; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Typography, Box } from '@mui/material'; import BacktestResultModal from './BacktestResultModal'; const API_BASE_URL = 'http://localhost:5000'; @@ -34,36 +35,44 @@ const BacktestResultsTable = ({ token }) => { }; return ( -
-

Backtest Results

- - - - - - - - - - - - - {backtests.map((backtest) => ( - handleRowClick(backtest)} className="cursor-pointer"> - - - - - - - - ))} - -
NameSymbolStart DateEnd DateInitial CashFee
{backtest.name}{backtest.symbol}{backtest.start_date}{backtest.end_date}{backtest.initial_cash}{backtest.fee}
+ + + Backtest Results + + + + + + Name + Symbol + Start Date + End Date + Initial Cash + Fee + + + + {backtests.map((backtest) => ( + handleRowClick(backtest)} + sx={{ cursor: 'pointer', '&:hover': { backgroundColor: '#f5f5f5' } }} + > + {backtest.name} + {backtest.symbol} + {backtest.start_date} + {backtest.end_date} + {backtest.inital_cash} + {backtest.fee} + + ))} + +
+
{selectedBacktest && ( )} -
+ ); }; diff --git a/mlflow.db b/mlflow.db deleted file mode 100644 index a37a477..0000000 Binary files a/mlflow.db and /dev/null differ diff --git a/scripts/backtest_runner.py b/scripts/backtest_runner.py index 615bffa..d760c3d 100644 --- a/scripts/backtest_runner.py +++ b/scripts/backtest_runner.py @@ -99,43 +99,43 @@ def next(self): if self.stoch.lines.percK[0] > self.params.stoch_high and self.stoch.lines.percK[-1] <= self.params.stoch_high: self.sell() + def run_backtest(strategy_class, symbol, initial_cash, fee, start_date, end_date): data = fetch_data(symbol, start_date, end_date) - print('we back to data') data_feed = bt.feeds.PandasData(dataname=data) - # Initialize cerebro cerebro = bt.Cerebro() cerebro.addstrategy(strategy_class) cerebro.adddata(data_feed) cerebro.broker.set_cash(float(initial_cash)) - cerebro.broker.setcommission(commission=0.002) + cerebro.broker.setcommission(commission=fee) - # Add analyzers cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='tradeanalyzer') cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown') cerebro.addanalyzer(bt.analyzers.SharpeRatio_A, _name='sharpe') - print(1) - # Print starting conditions - print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}') - # Run backtest + starting_value = cerebro.broker.getvalue() + print(f'Starting Portfolio Value: {starting_value:.2f}') + result = cerebro.run() - print(2) - # Extracting backtest metrics total_return = cerebro.broker.getvalue() / initial_cash - 1 - number_of_trades = result[0].analyzers.tradeanalyzer.get_analysis()['total']['closed'] - winning_trades = result[0].analyzers.tradeanalyzer.get_analysis()['won']['total'] - losing_trades = result[0].analyzers.tradeanalyzer.get_analysis()['lost']['total'] - max_drawdown = result[0].analyzers.drawdown.get_analysis()['max']['drawdown'] - sharpe_ratio = result[0].analyzers.sharpe.get_analysis().get('sharperatio', 0.0) - # Print ending conditions - print(f'Ending Portfolio Value: {cerebro.broker.getvalue():.2f}') - print(1) + # Extract trade analysis metrics + trade_analysis = result[0].analyzers.tradeanalyzer.get_analysis() + number_of_trades = trade_analysis.get('total', {}).get('closed', 0) + winning_trades = trade_analysis.get('won', {}).get('total', 0) + losing_trades = trade_analysis.get('lost', {}).get('total', 0) + + drawdown_analysis = result[0].analyzers.drawdown.get_analysis() + max_drawdown = drawdown_analysis.get('max', {}).get('drawdown', 0.0) + + sharpe_analysis = result[0].analyzers.sharpe.get_analysis() + sharpe_ratio = sharpe_analysis.get('sharperatio', 0.0) + + ending_value = cerebro.broker.getvalue() + print(f'Ending Portfolio Value: {ending_value:.2f}') - # Return results as a dictionary return { 'backtest_id': 0, 'total_return': total_return, @@ -146,20 +146,19 @@ def run_backtest(strategy_class, symbol, initial_cash, fee, start_date, end_date 'sharpe_ratio': sharpe_ratio } + + def score_backtest(result): - # Define weights for each metric weights = { 'total_return': 0.4, 'sharpe_ratio': 0.4, 'max_drawdown': 0.2, } - # Normalize the values (example with min-max normalization) normalized_return = (result['total_return'] - min_return) / (max_return - min_return) normalized_sharpe = (result['sharpe_ratio'] - min_sharpe) / (max_sharpe - min_sharpe) normalized_drawdown = (max_drawdown - result['max_drawdown']) / (max_drawdown - min_drawdown) - # Calculate the score score = ( weights['total_return'] * normalized_return + weights['sharpe_ratio'] * normalized_sharpe + @@ -185,7 +184,7 @@ def score_backtest(result): for strategy in strategies: result = run_backtest(strategy, symbol, initial_cash, fee, start_date, end_date) results.append(result) - + print(results) # Determine the min and max values for normalization min_return = min(result['total_return'] for result in results) max_return = max(result['total_return'] for result in results)