From b3044687db964f424a5c1604ec9978eafae0c2a4 Mon Sep 17 00:00:00 2001 From: Julien Laurenceau Date: Fri, 21 Feb 2025 11:45:09 +0100 Subject: [PATCH 1/4] feat: add datasource sql and csv for webapp + clean cache button --- .gitignore | 2 + apps/candlestick-patterns/app.py | 116 ++++++++++++- .../candlestick-patterns/dal_stock_sql/dal.py | 127 ++++++++++++++ .../dal_stock_sql/util.py | 158 ++++++++++++++++++ 4 files changed, 400 insertions(+), 3 deletions(-) create mode 100644 apps/candlestick-patterns/dal_stock_sql/dal.py create mode 100644 apps/candlestick-patterns/dal_stock_sql/util.py diff --git a/.gitignore b/.gitignore index 1eab1e27..15045139 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.secret +secret.* .ipynb_checkpoints __pycache__ .vscode diff --git a/apps/candlestick-patterns/app.py b/apps/candlestick-patterns/app.py index ad208326..1499c013 100644 --- a/apps/candlestick-patterns/app.py +++ b/apps/candlestick-patterns/app.py @@ -6,6 +6,7 @@ # Run this app with `python app.py` and # visit http://127.0.0.1:8050/ in your web browser. +import time import dash import dash_table import dash_core_components as dcc @@ -16,6 +17,8 @@ import plotly.graph_objects as go import os +import getpass +import glob import numpy as np import pandas as pd import json @@ -34,6 +37,8 @@ from vectorbt.portfolio.enums import Direction, DirectionConflictMode from vectorbt.portfolio.base import Portfolio +from dal_stock_sql.dal import Dal + USE_CACHING = os.environ.get( "USE_CACHING", "True", @@ -96,7 +101,7 @@ # Defaults data_path = 'data/data.h5' default_metric = 'Total Return [%]' -default_symbol = 'BTC-USD' +default_symbol = 'MSFT:ISLAND:USD' default_period = '1y' default_interval = '1d' default_date_range = [0, 1] @@ -463,6 +468,10 @@ "Reset", id="reset_button" ), + html.Button( + "CleanCache", + id="clean_button" + ), html.Details( open=True, children=[ @@ -509,7 +518,20 @@ value=default_interval, ), ] - ) + ), + dbc.Col( + children=[ + html.Label("Source:"), + dcc.Dropdown( + id="datasource", + options=[{"value": 'yahoo', "label": 'yahoo'}, + {"value": 'csv', "label": 'csv'}, + {"value": 'csv_all', "label": 'csv_all'}, + {"value": 'sql', "label": 'sql'}], + value='csv', + ), + ] + ), ], ), html.Label("Filter period:"), @@ -938,8 +960,26 @@ ) +data_mode = 'sql' +df_all_symbol = None +sql_dal = None + + @cache.memoize() def fetch_data(symbol, period, interval, auto_adjust, back_adjust): + """Fetch OHLCV data from backend.""" + global data_mode + if data_mode == 'csv_all': + return fetch_data_csv_all(symbol, period, interval, auto_adjust, back_adjust) + elif data_mode == 'csv': + return fetch_data_csv(symbol, period, interval, auto_adjust, back_adjust) + elif data_mode == 'sql': + return fetch_data_sql(symbol, period, interval, auto_adjust, back_adjust) + return fetch_data_yf(symbol, period, interval, auto_adjust, back_adjust) + + +@cache.memoize() +def fetch_data_yf(symbol, period, interval, auto_adjust, back_adjust): """Fetch OHLCV data from Yahoo! Finance.""" return yf.Ticker(symbol).history( period=period, @@ -950,6 +990,73 @@ def fetch_data(symbol, period, interval, auto_adjust, back_adjust): ) +@cache.memoize() +def fetch_data_csv(symbol, period, interval, auto_adjust, back_adjust, csvdir='~/trade/data/kaggle_allUS_daily_with_volume_yahoo/stocks'): + """Fetch OHLCV data from csv containing one symbols.""" + csvfile = csvdir + '/' + symbol + '.csv' + _start = time.perf_counter() + df = pd.read_csv(csvfile, parse_dates=['Date'], index_col='Date') + _duration = time.perf_counter() - _start + return df + + +@cache.memoize() +def fetch_data_csv_all(symbol, period, interval, auto_adjust, back_adjust, csvfile='~/trade/data/test.csv'): + """Fetch OHLCV data from csv containing multiple symbols.""" + global df_all_symbol + csvfile = '~/trade/data/test.csv' + if df_all_symbol is None: + _start = time.perf_counter() + df_all_symbol = pd.read_csv(csvfile, parse_dates=['date']) + df_all_symbol = df_all_symbol.rename( + columns={"date": "Date", "open": "Open", "high": "High", "low": "Low", "close": "Close", + "volume": "Volume"}) + df_all_symbol['Date'] = df_all_symbol['Date'].map( + lambda t: pd.to_datetime(t.replace(tzinfo=None)).to_pydatetime()) + df_all_symbol = df_all_symbol.set_index(pd.DatetimeIndex(df_all_symbol['Date'])) + _duration = time.perf_counter() - _start + res = df_all_symbol[df_all_symbol['symbol'] == symbol] + res = res.drop(['symbol'], axis=1) + return res + + +@cache.memoize() +def fetch_data_sql(symbol, period, interval, auto_adjust, back_adjust): + """Fetch OHLCV data from SQL.""" + global sql_dal + if interval == '60m': + dbtableRead = 'stock_market.ohlcv_hour1' + elif interval == '15m': + dbtableRead = 'stock_market.ohlcv_minute15' + elif interval == '1d': + dbtableRead = 'stock_market.ohlcv_day1' + else: + raise(ValueError(f"error unknown interval {interval}")) + if sql_dal is None: + user = getpass.getuser() + password = os.getenv('pg_password') + sql_dal = Dal.Instance() + sql_dal.init(user=user, password=password) + res = sql_dal.get_one_symbol(dbtableRead, symbol) + res = res.rename( + columns={"date": "Date", "open": "Open", "high": "High", "low": "Low", "close": "Close", + "volume": "Volume"}) + return res + + +@app.callback( + [Output('clean_button', 'children')], + [Input('clean_button', 'n_clicks')], + prevent_initial_call=True +) +def clean_cache(_): + """Clean all data in cache.""" + files = glob.glob('data/*') + for f in files: + os.remove(f) + return + + @app.callback( [Output('data_signal', 'children'), Output('index_signal', 'children')], @@ -962,8 +1069,11 @@ def update_data(symbol, period, interval, yf_options): """Store data into a hidden DIV to avoid repeatedly calling Yahoo's API.""" auto_adjust = 'auto_adjust' in yf_options back_adjust = 'back_adjust' in yf_options + _start = time.perf_counter() df = fetch_data(symbol, period, interval, auto_adjust, back_adjust) - return df.to_json(date_format='iso', orient='split'), df.index.tolist() + _duration = time.perf_counter() - _start + res = df.to_json(date_format='iso', orient='split'), df.index.tolist() + return res @app.callback( diff --git a/apps/candlestick-patterns/dal_stock_sql/dal.py b/apps/candlestick-patterns/dal_stock_sql/dal.py new file mode 100644 index 00000000..0216148e --- /dev/null +++ b/apps/candlestick-patterns/dal_stock_sql/dal.py @@ -0,0 +1,127 @@ +import datetime as dt +import psycopg2 +from psycopg2 import pool +import pandas as pd +import logging +import sys +from dal_stock_sql.util import Singleton, timedelta_to_postgres_interval + +logger = logging.getLogger(__name__) + + +@Singleton +class Dal: + def init(self, database='postgres', user='admin', password='admin', host='127.0.0.1', port='5432', + ver_twsapi=10, maxconn=2): + self.ver_twsapi = ver_twsapi + # Establishing the connection + logger.info('connecting to host %s and DB %s' % (host, database)) + self.connection_pool = psycopg2.pool.ThreadedConnectionPool( + 1, maxconn, # minconn, maxconn + user=user, + password=password, + host=host, + port=port, + database=database + ) + + def get_connection(self): + try: + conn = self.connection_pool.getconn() + except Exception as e: + raise e + return conn + + def release_connection(self, connection): + self.connection_pool.putconn(connection) + + def end(self): + self.connection_pool.closeall() + + def get_all_symbol(self, dbtable: str, date_start: dt = None, date_end: dt = None): + if None not in [date_start, date_end]: + sql = '''SELECT * FROM %s WHERE date >= to_timestamp(%s) AND date <= to_timestamp(%s);''' % \ + (dbtable, date_start.strftime('%s'), date_end.strftime('%s')) + elif date_start == date_end: + sql = '''SELECT * FROM %s ;''' % (dbtable) + print(sql) + elif date_end is None: + sql = '''SELECT * FROM %s WHERE date >= to_timestamp(%s);''' % \ + (dbtable, date_start.strftime('%s')) + else: + sql = '''SELECT * FROM %s WHERE date <= to_timestamp(%s);''' % \ + (dbtable, date_end.strftime('%s')) + # pandas only supports SQLAlchemy connectable (engine/connection) or database string URI or sqlite3 DBAPI2 connection. Other DBAPI2 objects are not tested. Please consider using SQLAlchemy. + conn = self.get_connection() + try: + with conn.cursor() as cursor: + cursor.execute(sql) + result = cursor.fetchall() + columns = [desc[0] for desc in cursor.description] + cursor.close() + df = pd.DataFrame(result, columns=columns) + # Specify the types directly at DataFrame creation + df = df.astype({ + 'date': 'datetime64[ns, UTC]', + 'open': 'float', + 'high': 'float', + 'low': 'float', + 'close': 'float', + 'volume': 'int' + }) + except Exception as e: + raise e + finally: + self.release_connection(conn) + # remove TZ otherwise pandas DateTimeIndex lookup in pandas do not work + #df['date'] = df['date'].map(lambda t: pd.to_datetime(t.replace(tzinfo=None)).to_pydatetime()) + df = df.set_index(pd.DatetimeIndex(df['date'])) + # drop bar if no volume + df = df[df.volume != -1] + return df + + def get_one_symbol(self, dbtable: str, symbol: str, date_start: dt = None, date_end: dt = None): + if ':' not in symbol: + logger.error('Incorrect symbol %s' % symbol) + return + # raise ValueError + if None not in [date_start, date_end]: + sql = '''SELECT * FROM %s WHERE symbol = '%s' AND date >= to_timestamp(%s) AND date <= to_timestamp(%s);''' % \ + (dbtable, symbol, date_start.strftime('%s'), date_end.strftime('%s')) + elif date_start == date_end: + sql = '''SELECT * FROM %s WHERE symbol = '%s';''' % (dbtable, symbol) + print(sql) + elif date_end is None: + sql = '''SELECT * FROM %s WHERE symbol = '%s' AND date >= to_timestamp(%s);''' % \ + (dbtable, symbol, date_start.strftime('%s')) + else: + sql = '''SELECT * FROM %s WHERE symbol = '%s' AND date <= to_timestamp(%s);''' % \ + (dbtable, symbol, date_end.strftime('%s')) + # pandas only supports SQLAlchemy connectable (engine/connection) or database string URI or sqlite3 DBAPI2 connection. Other DBAPI2 objects are not tested. Please consider using SQLAlchemy. + conn = self.get_connection() + try: + with conn.cursor() as cursor: + cursor.execute(sql) + result = cursor.fetchall() + columns = [desc[0] for desc in cursor.description] + cursor.close() + df = pd.DataFrame(result, columns=columns) + # Specify the types directly at DataFrame creation + df = df.astype({ + 'date': 'datetime64[ns, UTC]', + 'open': 'float', + 'high': 'float', + 'low': 'float', + 'close': 'float', + 'volume': 'int' + }) + except Exception as e: + raise e + finally: + self.release_connection(conn) + # remove TZ otherwise pandas DateTimeIndex lookup in pandas do not work + #df['date'] = df['date'].map(lambda t: pd.to_datetime(t.replace(tzinfo=None)).to_pydatetime()) + df = df.set_index(pd.DatetimeIndex(df['date'])) + # drop bar if no volume + df = df[df.volume != -1] + return df diff --git a/apps/candlestick-patterns/dal_stock_sql/util.py b/apps/candlestick-patterns/dal_stock_sql/util.py new file mode 100644 index 00000000..84590cf9 --- /dev/null +++ b/apps/candlestick-patterns/dal_stock_sql/util.py @@ -0,0 +1,158 @@ +import os +import time +import logging +from math import ceil +import datetime as dt +import pytz + + +# Convert timedelta to PostgreSQL-compatible interval string +def timedelta_to_postgres_interval(td): + days = td.days + seconds = td.seconds + microseconds = td.microseconds + + # Extract hours, minutes, and seconds from the total seconds + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + seconds = seconds % 60 + + # Format the interval string + interval_str = f"{days} days {hours} hours {minutes} minutes {seconds} seconds" + return interval_str + + +def nbar_by_day(bar_size): + val = int(bar_size.split()[0]) + unit = bar_size.split()[1] + span_paris = dt.timedelta(minutes=510) + if unit == 'day': + d = 1 + elif unit == 'hour': + d = 9 + elif unit == 'mins': + d = ceil(span_paris / dt.timedelta(minutes=val)) + else: + raise NotImplementedError + return d + + +def barsize_to_timedelta(bar_size, val=None): + if val is None: + val = int(bar_size.split()[0]) + unit = bar_size.split()[1] + if unit == 'day': + d = dt.timedelta(days=val) + elif unit == 'hour': + d = dt.timedelta(hours=val) + elif unit == 'mins': + d = dt.timedelta(minutes=val) + else: + raise NotImplementedError + return d + + +def barsize_to_end(bar_size, timezone=None): + if not timezone: + timezone = pytz.timezone("Europe/Paris") + end = dt.datetime.now() + end = timezone.localize(end) + if bar_size == '1 day': + end -= dt.timedelta(days=1) + elif bar_size == '1 week': + end -= dt.timedelta(weeks=1) + elif bar_size == '1 hour': + end -= dt.timedelta(minutes=65) + elif bar_size == '15 mins': + end -= dt.timedelta(minutes=20) + elif bar_size == '5 mins': + end -= dt.timedelta(minutes=10) + else: + raise NotImplementedError + return end + + +def barsize_to_table(bar_size): + table = None + if bar_size == '1 day': + table = 'day1' + elif bar_size == '1 week': + table = 'week1' + elif bar_size == '1 hour': + table = 'hour1' + elif bar_size == '15 mins': + table = 'minute15' + elif bar_size == '5 mins': + table = 'minute5' + else: + raise NotImplementedError + return table + + +class Singleton: + """ + A non-thread-safe helper class to ease implementing singletons. + This should be used as a decorator -- not a metaclass -- to the + class that should be a singleton. + + The decorated class can define one `__init__` function that + takes only the `self` argument. Also, the decorated class cannot be + inherited from. Other than that, there are no restrictions that apply + to the decorated class. + + To get the singleton instance, use the `Instance` method. Trying + to use `__call__` will result in a `TypeError` being raised. + + """ + + def __init__(self, decorated): + self._decorated = decorated + + def Instance(self): + """ + Returns the singleton instance. Upon its first call, it creates a + new instance of the decorated class and calls its `__init__` method. + On all subsequent calls, the already created instance is returned. + + """ + try: + return self._instance + except AttributeError: + self._instance = self._decorated() + return self._instance + + def __call__(self): + raise TypeError('Singletons must be accessed through `Instance()`.') + + def __instancecheck__(self, inst): + return isinstance(inst, self._decorated) + + +def printWhenExecuting(fn): + def fn2(self): + print(" doing", fn.__name__) + fn(self) + print(" done w/", fn.__name__) + + return fn2 + +def SetupLogger(): + if not os.path.exists("../log"): + os.makedirs("../log") + + time.strftime("pyibapi.%Y%m%d_%H%M%S.log") + recfmt = '%(asctime)s.%(msecs)03d %(levelname)s (%(threadName)s) %(filename)s:%(lineno)d %(message)s' + + timefmt = '%Y-%m-%dT%H:%M:%S' + + # logging.basicConfig( level=logging.DEBUG, + # format=recfmt, datefmt=timefmt) + logging.basicConfig(filename=time.strftime("../log/past_ibkr.%y%m%d_%H%M%S.log"), + filemode="w", + level=logging.INFO, + format=recfmt, datefmt=timefmt) + logger = logging.getLogger() + console = logging.StreamHandler() + console.setLevel(logging.DEBUG) + logger.addHandler(console) + From 89b252393ce681663da09dfcbd2139f1b53f2d29 Mon Sep 17 00:00:00 2001 From: Julien Laurenceau Date: Wed, 26 Mar 2025 16:32:11 +0100 Subject: [PATCH 2/4] fix dal --- apps/candlestick-patterns/dal_stock_sql/dal.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/candlestick-patterns/dal_stock_sql/dal.py b/apps/candlestick-patterns/dal_stock_sql/dal.py index 0216148e..c859d720 100644 --- a/apps/candlestick-patterns/dal_stock_sql/dal.py +++ b/apps/candlestick-patterns/dal_stock_sql/dal.py @@ -76,8 +76,6 @@ def get_all_symbol(self, dbtable: str, date_start: dt = None, date_end: dt = Non # remove TZ otherwise pandas DateTimeIndex lookup in pandas do not work #df['date'] = df['date'].map(lambda t: pd.to_datetime(t.replace(tzinfo=None)).to_pydatetime()) df = df.set_index(pd.DatetimeIndex(df['date'])) - # drop bar if no volume - df = df[df.volume != -1] return df def get_one_symbol(self, dbtable: str, symbol: str, date_start: dt = None, date_end: dt = None): From 4710b1f5cf0ae96ee999aa67f5db92da96cb72e8 Mon Sep 17 00:00:00 2001 From: Julien Laurenceau Date: Thu, 8 May 2025 19:47:29 +0200 Subject: [PATCH 3/4] mr: remove util and rename class --- .gitignore | 2 - apps/candlestick-patterns/app.py | 12 +- .../candlestick-patterns/dal_stock_sql/dal.py | 13 +- .../dal_stock_sql/util.py | 158 ------------------ 4 files changed, 13 insertions(+), 172 deletions(-) delete mode 100644 apps/candlestick-patterns/dal_stock_sql/util.py diff --git a/.gitignore b/.gitignore index 15045139..1eab1e27 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -*.secret -secret.* .ipynb_checkpoints __pycache__ .vscode diff --git a/apps/candlestick-patterns/app.py b/apps/candlestick-patterns/app.py index 1499c013..74c64d26 100644 --- a/apps/candlestick-patterns/app.py +++ b/apps/candlestick-patterns/app.py @@ -37,7 +37,7 @@ from vectorbt.portfolio.enums import Direction, DirectionConflictMode from vectorbt.portfolio.base import Portfolio -from dal_stock_sql.dal import Dal +from dal_stock_sql.dal import MarketDataRepository USE_CACHING = os.environ.get( "USE_CACHING", @@ -101,7 +101,7 @@ # Defaults data_path = 'data/data.h5' default_metric = 'Total Return [%]' -default_symbol = 'MSFT:ISLAND:USD' +default_symbol = 'BTC-USD' default_period = '1y' default_interval = '1d' default_date_range = [0, 1] @@ -1025,17 +1025,17 @@ def fetch_data_sql(symbol, period, interval, auto_adjust, back_adjust): """Fetch OHLCV data from SQL.""" global sql_dal if interval == '60m': - dbtableRead = 'stock_market.ohlcv_hour1' + dbtableRead = 'stock_market.ohlcv_1_hour' elif interval == '15m': - dbtableRead = 'stock_market.ohlcv_minute15' + dbtableRead = 'stock_market.ohlcv_15_minute' elif interval == '1d': - dbtableRead = 'stock_market.ohlcv_day1' + dbtableRead = 'stock_market.ohlcv_1_day' else: raise(ValueError(f"error unknown interval {interval}")) if sql_dal is None: user = getpass.getuser() password = os.getenv('pg_password') - sql_dal = Dal.Instance() + sql_dal = MarketDataRepository.Instance() sql_dal.init(user=user, password=password) res = sql_dal.get_one_symbol(dbtableRead, symbol) res = res.rename( diff --git a/apps/candlestick-patterns/dal_stock_sql/dal.py b/apps/candlestick-patterns/dal_stock_sql/dal.py index c859d720..fe675b03 100644 --- a/apps/candlestick-patterns/dal_stock_sql/dal.py +++ b/apps/candlestick-patterns/dal_stock_sql/dal.py @@ -3,14 +3,17 @@ from psycopg2 import pool import pandas as pd import logging -import sys -from dal_stock_sql.util import Singleton, timedelta_to_postgres_interval + logger = logging.getLogger(__name__) -@Singleton -class Dal: +class MarketDataRepository: + def __init__(self): + """Initialize basic structure but don't connect yet""" + self.connection_pool = None + self.ver_twsapi = None + def init(self, database='postgres', user='admin', password='admin', host='127.0.0.1', port='5432', ver_twsapi=10, maxconn=2): self.ver_twsapi = ver_twsapi @@ -120,6 +123,4 @@ def get_one_symbol(self, dbtable: str, symbol: str, date_start: dt = None, date_ # remove TZ otherwise pandas DateTimeIndex lookup in pandas do not work #df['date'] = df['date'].map(lambda t: pd.to_datetime(t.replace(tzinfo=None)).to_pydatetime()) df = df.set_index(pd.DatetimeIndex(df['date'])) - # drop bar if no volume - df = df[df.volume != -1] return df diff --git a/apps/candlestick-patterns/dal_stock_sql/util.py b/apps/candlestick-patterns/dal_stock_sql/util.py deleted file mode 100644 index 84590cf9..00000000 --- a/apps/candlestick-patterns/dal_stock_sql/util.py +++ /dev/null @@ -1,158 +0,0 @@ -import os -import time -import logging -from math import ceil -import datetime as dt -import pytz - - -# Convert timedelta to PostgreSQL-compatible interval string -def timedelta_to_postgres_interval(td): - days = td.days - seconds = td.seconds - microseconds = td.microseconds - - # Extract hours, minutes, and seconds from the total seconds - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - seconds = seconds % 60 - - # Format the interval string - interval_str = f"{days} days {hours} hours {minutes} minutes {seconds} seconds" - return interval_str - - -def nbar_by_day(bar_size): - val = int(bar_size.split()[0]) - unit = bar_size.split()[1] - span_paris = dt.timedelta(minutes=510) - if unit == 'day': - d = 1 - elif unit == 'hour': - d = 9 - elif unit == 'mins': - d = ceil(span_paris / dt.timedelta(minutes=val)) - else: - raise NotImplementedError - return d - - -def barsize_to_timedelta(bar_size, val=None): - if val is None: - val = int(bar_size.split()[0]) - unit = bar_size.split()[1] - if unit == 'day': - d = dt.timedelta(days=val) - elif unit == 'hour': - d = dt.timedelta(hours=val) - elif unit == 'mins': - d = dt.timedelta(minutes=val) - else: - raise NotImplementedError - return d - - -def barsize_to_end(bar_size, timezone=None): - if not timezone: - timezone = pytz.timezone("Europe/Paris") - end = dt.datetime.now() - end = timezone.localize(end) - if bar_size == '1 day': - end -= dt.timedelta(days=1) - elif bar_size == '1 week': - end -= dt.timedelta(weeks=1) - elif bar_size == '1 hour': - end -= dt.timedelta(minutes=65) - elif bar_size == '15 mins': - end -= dt.timedelta(minutes=20) - elif bar_size == '5 mins': - end -= dt.timedelta(minutes=10) - else: - raise NotImplementedError - return end - - -def barsize_to_table(bar_size): - table = None - if bar_size == '1 day': - table = 'day1' - elif bar_size == '1 week': - table = 'week1' - elif bar_size == '1 hour': - table = 'hour1' - elif bar_size == '15 mins': - table = 'minute15' - elif bar_size == '5 mins': - table = 'minute5' - else: - raise NotImplementedError - return table - - -class Singleton: - """ - A non-thread-safe helper class to ease implementing singletons. - This should be used as a decorator -- not a metaclass -- to the - class that should be a singleton. - - The decorated class can define one `__init__` function that - takes only the `self` argument. Also, the decorated class cannot be - inherited from. Other than that, there are no restrictions that apply - to the decorated class. - - To get the singleton instance, use the `Instance` method. Trying - to use `__call__` will result in a `TypeError` being raised. - - """ - - def __init__(self, decorated): - self._decorated = decorated - - def Instance(self): - """ - Returns the singleton instance. Upon its first call, it creates a - new instance of the decorated class and calls its `__init__` method. - On all subsequent calls, the already created instance is returned. - - """ - try: - return self._instance - except AttributeError: - self._instance = self._decorated() - return self._instance - - def __call__(self): - raise TypeError('Singletons must be accessed through `Instance()`.') - - def __instancecheck__(self, inst): - return isinstance(inst, self._decorated) - - -def printWhenExecuting(fn): - def fn2(self): - print(" doing", fn.__name__) - fn(self) - print(" done w/", fn.__name__) - - return fn2 - -def SetupLogger(): - if not os.path.exists("../log"): - os.makedirs("../log") - - time.strftime("pyibapi.%Y%m%d_%H%M%S.log") - recfmt = '%(asctime)s.%(msecs)03d %(levelname)s (%(threadName)s) %(filename)s:%(lineno)d %(message)s' - - timefmt = '%Y-%m-%dT%H:%M:%S' - - # logging.basicConfig( level=logging.DEBUG, - # format=recfmt, datefmt=timefmt) - logging.basicConfig(filename=time.strftime("../log/past_ibkr.%y%m%d_%H%M%S.log"), - filemode="w", - level=logging.INFO, - format=recfmt, datefmt=timefmt) - logger = logging.getLogger() - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - logger.addHandler(console) - From 7a883240e95371574aff8340dad311908b9e76f1 Mon Sep 17 00:00:00 2001 From: Julien Laurenceau Date: Sun, 11 May 2025 10:49:11 +0200 Subject: [PATCH 4/4] fix : clean singleton --- apps/candlestick-patterns/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/candlestick-patterns/app.py b/apps/candlestick-patterns/app.py index 74c64d26..6dae0685 100644 --- a/apps/candlestick-patterns/app.py +++ b/apps/candlestick-patterns/app.py @@ -1035,7 +1035,7 @@ def fetch_data_sql(symbol, period, interval, auto_adjust, back_adjust): if sql_dal is None: user = getpass.getuser() password = os.getenv('pg_password') - sql_dal = MarketDataRepository.Instance() + sql_dal = MarketDataRepository() sql_dal.init(user=user, password=password) res = sql_dal.get_one_symbol(dbtableRead, symbol) res = res.rename(