From fc6662a6c94c064ea67aaf03a70eb1151d8f3ee9 Mon Sep 17 00:00:00 2001
From: simon <155122848+simonpotel@users.noreply.github.com>
Date: Thu, 29 Jan 2026 20:16:57 +0100
Subject: [PATCH 01/18] delete: useless tools, integrations, and scan route
---
.gitignore | 8 -
README.md | 36 +--
api/api.py | 6 -
api/routes/scan.py | 179 --------------
integrations/plotly/README.md | 0
integrations/plotly/main.py | 35 ---
integrations/plotly/utils/api_requests.py | 34 ---
integrations/plotly/utils/files.py | 12 -
integrations/plotly/utils/plots.py | 202 ----------------
requirements.txt | 8 -
tests/fibonacci.py | 73 ------
tests/integrations/plotly_unit.sh | 4 -
tests/integrations/strategy_viewer.sh | 13 -
tests/test_api_endpoints.py | 185 --------------
.../tools/data_fetch/binance-fetch-sample.py | 19 --
.../tools/data_fetch/binance-update-sample.py | 12 -
tests/tools/data_fetch/yahoo-fetch-sample.py | 133 -----------
tests/tools/data_fetch/yahoo-update-sample.py | 13 -
tools/README.md | 8 +-
tools/auto_fetch/auto-fetch.py | 5 -
tools/lab/main.py | 152 ------------
tools/strategy_scanner/README.md | 37 ---
tools/strategy_scanner/_results/README.md | 0
.../binance-scanner_sample.py | 18 --
tools/strategy_scanner/binance/scanner.py | 226 ------------------
25 files changed, 3 insertions(+), 1415 deletions(-)
delete mode 100644 api/routes/scan.py
delete mode 100644 integrations/plotly/README.md
delete mode 100644 integrations/plotly/main.py
delete mode 100644 integrations/plotly/utils/api_requests.py
delete mode 100644 integrations/plotly/utils/files.py
delete mode 100644 integrations/plotly/utils/plots.py
delete mode 100644 tests/fibonacci.py
delete mode 100644 tests/integrations/plotly_unit.sh
delete mode 100644 tests/integrations/strategy_viewer.sh
delete mode 100644 tests/test_api_endpoints.py
delete mode 100644 tests/tools/data_fetch/binance-fetch-sample.py
delete mode 100644 tests/tools/data_fetch/binance-update-sample.py
delete mode 100644 tests/tools/data_fetch/yahoo-fetch-sample.py
delete mode 100644 tests/tools/data_fetch/yahoo-update-sample.py
delete mode 100644 tools/lab/main.py
delete mode 100644 tools/strategy_scanner/README.md
delete mode 100644 tools/strategy_scanner/_results/README.md
delete mode 100644 tools/strategy_scanner/binance-scanner_sample.py
delete mode 100644 tools/strategy_scanner/binance/scanner.py
diff --git a/.gitignore b/.gitignore
index 459dbc7..d3dfd06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,22 +1,14 @@
*.pyc
.DS_Store
-logs/*
-
config/*
!config/README.md
-
data/bank/*
api/strategies/*
!api/strategies/default.py
!api/strategies/rsi_example.py
!api/strategies/README.md
-integrations/plotly/saved_results/*
-
-
-tools/strategy_scanner/_results/*
-!tools/strategy_scanner/_results/README.md
.env
\ No newline at end of file
diff --git a/README.md b/README.md
index db22861..124b309 100644
--- a/README.md
+++ b/README.md
@@ -104,41 +104,9 @@ python api/api.py
- Access the Swagger UI documentation: `http://127.0.0.1:5002/docs`
- Comprehensive endpoint documentation and testing interface
-4. **Running Tests**
-> [!TIP]
-> Run fixtures of the API with : `pytest tests/test_api_endpoints.py`
-
-5. **Visualization Tools**
-
- a. Generate Plotly Charts:
- ```bash
- sh tests/integrations/plotly_unit.sh
- ```
-
- b. Discord Integration:
- - Configure: `integrations/discord_chat_bot/bot.py`
- - Launch:
- ```bash
- sh sh/discord_chat_bot.sh
- ```
-
- c. Custom Interface Development
- - Framework available for building custom web interfaces
- - Reference the Smartswap project for implementation examples
-
-6. **Automated Data Collection**
+4. **Automated Data Collection**
- Configure: `tools/auto_fetch/config.json`
- - Launch collector:
- ```bash
- sh sh/auto_fetch.sh
- ```
-
-## Visualization Examples
-
-
-
-
-
+ - Launch collector: `python tools/auto_fetch/auto-fetch.py`
## License
diff --git a/api/api.py b/api/api.py
index 94a2ea8..6ce865c 100644
--- a/api/api.py
+++ b/api/api.py
@@ -15,7 +15,6 @@
from core.analysis import analyse
from routes.analyse import register_analyse_routes
from routes.analyse_custom import register_analyse_custom_routes
-from routes.scan import register_scan_routes
from routes.strategies import register_strategy_routes
from routes.get_tokens import register_get_tokens_routes
from routes.get_tokens_stats import register_get_tokens_stats_routes
@@ -40,10 +39,6 @@ def load_config():
def reload_loguru_config():
logger.remove()
logger.add(sys.stdout, level="DEBUG")
- if config['server']['debug']:
- logger.add(r"logs/api/debug.log", level="DEBUG")
- else:
- logger.add(r"logs/api/logs.log", level="INFO")
def import_signals_and_indicators(strategies_folder="strategies"):
strategies = {}
@@ -125,7 +120,6 @@ def after_request(response):
register_analyse_routes(app, strategies, analyse)
register_analyse_custom_routes(app, analyse)
- register_scan_routes(app, strategies, analyse)
register_strategy_routes(app, strategies)
register_get_tokens_routes(app)
register_get_tokens_stats_routes(app)
diff --git a/api/routes/scan.py b/api/routes/scan.py
deleted file mode 100644
index 3fdd859..0000000
--- a/api/routes/scan.py
+++ /dev/null
@@ -1,179 +0,0 @@
-from flask import jsonify, request
-from datetime import datetime
-from loguru import logger
-from stats.positions import get_position_stats
-from stats.drawdown import get_drawdowns_stats
-from stats.advanced import get_advanced_stats
-from core.file_utils import get_file_data
-from concurrent.futures import ThreadPoolExecutor, as_completed
-
-def register_scan_routes(app, strategies, analyse_func):
- @app.route('/QTSBE/scan')
- def scan_endpoint():
- ts_format = "%Y-%m-%d %H:%M:%S"
-
- pairs = request.args.get('pairs')
- strategy = request.args.get('strategy')
- start_ts = request.args.get('start_ts')
- end_ts = request.args.get('end_ts')
- multi_positions = request.args.get('multi_positions')
- position_type = request.args.get('position_type', 'long')
- concurrency = request.args.get('concurrency', 5)
-
- try:
- concurrency = int(concurrency)
- if concurrency <= 0:
- concurrency = 1
- except Exception:
- concurrency = 5
-
- if not pairs or not strategy:
- return jsonify({"error": "pairs and strategy parameters are required"}), 400
-
- if position_type not in ['long', 'short']:
- return jsonify({"error": "position_type must be either 'long' or 'short'"}), 400
-
- try:
- pairs_list = pairs.split(',')
- pairs_list = [pair.strip() for pair in pairs_list]
- except Exception as e:
- return jsonify({"error": "pairs must be a comma-separated list"}), 400
-
- if start_ts:
- start_ts = datetime.strptime(start_ts.strip("'").strip('"'), ts_format)
- if end_ts:
- end_ts = datetime.strptime(end_ts.strip("'").strip('"'), ts_format)
-
- multi_positions = bool(multi_positions) and (lambda s: s.lower() in {'true'})(multi_positions)
-
- def analyse_single_pair(pair):
- data = get_file_data(pair)
- if not data:
- return {
- "pair": pair,
- "error": f"No data found for pair {pair}"
- }
-
- data = [(
- (str(row[0]) + " 00:00:00" if isinstance(row[0], str) and len(row[0]) == 10
- else str(row[0]) if isinstance(row[0], (int, float))
- else row[0]),
- float(row[1]), float(row[2]), float(row[3]), float(row[4]), float(row[5])
- ) for row in data]
-
- try:
- result = analyse_func(data, start_ts, end_ts, multi_positions, strategies[strategy], position_type)
- return {
- "pair": pair,
- "strategy": strategy,
- "position_type": position_type,
- "result": (
- [], # we dont care about indicators
- result.positions,
- result.current_positions
- ),
- "stats": {
- "drawdown": get_drawdowns_stats(result),
- "positions": get_position_stats(result),
- "advanced": get_advanced_stats(result)
- }
- }
- except Exception as e:
- return {
- "pair": pair,
- "error": f"Analysis failed for pair {pair}: {str(e)}"
- }
-
- results_dict = {}
- with ThreadPoolExecutor(max_workers=concurrency) as executor:
- future_to_pair = {executor.submit(analyse_single_pair, pair): pair for pair in pairs_list}
- for future in as_completed(future_to_pair):
- pair = future_to_pair[future]
- try:
- results_dict[pair] = future.result()
- except Exception as exc:
- results_dict[pair] = {"pair": pair, "error": str(exc)}
-
- results = [results_dict[pair] for pair in pairs_list]
-
- # Aggregate statistics
- def flatten_dict(d, parent_key="", sep="."):
- items = {}
- for k, v in d.items():
- new_key = f"{parent_key}{sep}{k}" if parent_key else k
- if isinstance(v, dict):
- items.update(flatten_dict(v, new_key, sep=sep))
- else:
- items[new_key] = v
- return items
-
- field_values = {}
- for res in results:
- if "error" in res:
- continue
- flat_stats = flatten_dict(res.get("stats", {}))
- for field, value in flat_stats.items():
- if isinstance(value, (int, float)):
- field_values.setdefault(field, []).append((res["pair"], value))
-
- aggregate = {}
- for field, pair_values in field_values.items():
- pair_values_sorted = sorted(pair_values, key=lambda x: x[1])
- worst_pair, min_val = pair_values_sorted[0]
- best_pair, max_val = pair_values_sorted[-1]
- avg_val = sum(v for _, v in pair_values) / len(pair_values)
- aggregate[field] = {
- "average": avg_val,
- "best": {"pair": best_pair, "value": max_val},
- "worst": {"pair": worst_pair, "value": min_val}
- }
-
- descriptions = {
- "positions.average_ratio": ">",
- "positions.final_cumulative_ratio": ">",
- "positions.average_position_duration": "~",
- "drawdown.max_drawdown": "<",
- "drawdown.average_drawdown": "<",
- "advanced.sharpe_ratio": ">",
- "advanced.sortino_ratio": ">",
- "advanced.volatility": "<",
- "advanced.annualized_return": ">",
- "advanced.calmar_ratio": ">",
- "advanced.recovery_factor": ">",
- "advanced.win_rate": ">",
- "advanced.loss_rate": "<",
- "advanced.profit_factor": ">",
- "advanced.expectancy": ">",
- "advanced.trade_frequency_per_year": "info",
- "advanced.exposure_pct": "info",
- "advanced.skewness": "≈0",
- "advanced.kurtosis": "≈3",
- "advanced.VaR_95": ">",
- "advanced.CVaR_95": ">",
- "advanced.consecutive_wins": ">",
- "advanced.consecutive_losses": "<",
- "advanced.max_drawdown_period_days": "<",
- "advanced.time_to_recovery_days": "<"
- }
- for field, symbol in descriptions.items():
- if field not in aggregate:
- aggregate[field] = {
- "average": None,
- "best": None,
- "worst": None,
- "analysis": symbol
- }
- else:
- aggregate[field]["analysis"] = symbol
-
- response_data = {
- "strategy": strategy,
- "position_type": position_type,
- "concurrency": concurrency,
- "pairs_analyzed": len(pairs_list),
- "metrics_summary": aggregate,
- "results": results
- }
-
- logger.info(f"Scan request - pairs: {pairs_list} | strategy: {strategy} | position_type: {position_type}")
- return jsonify(response_data)
\ No newline at end of file
diff --git a/integrations/plotly/README.md b/integrations/plotly/README.md
deleted file mode 100644
index e69de29..0000000
diff --git a/integrations/plotly/main.py b/integrations/plotly/main.py
deleted file mode 100644
index b62fecc..0000000
--- a/integrations/plotly/main.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import sys
-from utils.api_requests import fetch_and_show_data
-
-def main():
- if len(sys.argv) < 3:
- print("Usage: main.py -data -strategy [-start_ts ] [-end_ts ] [-multi_positions ]")
- sys.exit(1)
-
- data = None
- strategy = None
- start_ts = "2000-01-01 00:00:00"
- end_ts = "9999-01-01 00:00:00"
- multi_positions = "True"
-
- args = sys.argv[1:]
- for i in range(len(args)):
- if args[i] == "-data":
- data = args[i+1]
- elif args[i] == "-strategy":
- strategy = args[i+1]
- elif args[i] == "-start_ts":
- start_ts = args[i+1]
- elif args[i] == "-end_ts":
- end_ts = args[i+1]
- elif args[i] == "-multi_positions":
- multi_positions = args[i+1]
-
- if data and strategy:
- fetch_and_show_data(data, strategy, start_ts, end_ts, multi_positions)
- else:
- print("Error: -data and -strategy arguments are required")
- sys.exit(1)
-
-if __name__ == "__main__":
- main()
diff --git a/integrations/plotly/utils/api_requests.py b/integrations/plotly/utils/api_requests.py
deleted file mode 100644
index 6dfc705..0000000
--- a/integrations/plotly/utils/api_requests.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import requests
-import json
-import os
-from datetime import datetime
-from utils.plots import plot_json_data_in_gui
-
-def fetch_and_show_data(data_file, strategy, start_ts, end_ts, multi_positions):
- url = f"http://127.0.0.1:5002/QTSBE/analyse?pair={data_file}&strategy={strategy}&start_ts={start_ts}&end_ts={end_ts}&multi_positions={multi_positions}&details=True"
-
- try:
- response = requests.get(url)
- response.raise_for_status()
- json_data = response.json()
- plot_json_data_in_gui(json_data, data_file, strategy)
- except requests.RequestException as e:
- print("Request failed:", e)
-
-def save_to_file(content):
- try:
- os.makedirs('integrations/plotly/saved_results/', exist_ok=True)
- filename = generate_filename(content)
- with open(filename, "w") as file:
- file.write(json.dumps(content, indent=4))
- print("Content saved")
- except Exception as e:
- print("Failed to save content:", e)
-
-def generate_filename(content):
- return f"integrations/plotly/saved_results/{datetime.now().strftime('%Y-%m-%d %H:%M:%S %H-%M-%S')}_{content['pair']}_{content['strategy']}.json"
-
-def extract_trade_data(trades):
- trade_indices = list(range(1, len(trades) + 1))
- trade_ratios = [trade['ratio'] for trade in trades if 'ratio' in trade]
- return trade_indices, trade_ratios
diff --git a/integrations/plotly/utils/files.py b/integrations/plotly/utils/files.py
deleted file mode 100644
index 933626e..0000000
--- a/integrations/plotly/utils/files.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import os
-
-def list_files_in_directory(directory, extension):
- """Get files of a directory and its subdirectories without their extension"""
- files = []
- for root, dirs, files_in_dir in os.walk(directory):
- for file_name in files_in_dir:
- if file_name.endswith(extension): # Only consider files with the specified extension
- relative_path = os.path.relpath(os.path.join(root, file_name), directory)
- name_without_extension = os.path.splitext(relative_path)[0].replace(os.sep, '_')
- files.append(name_without_extension)
- return files or ['None']
diff --git a/integrations/plotly/utils/plots.py b/integrations/plotly/utils/plots.py
deleted file mode 100644
index 04a8aad..0000000
--- a/integrations/plotly/utils/plots.py
+++ /dev/null
@@ -1,202 +0,0 @@
-import plotly.graph_objs as go
-from plotly.subplots import make_subplots
-import os
-import webbrowser
-import sys
-
-class ChartTheme:
- def __init__(self, theme='white'):
- self.theme = theme
- self.colors = {
- "Background": theme,
- "increasing_line": "black",
- "increasing_fill": "white",
- "decreasing_line": "black",
- "decreasing_fill": "black",
- "shapes": "#8288b0",
- "MA_100": "#B8336A",
- "MA_40": "#FF9B42",
- "MA_20": "#F4D35E",
- "MA_9": "#F95738",
- "MA_21": "#F4D35E",
- "MA_200": "#6A0572",
- "RSI": "#77C67E",
- "EMA": "#FF85A1",
- "EMA_MACD": "#FF85A1",
- "MACD": "#5E4AE3",
- "Normalize_MACD": "#947BD3",
- "Bollinger_Lower": "#09917b",
- "Bollinger_Rolling": "#0078ff",
- "Bollinger_Upper": "#c9313f",
- "Else": "#8FF7A7"
- }
-
-class DataExtractor:
- @staticmethod
- def extract_ohlc(data):
- dates, opens, highs, lows, closes, volume = zip(*data)
- return dates, opens, highs, lows, closes
-
- @staticmethod
- def extract_indicators(json_data):
- return json_data['result'][0]
-
- @staticmethod
- def extract_trades(trades):
- trade_indices = list(range(1, len(trades) + 1))
- trade_ratios = [trade['ratio'] for trade in trades if 'ratio' in trade]
- return trade_indices, trade_ratios
-
-class TraceBuilder:
- def __init__(self, theme):
- self.theme = theme
-
- def build_candlestick(self, dates, opens, highs, lows, closes):
- return go.Candlestick(
- x=dates, open=opens, high=highs, low=lows, close=closes,
- name="Price",
- increasing_line_color=self.theme.colors['increasing_line'],
- decreasing_line_color=self.theme.colors['decreasing_line'],
- increasing_fillcolor=self.theme.colors['increasing_fill'],
- decreasing_fillcolor=self.theme.colors['decreasing_fill']
- )
-
- def build_trade_traces(self, trades, json_data):
- traces = []
- buy_dates = [trade['buy_date'] for trade in trades]
- buy_prices = [trade['buy_price'] for trade in trades]
- buy_indices = [trade['buy_index'] for trade in trades]
- buy_signals = [trade['buy_signals']['Buy_Signal'] for trade in trades]
-
- sell_dates = [trade['sell_date'] for trade in trades]
- sell_prices = [trade['sell_price'] for trade in trades]
- sell_indices = [trade['sell_index'] for trade in trades]
- sell_signals = [trade['sell_signals']['Sell_Signal'] for trade in trades]
-
- ratios = [float(ratio) for ratio in json_data["stats"]["positions"]["all_ratios"]]
-
- buy_hover_texts = [f"Index: {index}
Price: {price}
Date: {date}
Buy Signal: {buy_signal}"
- for index, price, date, buy_signal in zip(buy_indices, buy_prices, buy_dates, buy_signals)]
- sell_hover_texts = [f"Index: {index}
Price: {price}
Date: {date}
Ratio: {ratio}
Sell Signal: {sell_signal}"
- for index, price, date, ratio, sell_signal in zip(sell_indices, sell_prices, sell_dates, ratios, sell_signals)]
-
- traces.extend([
- go.Scatter(x=buy_dates, y=buy_prices, mode='markers', name='Buy',
- marker=dict(symbol='triangle-up', color='#16DB93', size=10),
- hovertext=buy_hover_texts, hoverinfo='text'),
- go.Scatter(x=sell_dates, y=sell_prices, mode='markers', name='Sell',
- marker=dict(symbol='triangle-down', color='#EFEA5A', size=10),
- hovertext=sell_hover_texts, hoverinfo='text')
- ])
- return traces
-
-class PlotlyVisualizer:
- def __init__(self):
- self.theme = ChartTheme()
- self.data_extractor = DataExtractor()
- self.trace_builder = TraceBuilder(self.theme)
-
- def create_subplots_layout(self, indicators, trade_ratios):
- rows = 2
- cols = 1
- bound_hundred_plot = any(ind in indicators for ind in ['RSI', 'ATR', 'ATR_MA'])
-
- if bound_hundred_plot and trade_ratios:
- cols += 1
- if not bound_hundred_plot and not trade_ratios:
- rows = 1
-
- row_heights = [0.7, 0.3] if cols == 1 and rows == 2 else [0.75, 0.25] if cols == 2 and rows == 2 else [1]
- column_widths = [1] if cols == 1 else [0.75, 0.25]
-
- return make_subplots(rows=rows, cols=cols, shared_xaxes='all',
- vertical_spacing=0.25, row_heights=row_heights,
- column_widths=column_widths)
-
- def plot_json_data_in_gui(self, json_data, data_file, strategy):
- dates, opens, highs, lows, closes = self.data_extractor.extract_ohlc(json_data['data'])
- indicators = self.data_extractor.extract_indicators(json_data)
- trades = json_data['result'][1]
- trade_indices, trade_ratios = self.data_extractor.extract_trades(trades)
-
- fig = self.create_subplots_layout(indicators, trade_ratios)
- fig.add_trace(self.trace_builder.build_candlestick(dates, opens, highs, lows, closes), row=1, col=1)
-
- if trade_ratios:
- self._add_trade_ratio_traces(fig, trade_indices, trade_ratios, json_data, 'RSI' in indicators)
-
- self._add_indicator_traces(fig, indicators, dates)
- self._add_trade_traces(fig, trades, json_data)
- self._update_layout(fig, data_file, strategy)
- self._save_and_show(fig, data_file, strategy)
-
- def _add_trade_ratio_traces(self, fig, trade_indices, trade_ratios, json_data, bound_hundred_plot):
- row, col = (1, 2) if bound_hundred_plot else (2, 1)
- fig.add_trace(go.Scatter(x=trade_indices, y=trade_ratios, mode='lines',
- name='Trade Ratios', line=dict(color='#DBB4AD')), row=row, col=col)
-
- cumulative_ratios = [float(ratio) for ratio in json_data["stats"]["positions"]["cumulative_ratios"]]
- fig.add_trace(go.Scatter(x=trade_indices, y=cumulative_ratios, mode='lines',
- name='Cumulative Ratios', line=dict(color='#D30C7B')), row=row, col=col)
-
- moving_avg = [sum(cumulative_ratios[:i+1])/(i+1) for i in range(len(cumulative_ratios))]
- fig.add_trace(go.Scatter(x=trade_indices, y=moving_avg, mode='lines',
- name='Moving Avg Cumulative Ratios', line=dict(color='#FFE3DC')), row=row, col=col)
-
- self._add_ratio_shapes(fig, trade_indices, trade_ratios, row, col)
-
- def _add_indicator_traces(self, fig, indicators, dates):
- for indicator, values in indicators.items():
- color = self.theme.colors.get(indicator, self.theme.colors['Else'])
- row = 2 if indicator in ['RSI', 'ATR', 'ATR_MA'] else 1
- fig.add_trace(go.Scatter(x=dates, y=values, mode='lines',
- name=indicator, line=dict(color=color)), row=row, col=1)
-
- def _add_trade_traces(self, fig, trades, json_data):
- traces = self.trace_builder.build_trade_traces(trades, json_data)
- for trace in traces:
- fig.add_trace(trace, row=1, col=1)
-
- def _add_ratio_shapes(self, fig, trade_indices, trade_ratios, row, col):
- min_ratio = min(trade_ratios)
- shapes = [
- dict(type="line", x0=min(trade_indices), y0=1, x1=max(trade_indices), y1=1,
- line=dict(color=self.theme.colors['shapes'], width=1.5)),
- dict(type="line", x0=min(trade_indices), y0=min_ratio, x1=max(trade_indices), y1=min_ratio,
- line=dict(color='red', width=1.5, dash='dash'))
- ]
- for shape in shapes:
- fig.add_shape(**shape, row=row, col=col)
-
- def _update_layout(self, fig, data_file, strategy):
- fig.update_layout(
- title=f"{data_file} ({strategy})",
- xaxis_title='Date',
- yaxis_title='Price',
- xaxis_rangeslider_visible=False,
- plot_bgcolor=self.theme.colors['Background'],
- paper_bgcolor=self.theme.colors['Background'],
- font=dict(color="black" if self.theme.theme == 'white' else "white"),
- yaxis=dict(gridcolor=self.theme.colors['Background']),
- xaxis=dict(gridcolor=self.theme.colors['Background']),
- yaxis2=dict(gridcolor=self.theme.colors['Background']),
- xaxis2=dict(gridcolor=self.theme.colors['Background'])
- )
-
- def _save_and_show(self, fig, data_file, strategy):
- directory = 'integrations/plotly/saved_results/'
- os.makedirs(directory, exist_ok=True)
- plot_filename = f'plot_{data_file}_{strategy}.html'
- fig.write_html(directory + plot_filename)
-
- if sys.platform == "darwin":
- safari_path = 'open -a /Applications/Safari.app %s'
- webbrowser.get(safari_path).open('file://' + os.path.realpath(os.path.join('integrations', 'plotly', 'saved_results', plot_filename)))
- else:
- fig.show()
-
-def plot_json_data_in_gui(json_data, data_file, strategy):
- visualizer = PlotlyVisualizer()
- visualizer.plot_json_data_in_gui(json_data, data_file, strategy)
-
-
diff --git a/requirements.txt b/requirements.txt
index 0b3bb02..b329e7b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,20 +1,12 @@
-customtkinter==5.2.2
-matplotlib==3.8.2
loguru==0.7.2
ccxt==4.3.33
-plotly>=5.3.0
pandas>=1.3.0
flask==3.0.2
flask_cors==4.0.0
colorama==0.4.6
-tqdm==4.66.2
-aiofiles
-selenium
-kaleido
yfinance>=0.1.70
flask_caching>=2.0.0
numpy>=1.21.0
-discord.py==2.3.2
flask-swagger-ui>=4.11.0
python-binance>=1.0.0
python-dotenv>=1.0.0
\ No newline at end of file
diff --git a/tests/fibonacci.py b/tests/fibonacci.py
deleted file mode 100644
index 720fdf4..0000000
--- a/tests/fibonacci.py
+++ /dev/null
@@ -1,73 +0,0 @@
-import sys
-import os
-import pandas as pd
-import plotly.graph_objects as go
-sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-
-def get_fibonacci_retracement_levels(data, start_index, end_index):
-
- if start_index < 0 or end_index >= len(data) or start_index >= end_index:
- # Increment end_index if it is equal to start_index and within bounds
- if start_index == end_index and end_index + 1 < len(data):
- end_index += 1
- else:
- raise ValueError("Invalid indices. Ensure 0 <= start_index < end_index < len(prices).")
-
- low_price = min([float(row[3]) for row in data[start_index:end_index + 1]])
- high_price = max([float(row[2]) for row in data[start_index:end_index + 1]])
- price_range = high_price - low_price
- retracement_levels = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0]
- retracement_prices = {f"{level * 100:.1f}%": high_price - (price_range * level) for level in retracement_levels}
-
- return retracement_prices
-
-def get_file_data(pair):
- file_path = f"data/bank/{pair}.csv"
- try:
- csv_data = pd.read_csv(file_path).to_dict(orient='records')
- except FileNotFoundError:
- print(f"The file {pair}.csv was not found.")
- return []
-
- data = [[str(row["timestamp"]), str(row["open"]), str(row["high"]), str(row["low"]), str(row["close"]), str(row["volume"])] for row in csv_data]
- print(f"Data was successfully retrieved for {pair}.")
- return data
-pair = "Binance_SOLUSDC_1d"
-data = get_file_data(pair)
-if not data:
- sys.exit("Data retrieval failed.")
-
-first_index = 0
-second_index = len(data)//2
-fibonacci_retracement_levels = get_fibonacci_retracement_levels(data, first_index, second_index)
-fig = go.Figure(data=[go.Candlestick(
- x=[index for index in range(first_index, second_index + 1, 1)],
- open=[entry[1] for entry in data[first_index:second_index + 1]],
- high=[entry[2] for entry in data[first_index:second_index + 1]],
- low=[entry[3] for entry in data[first_index:second_index + 1]],
- close=[entry[4] for entry in data[first_index:second_index + 1]]
-)])
-levels = list(fibonacci_retracement_levels.keys())
-for level, color, name in zip(fibonacci_retracement_levels.values(), ['blue', 'green', 'red', 'orange', 'purple', 'magenta'], levels):
- fig.add_hline(y=level, line_dash="dash", line_color=color, name=f"Fib Retracement: {name}")
- fig.add_annotation(
- xref="paper",
- yref="y",
- x=1.02,
- y=level,
- text=f"Fib Retracement: {name}",
- showarrow=False
- )
-fig.update_layout(
- title=f"{pair} : Fibonacci Retracement Levels",
- xaxis_title='Date',
- yaxis_title='Price',
- xaxis_rangeslider_visible=False,
- plot_bgcolor='#161a25',
- paper_bgcolor='#161a25',
- font=dict(color='white'),
- yaxis=dict(gridcolor='#6c7386'),
- xaxis=dict(gridcolor='#6c7386'),
-)
-fig.show()
\ No newline at end of file
diff --git a/tests/integrations/plotly_unit.sh b/tests/integrations/plotly_unit.sh
deleted file mode 100644
index 45e2c32..0000000
--- a/tests/integrations/plotly_unit.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-python integrations/plotly/main.py -strategy default -data Binance_BTCUSDC_1d
-python integrations/plotly/main.py -strategy rsi_example -data Binance_BTCUSDC_1d -multi_positions False
-python integrations/plotly/main.py -strategy rsi_example -data Binance_BTCUSDC_1d -start_ts '2020-01-01 00:00:00' -end_ts '2021-01-10 00:00:00' -multi_positions False
-python integrations/plotly/main.py -strategy rsi_example -data Binance_BTCUSDC_1h -start_ts '2020-01-01 00:00:00' -end_ts '2021-01-10 00:00:00' -multi_positions False
diff --git a/tests/integrations/strategy_viewer.sh b/tests/integrations/strategy_viewer.sh
deleted file mode 100644
index 2cf4996..0000000
--- a/tests/integrations/strategy_viewer.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-strategy="QTS_fibo"
-multi_positions="False"
-
-specific_symbols=(
- "BTC/USDC" "ETH/USDC" "BNB/USDC" "ADA/USDC" "XRP/USDC" "DOGE/USDC"
- "LTC/USDC" "DOT/USDC" "UNI/USDC" "LINK/USDC" "LUNA/USDC" "SOL/USDC"
- "AVAX/USDC" "POL/USDC" "ATOM/USDC" "XLM/USDC" "TRX/USDC" "AAVE/USDC"
-)
-
-for symbol in "${specific_symbols[@]}"; do
- data="Binance_${symbol//\//}_1d"
- python integrations/plotly/main.py -strategy "$strategy" -data "$data" -multi_positions "$multi_positions" -symbol "$symbol"
-done
\ No newline at end of file
diff --git a/tests/test_api_endpoints.py b/tests/test_api_endpoints.py
deleted file mode 100644
index e62070e..0000000
--- a/tests/test_api_endpoints.py
+++ /dev/null
@@ -1,185 +0,0 @@
-import pytest
-import requests
-import json
-from datetime import datetime, timedelta
-
-BASE_URL = "http://127.0.0.1:5002/QTSBE"
-
-@pytest.fixture
-def api_client():
- return requests.Session()
-
-def test_analyse_endpoint_success(api_client):
- params = {
- "pair": "Binance_ETHUSDC_1d",
- "strategy": "rsi_example",
- "details": "True",
- "position_type": "long"
- }
- response = api_client.get(f"{BASE_URL}/analyse", params=params)
- assert response.status_code == 200
- data = response.json()
- assert "pair" in data
- assert "strategy" in data
- assert "data" in data
- assert "result" in data
- assert "stats" in data
- assert "position_type" in data
- assert data["position_type"] == "long"
-
-def test_analyse_endpoint_missing_params(api_client):
- response = api_client.get(f"{BASE_URL}/analyse")
- assert response.status_code == 400
- assert "error" in response.json()
-
-def test_analyse_endpoint_invalid_pair(api_client):
- params = {
- "pair": "INVALID_PAIR",
- "strategy": "rsi_example"
- }
- response = api_client.get(f"{BASE_URL}/analyse", params=params)
- assert response.status_code == 404
-
-def test_analyse_endpoint_with_timestamps(api_client):
- end_ts = datetime.now()
- start_ts = end_ts - timedelta(days=30)
- params = {
- "pair": "Binance_ETHUSDC_1d",
- "strategy": "rsi_example",
- "start_ts": start_ts.strftime("%Y-%m-%d %H:%M:%S"),
- "end_ts": end_ts.strftime("%Y-%m-%d %H:%M:%S"),
- "position_type": "long"
- }
- response = api_client.get(f"{BASE_URL}/analyse", params=params)
- assert response.status_code == 200
-
-def test_analyse_endpoint_short_position(api_client):
- params = {
- "pair": "Binance_ETHUSDC_1d",
- "strategy": "rsi_example",
- "details": "True",
- "position_type": "short"
- }
- response = api_client.get(f"{BASE_URL}/analyse", params=params)
- assert response.status_code == 200
- data = response.json()
- assert "position_type" in data
- assert data["position_type"] == "short"
-
-def test_analyse_endpoint_invalid_position_type(api_client):
- params = {
- "pair": "Binance_ETHUSDC_1d",
- "strategy": "rsi_example",
- "position_type": "invalid"
- }
- response = api_client.get(f"{BASE_URL}/analyse", params=params)
- assert response.status_code == 400
- assert "error" in response.json()
- assert "position_type must be either 'long' or 'short'" in response.json()["error"]
-
-def test_analyse_custom_endpoint_success(api_client):
- params = {
- "pair": "Binance_ETHUSDC_1d",
- "details": "True",
- "position_type": "long"
- }
- strategy_code = """
-import numpy as np
-
-def get_RSI(prices, window=14):
- deltas = np.diff(prices)
- seed = deltas[:window+1]
- up = seed[seed >= 0].sum() / window
- down = -seed[seed < 0].sum() / window
- rs = up / down
- rsi = np.zeros_like(prices)
- rsi[:window] = 100. - 100. / (1. + rs)
- return rsi
-
-class Indicators:
- def __init__(self, data):
- self.data = data
- self.indicators = self.calculate_indicators()
-
- def calculate_indicators(self):
- data_open = [row[1] for row in self.data]
- indicators = {"RSI": get_RSI(data_open)}
- return {k: list(v) for k, v in indicators.items()}
-
-def buy_signal(open_position, data, index_check, indicators, current_price=None):
- if current_price is not None:
- data[index_check][4] = current_price
- if indicators["RSI"][index_check] is None:
- return -2, None
- if indicators["RSI"][index_check] < 40:
- return 1, data[index_check][4]
- return 0, None
-
-def sell_signal(open_position, data, index_check, indicators, current_price=None):
- if current_price is not None:
- data[index_check][4] = current_price
- if indicators["RSI"][index_check] is None:
- return -1, None
- if indicators["RSI"][index_check] > 60:
- return 1, data[index_check][4]
- return 0, None
-"""
- response = api_client.post(f"{BASE_URL}/analyse_custom", params=params, json={"strategy_code": strategy_code})
- assert response.status_code == 200
- data = response.json()
- assert "pair" in data
- assert "strategy" in data
- assert "data" in data
- assert "result" in data
- assert "stats" in data
- assert "position_type" in data
- assert data["position_type"] == "long"
-
-def test_analyse_custom_endpoint_missing_params(api_client):
- headers = {'Content-Type': 'application/json'}
- response = api_client.post(f"{BASE_URL}/analyse_custom", headers=headers, json={})
- assert response.status_code == 400
- assert "error" in response.json()
-
-def test_analyse_custom_endpoint_invalid_code(api_client):
- params = {
- "pair": "Binance_ETHUSDC_1d"
- }
- strategy_code = "invalid python code"
- response = api_client.post(f"{BASE_URL}/analyse_custom", params=params, json={"strategy_code": strategy_code})
- assert response.status_code == 400
- assert "error" in response.json()
-
-def test_get_tokens_endpoint(api_client):
- response = api_client.get(f"{BASE_URL}/get_tokens")
- assert response.status_code == 200
- data = response.json()
- assert "tokens" in data
- assert "timestamp" in data
- assert isinstance(data["tokens"], dict)
-
-def test_get_strategies_endpoint(api_client):
- response = api_client.get(f"{BASE_URL}/get_strategies")
- assert response.status_code == 200
- data = response.json()
- assert "strategies" in data
- assert "timestamp" in data
- assert isinstance(data["strategies"], list)
-
-def test_get_tokens_stats_endpoint(api_client):
- response = api_client.get(f"{BASE_URL}/get_tokens_stats")
- assert response.status_code == 200
- data = response.json()
- assert "tokens" in data
- assert "timestamp" in data
- assert isinstance(data["tokens"], dict)
-
- for exchange in data["tokens"].values():
- for token_info in exchange.values():
- assert "timeframes" in token_info
- assert "first_date" in token_info
- assert "last_date" in token_info
- assert "last_price" in token_info
- assert "volume_24h" in token_info
- assert "price_change_24h" in token_info
- assert "last_modified" in token_info
\ No newline at end of file
diff --git a/tests/tools/data_fetch/binance-fetch-sample.py b/tests/tools/data_fetch/binance-fetch-sample.py
deleted file mode 100644
index e637dc4..0000000
--- a/tests/tools/data_fetch/binance-fetch-sample.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import os
-import sys
-sys.path.append(os.getcwd())
-from tools.data_fetch.binance.binance import BinanceAPI
-
-binance_api = BinanceAPI()
-
-symbols = [
- 'BTC/USDC', 'ETH/USDC', 'BNB/USDC',
- 'ADA/USDC', 'XRP/USDC', 'DOGE/USDC',
- 'LTC/USDC', 'DOT/USDC', 'UNI/USDC',
- 'LINK/USDC', 'LUNA/USDC',
- 'SOL/USDC', 'AVAX/USDC',
- 'MATIC/USDC', 'ATOM/USDC', 'XLM/USDC',
- 'TRX/USDC',
- 'AAVE/USDC'
-]
-
-binance_api.fetch_tokens_daily_ohlcv(symbols)
\ No newline at end of file
diff --git a/tests/tools/data_fetch/binance-update-sample.py b/tests/tools/data_fetch/binance-update-sample.py
deleted file mode 100644
index 9d5aa84..0000000
--- a/tests/tools/data_fetch/binance-update-sample.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import os
-import sys
-sys.path.append(os.getcwd())
-from tools.data_fetch.binance.binance import BinanceAPI
-
-binance_api = BinanceAPI()
-
-symbols = [
- 'BTC/USDC', 'SOL/USDC'
-]
-
-binance_api.update_ohlcv_for_symbols(symbols)
\ No newline at end of file
diff --git a/tests/tools/data_fetch/yahoo-fetch-sample.py b/tests/tools/data_fetch/yahoo-fetch-sample.py
deleted file mode 100644
index f1f54c4..0000000
--- a/tests/tools/data_fetch/yahoo-fetch-sample.py
+++ /dev/null
@@ -1,133 +0,0 @@
-import os
-import sys
-sys.path.append(os.getcwd())
-from tools.data_fetch.yahoo.yahoo import YahooAPI
-
-if __name__ == "__main__":
- tickers = [
- 'AAPL', 'MSFT', 'GOOGL', 'AMZN', 'BRK-B', 'TSLA', 'META', 'NVDA', 'TSMC', 'JPM',
- 'JNJ', 'V', 'WMT', 'UNH', 'PG', 'MA', 'XOM', 'HD', 'BAC', 'DIS',
- 'VZ', 'KO', 'CMCSA', 'CSCO', 'PFE', 'PEP', 'ADBE', 'INTC', 'NFLX', 'ABT',
- 'TMO', 'MRK', 'CRM', 'NKE', 'ACN', 'AVGO', 'MCD', 'COST', 'NEE', 'TXN',
- 'LLY', 'ORCL', 'PM', 'MDT', 'UPS', 'HON', 'MS', 'UNP', 'IBM', 'QCOM',
- 'LIN', 'AMGN', 'CVX', 'BMY', 'SBUX', 'BLK', 'RTX', 'GE', 'AMAT', 'SCHW',
- 'INTU', 'NOW', 'T', 'LOW', 'SPGI', 'AXP', 'BA', 'MMM', 'CAT', 'GILD',
- 'PLD', 'MDLZ', 'ISRG', 'C', 'ADP', 'DE', 'AMT', 'DUK', 'CI', 'MO',
- 'CB', 'MU', 'SO', 'EL', 'BKNG', 'ADI', 'EW', 'ZTS', 'GD', 'SYK',
- 'ICE', 'PGR', 'REGN', 'LMT', 'AON', 'WM', 'CSX', 'MMC', 'USB', 'DHR'
- ]
-
- # mapping for ticker symbols that had issues
- ticker_corrections = {
- 'TSMC': 'TSM',
- 'BRK.A': 'BRK-B',
- 'META': 'META',
- }
-
- tickers = [ticker_corrections.get(ticker, ticker) for ticker in tickers]
-
- yahoo_api = YahooAPI()
- yahoo_api.download_and_save(tickers, interval='1d')
-
- # tickers details :
-
- # AAPL: Apple Inc. - Manufacturer of electronic devices, software, and digital services (iPhone, Mac, iPad).
- # MSFT: Microsoft Corporation - Developer of software, hardware, and cloud services (Windows, Office, Azure).
- # GOOGL: Alphabet Inc. - Parent company of Google, specializing in online search services, advertising, and technology.
- # AMZN: Amazon.com Inc. - E-commerce giant and provider of cloud services (AWS).
- # BRK.A: Berkshire Hathaway Inc. - Diversified conglomerate with investments across various sectors (insurance, energy, etc.).
- # TSLA: Tesla Inc. - Manufacturer of electric vehicles and renewable energy solutions.
- # META: Meta Platforms Inc. - Parent company of Facebook, specializing in social media and virtual reality.
- # NVDA: NVIDIA Corporation - Designer of graphics processors and technology for artificial intelligence.
- # TSM: Taiwan Semiconductor Manufacturing Company - Leading global manufacturer of semiconductors.
- # JPM: JPMorgan Chase & Co. - Major investment bank and financial services provider.
- # JNJ: Johnson & Johnson - Manufacturer of pharmaceuticals, medical devices, and consumer health products.
- # V: Visa Inc. - Global payments technology company facilitating digital payments.
- # WMT: Walmart Inc. - Multinational retail corporation operating a chain of hypermarkets, discount department stores, and grocery stores.
- # UNH: UnitedHealth Group Incorporated - Provider of healthcare products and insurance services.
- # PG: Procter & Gamble Co. - Producer of consumer goods including health, hygiene, and home products.
- # MA: Mastercard Incorporated - Global payments technology company specializing in digital payment solutions.
- # XOM: Exxon Mobil Corporation - Major oil and gas corporation.
- # HD: Home Depot Inc. - Retailer of home improvement and construction products and services.
- # BAC: Bank of America Corporation - Financial services company offering banking and investment services.
- # DIS: The Walt Disney Company - Global entertainment and media conglomerate.
- # VZ: Verizon Communications Inc. - Telecommunications company providing wireless services and broadband.
- # KO: The Coca-Cola Company - Manufacturer of beverages including soft drinks.
- # CMCSA: Comcast Corporation - Provider of cable television, internet, and telephone services.
- # CSCO: Cisco Systems Inc. - Designer and manufacturer of networking hardware and telecommunications equipment.
- # PFE: Pfizer Inc. - Pharmaceutical company known for developing medications and vaccines.
- # PEP: PepsiCo Inc. - Producer of beverages and snack foods.
- # ADBE: Adobe Inc. - Software company known for creative and multimedia software products.
- # INTC: Intel Corporation - Manufacturer of semiconductor chips and computing devices.
- # NFLX: Netflix Inc. - Streaming service provider of films and television series.
- # ABT: Abbott Laboratories - Global healthcare company specializing in diagnostics, medical devices, and nutrition products.
- # TMO: Thermo Fisher Scientific Inc. - Provider of scientific instrumentation, reagents, and consumables.
- # MRK: Merck & Co., Inc. - Pharmaceutical company focused on health and wellness.
- # CRM: Salesforce.com Inc. - Cloud-based software company specializing in customer relationship management (CRM).
- # NKE: Nike Inc. - Sportswear and equipment manufacturer.
- # ACN: Accenture plc - Global professional services company specializing in consulting and technology services.
- # AVGO: Broadcom Inc. - Designer of semiconductor and infrastructure software solutions.
- # MCD: McDonald's Corporation - Fast food restaurant chain.
- # COST: Costco Wholesale Corporation - Membership-based warehouse club retailer.
- # NEE: NextEra Energy Inc. - Renewable energy company.
- # TXN: Texas Instruments Inc. - Manufacturer of semiconductor and electronics products.
- # LLY: Eli Lilly and Company - Global pharmaceutical company focusing on healthcare solutions.
- # ORCL: Oracle Corporation - Software and hardware company specializing in database management and cloud services.
- # PM: Philip Morris International Inc. - Manufacturer of tobacco products.
- # MDT: Medtronic plc - Global leader in medical technology and services.
- # UPS: United Parcel Service, Inc. - Package delivery and supply chain management company.
- # HON: Honeywell International Inc. - Conglomerate involved in various industries including aerospace, building technologies, and performance materials.
- # MS: Morgan Stanley - Global financial services firm offering investment banking, wealth management, and other services.
- # UNP: Union Pacific Corporation - Railroad company providing freight transportation services.
- # IBM: International Business Machines Corporation - Technology and consulting company.
- # QCOM: Qualcomm Incorporated - Developer of semiconductor and telecommunications products.
- # LIN: Linde plc - Global industrial gases and engineering company.
- # AMGN: Amgen Inc. - Biotechnology company focused on novel therapies.
- # CVX: Chevron Corporation - Multinational energy corporation involved in oil, gas, and geothermal energy.
- # BMY: Bristol-Myers Squibb Company - Biopharmaceutical company known for innovative medicines.
- # SBUX: Starbucks Corporation - Coffeehouse chain known for its specialty coffee and beverages.
- # BLK: BlackRock, Inc. - Global asset management firm.
- # RTX: Raytheon Technologies Corporation - Aerospace and defense company.
- # GE: General Electric Company - Multinational conglomerate focusing on various industries including aviation, healthcare, and power.
- # AMAT: Applied Materials, Inc. - Supplier of equipment, services, and software for semiconductor manufacturing.
- # SCHW: Charles Schwab Corporation - Financial services company providing investment and brokerage services.
- # INTU: Intuit Inc. - Financial software company known for products like TurboTax and QuickBooks.
- # NOW: ServiceNow, Inc. - Provider of cloud-based services for digital workflows.
- # T: AT&T Inc. - Telecommunications company providing wireless, broadband, and media services.
- # LOW: Lowe’s Companies, Inc. - Retailer specializing in home improvement products and services.
- # SPGI: S&P Global Inc. - Provider of financial market intelligence and analytics.
- # AXP: American Express Company - Financial services corporation known for its charge cards and travel services.
- # BA: The Boeing Company - Aerospace company that designs and manufactures airplanes, satellites, and defense systems.
- # MMM: 3M Company - Conglomerate involved in manufacturing and innovation across various sectors including healthcare and consumer goods.
- # CAT: Caterpillar Inc. - Manufacturer of construction and mining equipment.
- # GILD: Gilead Sciences, Inc. - Biopharmaceutical company focusing on antiviral drugs.
- # PLD: Prologis, Inc. - Real estate investment trust focusing on logistics and industrial properties.
- # MDLZ: Mondelēz International, Inc. - Snack food and beverage company.
- # ISRG: Intuitive Surgical, Inc. - Developer of robotic-assisted surgical systems.
- # C: Citigroup Inc. - Multinational banking and financial services corporation.
- # ADP: Automatic Data Processing, Inc. - Provider of human resources management software and services.
- # DE: Deere & Company - Manufacturer of agricultural, construction, and forestry machinery.
- # AMT: American Tower Corporation - Owner and operator of wireless and broadcast communications infrastructure.
- # DUK: Duke Energy Corporation - Energy company providing electricity and natural gas.
- # CI: Cigna Corporation - Global health service company providing insurance and healthcare services.
- # MO: Altria Group, Inc. - Tobacco company known for its cigarette brands.
- # CB: Chubb Limited - Global insurance company offering property and casualty insurance.
- # MU: Micron Technology, Inc. - Manufacturer of memory and storage solutions.
- # SO: Southern Company - Energy company providing electricity and natural gas.
- # EL: Estée Lauder Companies Inc. - Manufacturer and marketer of skincare, makeup, and fragrance products.
- # BKNG: Booking Holdings Inc. - Provider of online travel and related services.
- # ADI: Analog Devices, Inc. - Designer of analog, mixed-signal, and digital signal processing integrated circuits.
- # EW: Edwards Lifesciences Corporation - Provider of heart valve therapies and hemodynamic monitoring.
- # ZTS: Zoetis Inc. - Animal health company.
- # GD : General Dynamics Corporation - Aerospace and defense company.
- # SYK: Stryker Corporation - Medical technology company specializing in orthopedic and surgical products.
- # ICE: Intercontinental Exchange, Inc. - Operator of global exchanges and clearing houses.
- # PGR: Progressive Corporation - Insurance company providing auto, home, and other types of insurance.
- # REGN: Regeneron Pharmaceuticals, Inc. - Biopharmaceutical company focused on developing therapies for serious medical conditions.
- # LMT: Lockheed Martin Corporation - Aerospace, defense, and security company.
- # AON: Aon plc - Global professional services firm providing risk, retirement, and health solutions.
- # WM: Waste Management, Inc. - Provider of waste management and environmental services.
- # CSX: CSX Corporation - Provider of rail-based transportation services.
- # MMC: Marsh & McLennan Companies, Inc. - Professional services firm specializing in insurance brokerage and risk management.
- # USB: U.S. Bancorp - Financial services holding company for U.S. Bank.
- # DHR: Danaher Corporation - Global science and technology innovator focusing on diagnostics, life sciences, and environmental solutions.
\ No newline at end of file
diff --git a/tests/tools/data_fetch/yahoo-update-sample.py b/tests/tools/data_fetch/yahoo-update-sample.py
deleted file mode 100644
index 9740e49..0000000
--- a/tests/tools/data_fetch/yahoo-update-sample.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import os
-import sys
-sys.path.append(os.getcwd())
-from tools.data_fetch.yahoo.yahoo import YahooAPI
-
-if __name__ == "__main__":
- tickers = [
- 'TSLA', # TSLA: Tesla Inc. - Manufacturer of electric vehicles and renewable energy solutions.
- 'AMZN' # AMZN: Amazon.com Inc. - E-commerce giant and provider of cloud services (AWS).
- ]
-
- yahoo_api = YahooAPI()
- yahoo_api.update_ohlcv_for_tickers(tickers, interval='1d')
\ No newline at end of file
diff --git a/tools/README.md b/tools/README.md
index 938bda3..96dcb35 100644
--- a/tools/README.md
+++ b/tools/README.md
@@ -4,10 +4,4 @@
Tool that aims to auto fetch a configuration of data symbols and providers to update the data stocked in `data/bank/`
## tools/data_fetch
-Tool that aims to fetch data of a symbol on a data provider.
-
-## tools/lab
-Tkinter interface for fast testing of a strategy (see json results from API and plot on plotly the chart).
-
-## tools/strategy_scanner
-Tool that aims to test a strategy on a set of symbols (pairs) and see the global results over years.
\ No newline at end of file
+Tool that aims to fetch data of a symbol on a data provider.
\ No newline at end of file
diff --git a/tools/auto_fetch/auto-fetch.py b/tools/auto_fetch/auto-fetch.py
index a0a223c..42093ab 100644
--- a/tools/auto_fetch/auto-fetch.py
+++ b/tools/auto_fetch/auto-fetch.py
@@ -15,11 +15,6 @@
load_dotenv()
-log_directory = "logs/auto_fetch"
-os.makedirs(log_directory, exist_ok=True)
-log_path = os.path.join(log_directory, "{time:YYYY-MM-DD}.log")
-
-logger.add(log_path, rotation="00:00", retention="7 days", level="INFO")
logger.info('Start')
app = Flask(__name__)
diff --git a/tools/lab/main.py b/tools/lab/main.py
deleted file mode 100644
index ad788fa..0000000
--- a/tools/lab/main.py
+++ /dev/null
@@ -1,152 +0,0 @@
-import sys
-import os
-import tkinter as tk
-from tkinter import ttk, scrolledtext
-import glob
-import requests
-import json
-
-sys.path.append(os.path.abspath(os.path.join(
- os.path.dirname(__file__), '..', '..')))
-
-from integrations.plotly.utils.plots import plot_json_data_in_gui
-
-
-class App(tk.Tk):
- def __init__(self):
- super().__init__()
- self.title("QTSBE Lab")
- self.create_widgets()
- self.grid_widgets()
-
- def create_widgets(self):
- self.api_source_label = tk.Label(self, text="API Source")
- self.api_source_combo = ttk.Combobox(self, values=["Yahoo", "Binance"])
- self.api_source_combo.bind("<>", self.update_pairs)
-
- self.pair_label = tk.Label(self, text="Trading Pair")
- self.pair_combo = ttk.Combobox(self)
-
- self.timeframe_label = tk.Label(self, text="Timeframe")
- self.timeframe_combo = ttk.Combobox(self, values=["1d", "1h"])
-
- self.strategy_label = tk.Label(self, text="Strategy")
- self.strategy_combo = ttk.Combobox(self, values=self.load_strategies())
-
- self.start_ts_label = tk.Label(self, text="Start Timestamp")
- self.start_ts_entry = tk.Entry(self)
-
- self.end_ts_label = tk.Label(self, text="End Timestamp")
- self.end_ts_entry = tk.Entry(self)
-
- self.hide_data_var = tk.BooleanVar(value=True)
- self.hide_data_checkbox = tk.Checkbutton(
- self, text="Hide Data", variable=self.hide_data_var)
-
- self.hide_indicators_var = tk.BooleanVar(value=True)
- self.hide_indicators_checkbox = tk.Checkbutton(
- self, text="Hide Indicators", variable=self.hide_indicators_var)
-
- self.open_graphics_var = tk.BooleanVar(value=True)
- self.open_graphics_checkbox = tk.Checkbutton(
- self, text="Open Graphics", variable=self.open_graphics_var)
-
- self.multi_positions_var = tk.BooleanVar(value=False)
- self.multi_positions_checkbox = tk.Checkbutton(
- self, text="Multi Positions", variable=self.multi_positions_var)
-
- self.load_button = tk.Button(
- self, text="Load", command=self.load_request)
-
- self.response_text = scrolledtext.ScrolledText(
- self, wrap=tk.WORD, width=60, height=20)
-
- def grid_widgets(self):
- self.api_source_label.grid(row=0, column=0, padx=10, pady=10)
- self.api_source_combo.grid(row=0, column=1, padx=10, pady=10)
- self.pair_label.grid(row=1, column=0, padx=10, pady=10)
- self.pair_combo.grid(row=1, column=1, padx=10, pady=10)
- self.timeframe_label.grid(row=2, column=0, padx=10, pady=10)
- self.timeframe_combo.grid(row=2, column=1, padx=10, pady=10)
- self.strategy_label.grid(row=3, column=0, padx=10, pady=10)
- self.strategy_combo.grid(row=3, column=1, padx=10, pady=10)
- self.start_ts_label.grid(row=4, column=0, padx=10, pady=10)
- self.start_ts_entry.grid(row=4, column=1, padx=10, pady=10)
- self.end_ts_label.grid(row=5, column=0, padx=10, pady=10)
- self.end_ts_entry.grid(row=5, column=1, padx=10, pady=10)
- self.hide_data_checkbox.grid(row=6, column=0, padx=10, pady=10)
- self.hide_indicators_checkbox.grid(row=6, column=1, padx=10, pady=10)
- self.open_graphics_checkbox.grid(row=7, column=0, padx=10, pady=10)
- self.multi_positions_checkbox.grid(row=7, column=1, padx=10, pady=10)
- self.load_button.grid(row=8, column=0, columnspan=2, padx=10, pady=10)
- self.response_text.grid(
- row=9, column=0, columnspan=2, padx=10, pady=10)
-
- def load_pairs(self):
- pairs = set()
- bank_path = "data/bank"
- for file_name in os.listdir(bank_path):
- if file_name.startswith(("Binance_", "Yahoo_")):
- pair = file_name.split('_')[1]
- pairs.add(file_name.split('_')[0] + "_" + pair)
- return list(pairs)
-
- def update_pairs(self, event):
- api_source = self.api_source_combo.get()
- pairs = self.load_pairs()
- filtered_pairs = [pair.split('_')[1]
- for pair in pairs if pair.startswith(api_source)]
- self.pair_combo['values'] = filtered_pairs
-
- def load_strategies(self):
- strategies = []
- strategies_path = "api/strategies"
- for file_path in glob.glob(os.path.join(strategies_path, '**/*.py'), recursive=True):
- if not file_path.endswith("__init__.py"):
- relative_path = os.path.relpath(file_path, strategies_path)
- strategy_name = relative_path.replace(
- os.sep, '_').rsplit('.', 1)[0]
- strategies.append(strategy_name)
- return strategies
-
- def load_request(self):
- api_source = self.api_source_combo.get()
- pair = self.pair_combo.get()
- timeframe = self.timeframe_combo.get()
- strategy = self.strategy_combo.get()
- start_ts = self.start_ts_entry.get()
- end_ts = self.end_ts_entry.get()
- multi_positions = self.multi_positions_var.get()
-
- self.response_text.delete(1.0, tk.END)
- self.response_text.insert(tk.END, f"Loading {api_source} {pair} {timeframe} {strategy}...\n")
-
- url = f"http://127.0.0.1:5002/QTSBE/analyse?pair={api_source}_{pair}_{timeframe}&strategy={strategy}&details=True&multi_positions={multi_positions}"
-
- if start_ts:
- url += f"&start_ts={start_ts}"
- if end_ts:
- url += f"&end_ts={end_ts}"
-
- response = requests.get(url)
- self.response_text.delete(1.0, tk.END)
- try:
- data = response.json()
-
- if self.open_graphics_var.get():
- plot_json_data_in_gui(data, f"{api_source}_{pair}_{timeframe}", strategy)
-
- if self.hide_data_var.get():
- data['data'] = []
- if self.hide_indicators_var.get():
- data['result'][0] = {}
-
- self.response_text.insert(tk.END, json.dumps(data, indent=4))
-
- except requests.exceptions.JSONDecodeError:
- self.response_text.insert(
- tk.END, f"Invalid JSON response: {response.text}")
-
-if __name__ == "__main__":
- app = App()
- app.mainloop()
diff --git a/tools/strategy_scanner/README.md b/tools/strategy_scanner/README.md
deleted file mode 100644
index 29204eb..0000000
--- a/tools/strategy_scanner/README.md
+++ /dev/null
@@ -1,37 +0,0 @@
-## Binance Strategy Scanner
-
-This folder contains an algorithm designed to scan all pairs on Binance exchange based on a given strategy and generate statistics on the best pairs to take positions according to this strategy.
-
-### Overview
-
-The algorithm utilizes Binance API to fetch data for all available trading pairs. It then applies the specified strategy to each pair and calculates relevant statistics to identify the most promising pairs.
-
-### Options
-
-- **Fetch Latest Data**: The scanner can optionally fetch the latest OHLCV data for all trading pairs before performing the analysis. This ensures that the analysis is based on the most recent market data.
-- **Multithreaded Processing**: Uses multithreading to speed up the data fetching and analysis process. -> Be aware of this in `ThreadPoolExecutor(max_workers=7)`, if you put a large number, your processor will be done.
-
-### Usage
-
-#### Scan Tokens
-
-To scan tokens with the option to fetch the latest data: (with scan.py)
-
-```python
-scanner = BinanceScanner()
-
-specific_symbols = [
- "BTC/USDC",
- "ETH/USDC"]
-
-timeframe = '1d'
-strategy = 'rsi_example'
-fetch_latest_data = True
-start_ts = '2020-01-01 00:00:00'
-end_ts = '2021-01-01 00:00:00'
-
-scanner.scan(timeframe, strategy, fetch_latest_data, symbols=specific_symbols, start_ts=start_ts, end_ts=end_ts)
-
-### Output
-
-Results are saved in `tools/strategy_scanner/_results` with `binance_`prefix.
\ No newline at end of file
diff --git a/tools/strategy_scanner/_results/README.md b/tools/strategy_scanner/_results/README.md
deleted file mode 100644
index e69de29..0000000
diff --git a/tools/strategy_scanner/binance-scanner_sample.py b/tools/strategy_scanner/binance-scanner_sample.py
deleted file mode 100644
index 6acdf0e..0000000
--- a/tools/strategy_scanner/binance-scanner_sample.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import os
-import sys
-sys.path.append(os.getcwd())
-from tools.strategy_scanner.binance.scanner import BinanceScanner
-
-scanner = BinanceScanner()
-
-specific_symbols = [
- 'BTC/USDC', 'ETH/USDC'
-]
-
-timeframe = '1d'
-strategy = 'QTS_daily_SMACD'
-fetch_latest_data = False
-start_ts = '2024-01-01 00:00:00'
-end_ts = '2025-01-01 00:00:00'
-
-scanner.scan(timeframe, strategy, fetch_latest_data, symbols=specific_symbols, start_ts=start_ts, end_ts=end_ts)
\ No newline at end of file
diff --git a/tools/strategy_scanner/binance/scanner.py b/tools/strategy_scanner/binance/scanner.py
deleted file mode 100644
index 3341e94..0000000
--- a/tools/strategy_scanner/binance/scanner.py
+++ /dev/null
@@ -1,226 +0,0 @@
-import os
-import time
-import json
-import requests
-from datetime import datetime
-from concurrent.futures import ThreadPoolExecutor
-from tqdm import tqdm
-from colorama import Fore, Style
-from tools.data_fetch.binance.binance import BinanceAPI
-
-def format_time(seconds):
- mins, secs = divmod(seconds, 60)
- hours, mins = divmod(mins, 60)
- return f"{int(hours):02}:{int(mins):02}:{int(secs):02}"
-
-class BinanceScanner(object):
- def __init__(self):
- self.binance = BinanceAPI()
-
- def load_symbols(self):
- return self.binance.exchange.load_markets()
-
- def analyze_symbol(self, symbol, timeframe, strategy, start_ts=None, end_ts=None):
- data_file = f"Binance_{symbol.replace('/', '')}_{timeframe}"
- url = f"http://127.0.0.1:5002/QTSBE/analyse?pair={data_file}&strategy={strategy}&details=True"
-
- if start_ts:
- url += f"&start_ts={start_ts}"
- if end_ts:
- url += f"&end_ts={end_ts}"
-
- try:
- response = requests.get(url)
- response.raise_for_status()
- return response.json()
- except Exception as e:
- logger.error(f"Failed to analyze {symbol}: {e}")
- return None
-
- def process_symbol(self, symbol, index, total_symbols, start_time, timeframe, strategy, analysis_func, start_ts=None, end_ts=None):
- elapsed_time = time.time() - start_time
- avg_time_per_token = elapsed_time / index
- remaining_time = avg_time_per_token * (total_symbols - index)
- formatted_remaining_time = format_time(remaining_time)
-
- data = analysis_func(symbol, timeframe, strategy, start_ts, end_ts)
- return data, symbol, formatted_remaining_time
-
- def process_symbols(self, symbols, timeframe, strategy, fetch_latest_data, analysis_func, start_ts=None, end_ts=None):
- total_symbols = len(symbols)
- start_time = time.time()
- all_stats = []
-
- def process_symbol_wrapper(symbol, index):
- return self.process_symbol(symbol, index, total_symbols, start_time, timeframe, strategy, analysis_func, start_ts, end_ts)
-
- if fetch_latest_data:
- with ThreadPoolExecutor(max_workers=7) as executor:
- futures = {executor.submit(self.binance.fetch_and_save_ohlcv, symbol, timeframe): symbol for symbol in symbols}
- print(f"{Fore.LIGHTMAGENTA_EX}Fetching and saving OHLCV data for all tokens...\r")
- for future in futures:
- future.result()
-
- positions_stats = []
- with tqdm(total=total_symbols, desc="Processing scan", unit="symbol", ncols=100, dynamic_ncols=True, colour="BLUE") as pbar:
- with ThreadPoolExecutor(max_workers=7) as executor:
- futures = {executor.submit(process_symbol_wrapper, symbol, index): symbol for index, symbol in enumerate(symbols, start=1)}
- for index, future in enumerate(futures, start=1):
- data, symbol, formatted_remaining_time = future.result()
- if data != {}:
- if data and "stats" in data:
- stats = data["stats"]
- all_stats.append((symbol, stats, data))
- if "positions" in stats:
- positions_stats.append((symbol, stats))
- pbar.set_postfix({
- 'Symbol': symbol,
- 'Time left': formatted_remaining_time,
- })
- pbar.update(1)
-
- pbar.write(f"{Fore.LIGHTBLUE_EX}{Style.BRIGHT}All tokens processed!")
- all_stats_sorted = sorted(all_stats, key=lambda x: x[1]["positions"]["max_cumulative_ratio"], reverse=True)
- self.save_stats_to_json(all_stats_sorted, strategy, start_ts, end_ts)
- self.save_global_stats_to_json(positions_stats, strategy, start_ts, end_ts)
-
- def save_stats_to_json(self, all_stats, strategy, start_ts, end_ts):
- result = {"tokens": []}
- for symbol, _, data in all_stats:
- result["tokens"].append({
- "symbol": symbol,
- "data": data
- })
- date_str = datetime.now().strftime("%Y%m%d")
- directory = f'tools/strategy_scanner/_results/binance_{strategy}-{date_str}-{start_ts}-{end_ts}'
- os.makedirs(directory, exist_ok=True)
- file_path = os.path.join(directory, 'scan_results.json')
- with open(file_path, 'w') as json_file:
- json.dump(result, json_file, indent=4)
- print(f"{Fore.LIGHTBLUE_EX}{Style.BRIGHT}Results saved to {file_path}")
-
- def save_global_stats_to_json(self, positions_stats, strategy, start_ts, end_ts):
- global_stats = {
- "positions": {
- "all_ratios": [],
- "average_position_duration": 0,
- "average_ratio": 0,
- "biggest_position_duration": 0,
- "biggest_ratio": 0,
- "biggest_ratio_symbol": None,
- "lowest_position_duration": 0,
- "lowest_ratio": 0,
- "lowest_ratio_symbol": None,
- "percent_above_1_ratio": 0,
- "percent_below_1_ratio": 0,
- "max_cumulative_ratio": 0,
- "max_cumulative_symbol": None,
- "min_cumulative_ratio": 0,
- "min_cumulative_symbol": None,
- "average_cumulative_ratio": 0,
- "total_signals": 0,
- "average_signals": {},
- "yearly_best_symbol": {},
- "yearly_worst_symbol": {},
- "yearly_averages": {}
- }
- }
- if positions_stats:
- all_ratios = []
- cumulative_ratios = []
- durations = []
- buy_signals_count = {}
- sell_signals_count = {}
- yearly_data = {}
- total_signals = 0
-
- for symbol, stats in positions_stats:
- ratios = stats["positions"].get("all_ratios", [])
- all_ratios.extend(ratios)
- cumulative_ratios.append(stats["positions"].get("max_cumulative_ratio", 0))
- durations.append(stats["positions"].get("biggest_position_duration", 0))
-
- for key, value in stats["positions"].get("buy_signals_count", {}).items():
- buy_signals_count[key] = buy_signals_count.get(key, 0) + value
- total_signals += value
- for key, value in stats["positions"].get("sell_signals_count", {}).items():
- sell_signals_count[key] = sell_signals_count.get(key, 0) + value
- total_signals += value
-
- for year, ratio in stats["positions"].get("yearly_ratio", {}).items():
- if year not in yearly_data:
- yearly_data[year] = []
- yearly_data[year].append((symbol, ratio))
-
- above_1 = sum(1 for r in all_ratios if r > 1)
- below_1 = sum(1 for r in all_ratios if r <= 1)
- global_stats["positions"]["all_ratios"] = all_ratios
- global_stats["positions"]["percent_above_1_ratio"] = (above_1 / len(all_ratios) * 100) if all_ratios else 0
- global_stats["positions"]["percent_below_1_ratio"] = (below_1 / len(all_ratios) * 100) if all_ratios else 0
- global_stats["positions"]["average_ratio"] = sum(all_ratios) / len(all_ratios) if all_ratios else 0
- global_stats["positions"]["biggest_ratio"] = max(all_ratios) if all_ratios else 0
- global_stats["positions"]["lowest_ratio"] = min(all_ratios) if all_ratios else 0
- global_stats["positions"]["biggest_ratio_symbol"] = max(positions_stats, key=lambda x: max(x[1]["positions"].get("all_ratios", [0])) if x[1]["positions"].get("all_ratios") else 0)[0]
- global_stats["positions"]["lowest_ratio_symbol"] = min(positions_stats, key=lambda x: min(x[1]["positions"].get("all_ratios", [float('inf')])) if x[1]["positions"].get("all_ratios") else float('inf'))[0]
-
- global_stats["positions"]["average_position_duration"] = sum(durations) / len(durations) if durations else 0
- global_stats["positions"]["biggest_position_duration"] = max(durations) if durations else 0
-
- global_stats["positions"]["max_cumulative_ratio"] = max(cumulative_ratios) if cumulative_ratios else 0
- global_stats["positions"]["min_cumulative_ratio"] = min(cumulative_ratios) if cumulative_ratios else 0
- global_stats["positions"]["max_cumulative_symbol"] = max(positions_stats, key=lambda x: x[1]["positions"].get("max_cumulative_ratio", 0))[0]
- global_stats["positions"]["min_cumulative_symbol"] = min(positions_stats, key=lambda x: x[1]["positions"].get("max_cumulative_ratio", float('inf')))[0]
- global_stats["positions"]["average_cumulative_ratio"] = sum(cumulative_ratios) / len(cumulative_ratios) if cumulative_ratios else 0
-
- global_stats["positions"]["total_signals"] = total_signals
- total_signal_types = {**buy_signals_count, **sell_signals_count}
- for signal, count in total_signal_types.items():
- global_stats["positions"]["average_signals"][signal] = count / len(positions_stats)
-
- for year, data in yearly_data.items():
- best_symbol = max(data, key=lambda x: x[1])[0]
- worst_symbol = min(data, key=lambda x: x[1])[0]
- average_yearly_ratio = sum(ratio for _, ratio in data) / len(data)
- global_stats["positions"]["yearly_best_symbol"][year] = best_symbol
- global_stats["positions"]["yearly_worst_symbol"][year] = worst_symbol
- global_stats["positions"]["yearly_averages"][year] = average_yearly_ratio
-
- date_str = datetime.now().strftime("%Y%m%d")
- directory = f'tools/strategy_scanner/_results/binance_{strategy}-{date_str}-{start_ts}-{end_ts}'
- os.makedirs(directory, exist_ok=True)
- file_path = os.path.join(directory, 'global_stats.json')
- with open(file_path, 'w') as json_file:
- json.dump(global_stats, json_file, indent=4)
- print(f"{Fore.LIGHTBLUE_EX}{Style.BRIGHT}Global statistics saved to {file_path}")
-
- def scan(self, timeframe, strategy, fetch_latest_data, symbols=None, start_ts=None, end_ts=None):
- print(f"{Fore.WHITE}{Style.BRIGHT}Strategy Scanner: {Fore.LIGHTBLUE_EX}{strategy}\n{Fore.WHITE}Timeframe: {Fore.LIGHTBLUE_EX}{timeframe}\n{Fore.WHITE}Fetch Latest Data: {Fore.LIGHTBLUE_EX}{fetch_latest_data}")
- if symbols is None:
- symbols = self.load_symbols()
- self.process_symbols(symbols, timeframe, strategy, fetch_latest_data, self.analyze_symbol, start_ts, end_ts)
-
- def rank_symbols_by_recent_buy_date(self, timeframe, strategy, symbols=None):
- print(f"{Fore.WHITE}{Style.BRIGHT}Ranking Symbols by Most Recent Buy Date for Strategy: {Fore.LIGHTBLUE_EX}{strategy}\n{Fore.WHITE}Timeframe: {Fore.LIGHTBLUE_EX}{timeframe}")
- if symbols is None:
- symbols = self.load_symbols()
-
- def analyze_and_get_recent_buy_date(symbol):
- data = self.analyze_symbol(symbol, timeframe, strategy)
- if data and "result" in data and len(data["result"]) >= 3:
- result = data["result"][2]
- if result and "buy_date" in result:
- return result["buy_date"], symbol
- return None
-
- valid_symbols = []
- with ThreadPoolExecutor(max_workers=7) as executor:
- futures = {executor.submit(analyze_and_get_recent_buy_date, symbol): symbol for symbol in symbols}
- for index, future in enumerate(futures):
- result = future.result()
- if result:
- print(result, str(index)+"/"+str(len(futures)))
- valid_symbols.append(result)
-
- print(" ")
- ranked_symbols = sorted(valid_symbols, key=lambda x: x[0], reverse=True)
- return ranked_symbols
From 2318ca6511ccd5b1bcc1ecd3be25e813ce2115c2 Mon Sep 17 00:00:00 2001
From: simon <155122848+simonpotel@users.noreply.github.com>
Date: Thu, 29 Jan 2026 20:26:36 +0100
Subject: [PATCH 02/18] refactoring/enhance: auto fetch for cron data fetch
---
.env.exemple | 5 +-
.gitignore | 3 -
README.md | 4 +-
assets/integration/plotly/black_2.png | Bin 199490 -> 0 bytes
assets/integration/plotly/void.png | Bin 203720 -> 0 bytes
assets/integration/plotly/white_2.png | Bin 513792 -> 0 bytes
assets/integration/plotly/white_3.png | Bin 308143 -> 0 bytes
.../BinanceScanner/scan_example.png | Bin 30585 -> 0 bytes
config/README.md | 2 +-
config/api.json | 17 ++
config/data_cron.json | 22 ++
docker-compose.yml | 4 +-
tools/README.md | 10 +-
tools/auto_fetch/auto-fetch.py | 105 ---------
tools/data/binance.py | 49 ++++
tools/data/cron.py | 121 ++++++++++
tools/data/yahoo.py | 91 ++++++++
tools/data_fetch/binance/binance.py | 210 ------------------
tools/data_fetch/yahoo/yahoo.py | 72 ------
19 files changed, 313 insertions(+), 402 deletions(-)
delete mode 100644 assets/integration/plotly/black_2.png
delete mode 100644 assets/integration/plotly/void.png
delete mode 100644 assets/integration/plotly/white_2.png
delete mode 100644 assets/integration/plotly/white_3.png
delete mode 100644 assets/strategy_scanner/BinanceScanner/scan_example.png
create mode 100644 config/api.json
create mode 100644 config/data_cron.json
delete mode 100644 tools/auto_fetch/auto-fetch.py
create mode 100644 tools/data/binance.py
create mode 100644 tools/data/cron.py
create mode 100644 tools/data/yahoo.py
delete mode 100644 tools/data_fetch/binance/binance.py
delete mode 100644 tools/data_fetch/yahoo/yahoo.py
diff --git a/.env.exemple b/.env.exemple
index 04b1a08..866b222 100644
--- a/.env.exemple
+++ b/.env.exemple
@@ -1,8 +1,9 @@
QTSBE_PORT=5002
-QTSBE_AUTO_FETCH_PORT=5004
QTSBE_HOST=0.0.0.0
QTSBE_DEBUG=false
QTSBE_CACHE_TYPE=simple
QTSBE_CACHE_DEFAULT_TIMEOUT=300
QTSBE_CORS_ORIGINS=http://127.0.0.1:1337,http://localhost:1337
-QTSBE_CORS_METHODS='GET,PUT,POST,DELETE,OPTIONS'
\ No newline at end of file
+QTSBE_CORS_METHODS='GET,PUT,POST,DELETE,OPTIONS'
+QTSBE_CRON_PORT=5004
+QTSBE_CRON_INTERVAL_SECONDS=10
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index d3dfd06..9bd66b3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,6 @@
*.pyc
.DS_Store
-config/*
-!config/README.md
-
data/bank/*
api/strategies/*
diff --git a/README.md b/README.md
index 124b309..e6d3916 100644
--- a/README.md
+++ b/README.md
@@ -105,8 +105,8 @@ python api/api.py
- Comprehensive endpoint documentation and testing interface
4. **Automated Data Collection**
- - Configure: `tools/auto_fetch/config.json`
- - Launch collector: `python tools/auto_fetch/auto-fetch.py`
+ - Configure: `config/data_cron.json`
+ - Launch collector: `python tools/data/cron.py`
## License
diff --git a/assets/integration/plotly/black_2.png b/assets/integration/plotly/black_2.png
deleted file mode 100644
index a33f2472f26d0496a64bc1969194f8651977c5ea..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 199490
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¨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