Made a lot of changes to DataCache indicator data is not being saved to the database.

This commit is contained in:
Rob 2024-09-15 14:05:08 -03:00
parent f2b7621b6d
commit 1ff21b56dd
10 changed files with 1911 additions and 1150 deletions

View File

@ -14,11 +14,14 @@ from trade import Trades
class BrighterTrades: class BrighterTrades:
def __init__(self): def __init__(self):
# Object that interacts and maintains exchange_interface and account data
self.exchanges = ExchangeInterface()
# Object that interacts with the persistent data. # Object that interacts with the persistent data.
self.data = DataCache(self.exchanges) self.data = DataCache()
# Object that interacts and maintains exchange_interface and account data
self.exchanges = ExchangeInterface(self.data)
# Set the exchange for datacache to use
self.data.set_exchange(self.exchanges)
# Configuration for the app # Configuration for the app
self.config = Configuration() self.config = Configuration()
@ -34,7 +37,7 @@ class BrighterTrades:
config=self.config) config=self.config)
# Object that interacts with and maintains data from available indicators # Object that interacts with and maintains data from available indicators
self.indicators = Indicators(self.candles, self.users) self.indicators = Indicators(self.candles, self.users, self.data)
# Object that maintains the trades data # Object that maintains the trades data
self.trades = Trades(self.users) self.trades = Trades(self.users)
@ -186,8 +189,8 @@ class BrighterTrades:
:return: bool - True on success. :return: bool - True on success.
""" """
active_exchanges = self.users.get_exchanges(user_name, category='active_exchanges') active_exchanges = self.users.get_exchanges(user_name, category='active_exchanges')
success = False
success = False
for exchange in active_exchanges: for exchange in active_exchanges:
keys = self.users.get_api_keys(user_name, exchange) keys = self.users.get_api_keys(user_name, exchange)
result = self.connect_or_config_exchange(user_name=user_name, result = self.connect_or_config_exchange(user_name=user_name,
@ -391,7 +394,8 @@ class BrighterTrades:
} }
try: try:
if self.exchanges.exchange_data.query("user == @user_name and name == @exchange_name").empty: if self.data.get_cache_item().get_cache('exchange_data').query([('user', user_name),
('name', exchange_name)]).empty:
# Exchange is not connected, try to connect # Exchange is not connected, try to connect
success = self.exchanges.connect_exchange(exchange_name=exchange_name, user_name=user_name, success = self.exchanges.connect_exchange(exchange_name=exchange_name, user_name=user_name,
api_keys=api_keys) api_keys=api_keys)

View File

@ -180,7 +180,7 @@ class DataCache:
params.append(additional_filter[1]) params.append(additional_filter[1])
# Execute the SQL query to remove the row from the database # Execute the SQL query to remove the row from the database
self.db.execute_sql(sql, tuple(params)) self.db.execute_sql(sql, params)
logger.info( logger.info(
f"Row removed from database: table={table}, filter={filter_vals}," f"Row removed from database: table={table}, filter={filter_vals},"
f" additional_filter={additional_filter}") f" additional_filter={additional_filter}")

File diff suppressed because it is too large Load Diff

View File

@ -85,7 +85,7 @@ class Database:
def __init__(self, db_file: str = None): def __init__(self, db_file: str = None):
self.db_file = db_file self.db_file = db_file
def execute_sql(self, sql: str, params: tuple = ()) -> None: def execute_sql(self, sql: str, params: list = None) -> None:
""" """
Executes a raw SQL statement with optional parameters. Executes a raw SQL statement with optional parameters.
@ -115,22 +115,28 @@ class Database:
error = f"Couldn't fetch item {item_name} from {table_name} where {filter_vals[0]} = {filter_vals[1]}" error = f"Couldn't fetch item {item_name} from {table_name} where {filter_vals[0]} = {filter_vals[1]}"
raise ValueError(error) raise ValueError(error)
def get_rows_where(self, table: str, filter_vals: Tuple[str, Any]) -> pd.DataFrame | None: def get_rows_where(self, table: str, filter_vals: List[Tuple[str, Any]]) -> pd.DataFrame | None:
""" """
Returns a DataFrame containing all rows of a table that meet the filter criteria. Returns a DataFrame containing all rows of a table that meet the filter criteria.
:param table: Name of the table. :param table: Name of the table.
:param filter_vals: Tuple of column name and value to filter by. :param filter_vals: List of tuples containing column names and values to filter by.
:return: DataFrame of the query result or None if empty or column does not exist. :return: DataFrame of the query result or None if empty or column does not exist.
""" """
try: try:
with SQLite(self.db_file) as con: with SQLite(self.db_file) as con:
qry = f"SELECT * FROM {table} WHERE {filter_vals[0]} = ?" # Construct the WHERE clause with multiple conditions
result = pd.read_sql(qry, con, params=(filter_vals[1],)) where_clause = " AND ".join([f"{col} = ?" for col, _ in filter_vals])
params = [val for _, val in filter_vals]
# Prepare and execute the query with the constructed WHERE clause
qry = f"SELECT * FROM {table} WHERE {where_clause}"
result = pd.read_sql(qry, con, params=params)
return result if not result.empty else None return result if not result.empty else None
except (sqlite3.OperationalError, pd.errors.DatabaseError) as e: except (sqlite3.OperationalError, pd.errors.DatabaseError) as e:
# Log the error or handle it appropriately # Log the error or handle it appropriately
print(f"Error querying table '{table}' for column '{filter_vals[0]}': {e}") print(f"Error querying table '{table}' with filters {filter_vals}: {e}")
return None return None
def insert_dataframe(self, df: pd.DataFrame, table: str) -> int: def insert_dataframe(self, df: pd.DataFrame, table: str) -> int:

View File

@ -1,8 +1,10 @@
import logging import logging
from typing import List, Any, Dict from typing import List, Any, Dict, TYPE_CHECKING
import pandas as pd import pandas as pd
import ccxt import ccxt
from Exchange import Exchange from Exchange import Exchange
from DataCache_v3 import DataCache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,23 +19,38 @@ class ExchangeInterface:
Connects, maintains, and routes data requests to/from multiple exchanges. Connects, maintains, and routes data requests to/from multiple exchanges.
""" """
def __init__(self): def __init__(self, cache_manager: DataCache):
self.exchange_data = pd.DataFrame(columns=['user', 'name', 'reference', 'balances']) self.cache_manager = cache_manager
self.cache_manager.create_cache(
name='exchange_data',
cache_type='table',
size_limit=100,
eviction_policy='deny',
columns=['user', 'name', 'reference', 'balances']
)
self.available_exchanges = self.get_ccxt_exchanges() self.available_exchanges = self.get_ccxt_exchanges()
# Create a default user and exchange for unsigned requests self.default_ex_name = 'binance'
default_ex_name = 'binance' self.default_exchange = None
self.connect_exchange(exchange_name=default_ex_name, user_name='default')
self.default_exchange = self.get_exchange(ename=default_ex_name, uname='default')
def get_ccxt_exchanges(self) -> List[str]: def connect_default_exchange(self):
if self.default_exchange is not None:
return
# Create a default user and exchange for unsigned requests
self.connect_exchange(exchange_name=self.default_ex_name, user_name='default')
self.default_exchange = self.get_exchange(ename=self.default_ex_name, uname='default')
@staticmethod
def get_ccxt_exchanges() -> List[str]:
"""Retrieve the list of available exchanges from CCXT.""" """Retrieve the list of available exchanges from CCXT."""
return ccxt.exchanges return ccxt.exchanges
def get_public_exchanges(self) -> List[str]: @staticmethod
def get_public_exchanges() -> List[str]:
"""Return a list of public exchanges available from CCXT.""" """Return a list of public exchanges available from CCXT."""
public_list = [] public_list = []
file_path = 'src\working_public_exchanges.txt' file_path = r"src\working_public_exchanges.txt"
try: try:
with open(file_path, 'r') as file: with open(file_path, 'r') as file:
@ -70,8 +87,12 @@ class ExchangeInterface:
:param exchange: The Exchange object to add. :param exchange: The Exchange object to add.
""" """
try: try:
row = {'user': user_name, 'name': exchange.name, 'reference': exchange, 'balances': exchange.balances} row = pd.DataFrame([{
self.exchange_data = add_row(self.exchange_data, row) 'user': user_name, 'name': exchange.name,
'reference': exchange, 'balances': exchange.balances}])
cache = self.cache_manager.get_cache('exchange_data')
cache.add_table(df=row)
except Exception as e: except Exception as e:
logger.error(f"Couldn't create an instance of the exchange! {str(e)}") logger.error(f"Couldn't create an instance of the exchange! {str(e)}")
raise raise
@ -87,7 +108,9 @@ class ExchangeInterface:
if not ename or not uname: if not ename or not uname:
raise ValueError('Missing argument!') raise ValueError('Missing argument!')
exchange_data = self.exchange_data.query("name == @ename and user == @uname") cache = self.cache_manager.get_cache('exchange_data')
exchange_data = cache.query([('name', ename), ('user', uname)])
if exchange_data.empty: if exchange_data.empty:
raise ValueError('No matching exchange found.') raise ValueError('No matching exchange found.')
@ -100,7 +123,9 @@ class ExchangeInterface:
:param user_name: The name of the user. :param user_name: The name of the user.
:return: A list of connected exchange names. :return: A list of connected exchange names.
""" """
return self.exchange_data.loc[self.exchange_data['user'] == user_name, 'name'].tolist() cache = self.cache_manager.get_cache('exchange_data')
exchanges = cache.query([('user', user_name)])
return exchanges['name'].tolist()
def get_available_exchanges(self) -> List[str]: def get_available_exchanges(self) -> List[str]:
"""Get a list of available exchanges.""" """Get a list of available exchanges."""
@ -114,9 +139,10 @@ class ExchangeInterface:
:param name: The name of the exchange. :param name: The name of the exchange.
:return: A Series containing the balances. :return: A Series containing the balances.
""" """
filtered_data = self.exchange_data.query("user == @user_name and name == @name") cache = self.cache_manager.get_cache('exchange_data')
if not filtered_data.empty: exchange = cache.query([('user', user_name), ('name', name)])
return filtered_data.iloc[0]['balances'] if not exchange.empty:
return exchange.iloc[0]['balances']
else: else:
return pd.Series(dtype='object') # Return an empty Series if no match is found return pd.Series(dtype='object') # Return an empty Series if no match is found
@ -127,12 +153,15 @@ class ExchangeInterface:
:param user_name: The name of the user. :param user_name: The name of the user.
:return: A dictionary containing the balances of all connected exchanges. :return: A dictionary containing the balances of all connected exchanges.
""" """
filtered_data = self.exchange_data.loc[self.exchange_data['user'] == user_name, ['name', 'balances']] # Query exchange data for the given user
if filtered_data.empty: cache = self.cache_manager.get_cache('exchange_data')
return {} exchanges = cache.query([('user', user_name)])
balances_dict = {row['name']: row['balances'] for _, row in filtered_data.iterrows()} # Select 'name' and 'balances' columns for all rows
return balances_dict filtered_data = exchanges.loc[:, ['name', 'balances']]
# Return a dictionary where exchange 'name' is the key and 'balances' is the value
return {row['name']: row['balances'] for _, row in filtered_data.iterrows()}
def get_all_activated(self, user_name: str, fetch_type: str = 'trades') -> Dict[str, List[Dict[str, Any]]]: def get_all_activated(self, user_name: str, fetch_type: str = 'trades') -> Dict[str, List[Dict[str, Any]]]:
""" """
@ -142,16 +171,24 @@ class ExchangeInterface:
:param fetch_type: The type of data to fetch ('trades' or 'orders'). :param fetch_type: The type of data to fetch ('trades' or 'orders').
:return: A dictionary indexed by exchange name with lists of active trades or open orders. :return: A dictionary indexed by exchange name with lists of active trades or open orders.
""" """
filtered_data = self.exchange_data.loc[self.exchange_data['user'] == user_name, ['name', 'reference']] cache = self.cache_manager.get_cache('exchange_data')
exchanges = cache.query([('user', user_name)])
# Select the 'name' and 'reference' columns
filtered_data = exchanges.loc[:, ['name', 'reference']]
if filtered_data.empty: if filtered_data.empty:
return {} return {}
data_dict = {} data_dict = {}
# Iterate over the filtered data
for name, reference in filtered_data.itertuples(index=False): for name, reference in filtered_data.itertuples(index=False):
if pd.isna(reference): if pd.isna(reference):
continue continue
try: try:
# Fetch active trades or open orders based on the fetch_type
if fetch_type == 'trades': if fetch_type == 'trades':
data = reference.get_active_trades() data = reference.get_active_trades()
elif fetch_type == 'orders': elif fetch_type == 'orders':
@ -222,6 +259,7 @@ class ExchangeInterface:
:return: The current price. :return: The current price.
""" """
if price_source is None: if price_source is None:
self.connect_default_exchange()
return self.default_exchange.get_price(symbol=symbol) return self.default_exchange.get_price(symbol=symbol)
else: else:
raise ValueError(f'No implementation for price source: {price_source}') raise ValueError(f'No implementation for price source: {price_source}')

View File

@ -2,9 +2,9 @@ import copy
import datetime as dt import datetime as dt
import json import json
import random import random
from typing import Any
from passlib.hash import bcrypt from passlib.hash import bcrypt
import pandas as pd import pandas as pd
from typing import Any
from DataCache_v3 import DataCache from DataCache_v3 import DataCache
@ -21,6 +21,16 @@ class BaseUser:
:param data_cache: Object responsible for managing cached data and database interaction. :param data_cache: Object responsible for managing cached data and database interaction.
""" """
self.data = data_cache self.data = data_cache
# Create a table-based cache with specified columns
self.data.create_cache(name='users',
cache_type='table',
size_limit=100,
eviction_policy='deny',
default_expiration=dt.timedelta(hours=24),
columns=["id", "user_name", "status", "chart_views", "email",
"active_exchanges", "configured_exchanges", "password",
"api_keys", "signin_time", "active_indicators"]
)
def get_id(self, user_name: str) -> int: def get_id(self, user_name: str) -> int:
""" """
@ -29,7 +39,7 @@ class BaseUser:
:param user_name: The name of the user. :param user_name: The name of the user.
:return: The ID of the user as an integer. :return: The ID of the user as an integer.
""" """
return self.data.fetch_item( return self.data.fetch_datacache_item(
item_name='id', item_name='id',
cache_name='users', cache_name='users',
filter_vals=('user_name', user_name) filter_vals=('user_name', user_name)
@ -42,7 +52,7 @@ class BaseUser:
:param id: The id of the user. :param id: The id of the user.
:return: The name of the user as a str. :return: The name of the user as a str.
""" """
return self.data.fetch_item( return self.data.fetch_datacache_item(
item_name='user_name', item_name='user_name',
cache_name='users', cache_name='users',
filter_vals=('id', id) filter_vals=('id', id)
@ -55,10 +65,9 @@ class BaseUser:
:param user_name: The name of the user to remove from the cache. :param user_name: The name of the user to remove from the cache.
""" """
# Remove the user from the cache only # Remove the user from the cache only
self.data.remove_row( self.data.remove_row_from_datacache(cache_name='users',
cache_name='users', filter_vals=[('user_name', user_name)],
filter_vals=('user_name', user_name), remove_from_db=False remove_from_db=False)
)
def delete_user(self, user_name: str) -> None: def delete_user(self, user_name: str) -> None:
""" """
@ -66,10 +75,8 @@ class BaseUser:
:param user_name: The name of the user to delete. :param user_name: The name of the user to delete.
""" """
self.data.remove_row( self.data.remove_row_from_datacache(filter_vals=[('user_name', user_name)],
filter_vals=('user_name', user_name), cache_name='users')
cache_name='users'
)
def get_user_data(self, user_name: str) -> pd.DataFrame | None: def get_user_data(self, user_name: str) -> pd.DataFrame | None:
""" """
@ -81,10 +88,8 @@ class BaseUser:
:raises ValueError: If the user is not found in both the cache and the database. :raises ValueError: If the user is not found in both the cache and the database.
""" """
# Attempt to fetch the user data from the cache or database via DataCache # Attempt to fetch the user data from the cache or database via DataCache
user = self.data.get_or_fetch_rows( user = self.data.get_rows_from_datacache(
cache_name='users', cache_name='users', filter_vals=[('user_name', user_name)])
filter_vals=('user_name', user_name)
)
if user is None or user.empty: if user is None or user.empty:
raise ValueError(f"User '{user_name}' not found in database or cache!") raise ValueError(f"User '{user_name}' not found in database or cache!")
@ -100,9 +105,9 @@ class BaseUser:
:param new_data: The new data to be set. :param new_data: The new data to be set.
""" """
# Use DataCache to modify the user's data # Use DataCache to modify the user's data
self.data.modify_item( self.data.modify_datacache_item(
cache_name='users', cache_name='users',
filter_vals=('user_name', username), filter_vals=[('user_name', username)],
field_name=field_name, field_name=field_name,
new_data=new_data new_data=new_data
) )
@ -154,7 +159,7 @@ class UserAccountManagement(BaseUser):
:return: True if the password is correct, False otherwise. :return: True if the password is correct, False otherwise.
""" """
# Retrieve the hashed password using DataCache # Retrieve the hashed password using DataCache
user_data = self.data.get_or_fetch_rows(cache_name='users', filter_vals=('user_name', username)) user_data = self.data.get_rows_from_datacache(cache_name='users', filter_vals=[('user_name', username)])
if user_data is None or user_data.empty: if user_data is None or user_data.empty:
return False return False
@ -238,13 +243,13 @@ class UserAccountManagement(BaseUser):
def user_attr_is_taken(self, attr: str, val: str) -> bool: def user_attr_is_taken(self, attr: str, val: str) -> bool:
""" """
Checks if a specific user attribute (e.g., username, email) is already taken. Checks if a specific user attribute (e.g., username, email) is already taken.
:param attr: The attribute to check (e.g., 'user_name', 'email'). :param attr: The attribute to check (e.g., 'user_name', 'email').
:param val: The value of the attribute to check. :param val: The value of the attribute to check.
:return: True if the attribute is already taken, False otherwise. :return: True if the attribute is already taken, False otherwise.
""" """
# Use DataCache to check if the attribute is taken # Use DataCache to check if the attribute is taken
return self.data.is_attr_taken(cache_name='users', attr=attr, val=val) user_cache = self.data.get_rows_from_datacache('users', [(attr, val)])
return True if not user_cache.empty else False
def create_unique_guest_name(self) -> str | None: def create_unique_guest_name(self) -> str | None:
""" """
@ -262,7 +267,7 @@ class UserAccountManagement(BaseUser):
username = f'guest_{suffix}' username = f'guest_{suffix}'
# Check if the username already exists in the database # Check if the username already exists in the database
if not self.data.get_or_fetch_rows(cache_name='users', filter_vals=('user_name', username)): if not self.data.get_rows_from_datacache(cache_name='users', filter_vals=[('user_name', username)]):
return username return username
attempts += 1 attempts += 1
@ -298,7 +303,7 @@ class UserAccountManagement(BaseUser):
raise ValueError("Attributes must be a tuple of single key-value pair dictionaries.") raise ValueError("Attributes must be a tuple of single key-value pair dictionaries.")
# Retrieve the default user template from the database using DataCache # Retrieve the default user template from the database using DataCache
default_user = self.data.get_or_fetch_rows(cache_name='users', filter_vals=('user_name', 'guest')) default_user = self.data.get_rows_from_datacache(cache_name='users', filter_vals=[('user_name', 'guest')])
if default_user is None or default_user.empty: if default_user is None or default_user.empty:
raise ValueError("Default user template not found in the database.") raise ValueError("Default user template not found in the database.")
@ -314,8 +319,10 @@ class UserAccountManagement(BaseUser):
# Remove the 'id' column before inserting into the database # Remove the 'id' column before inserting into the database
new_user = new_user.drop(columns='id') new_user = new_user.drop(columns='id')
# Insert the modified user data into the database, skipping cache insertion # Insert the modified user as a single row, skipping cache
self.data.insert_df(df=new_user, cache_name="users", skip_cache=True) columns = tuple(new_user.columns)
values = tuple(new_user.iloc[0])
self.data.insert_row_into_datacache(cache_name="users", columns=columns, values=values, skip_cache=True)
def create_new_user(self, username: str, email: str, password: str) -> bool: def create_new_user(self, username: str, email: str, password: str) -> bool:
""" """
@ -464,7 +471,7 @@ class UserIndicatorManagement(UserExchangeManagement):
user_id = int(self.get_id(user_name)) user_id = int(self.get_id(user_name))
# Fetch the indicators from the database using DataCache # Fetch the indicators from the database using DataCache
df = self.data.get_or_fetch_rows(cache_name='indicators', filter_vals=('creator', user_id)) df = self.data.get_rows_from_datacache(cache_name='indicators', filter_vals=[('creator', user_id)])
# If indicators are found, process the JSON fields # If indicators are found, process the JSON fields
if df is not None and not df.empty: if df is not None and not df.empty:
@ -492,25 +499,11 @@ class UserIndicatorManagement(UserExchangeManagement):
columns = ('creator', 'name', 'visible', 'kind', 'source', 'properties') columns = ('creator', 'name', 'visible', 'kind', 'source', 'properties')
# Insert the row into the database and cache using DataCache # Insert the row into the database and cache using DataCache
self.data.insert_row(cache_name='indicators', columns=columns, values=values) self.data.insert_row_into_datacache(cache_name='indicators', columns=columns, values=values)
except Exception as e: except Exception as e:
print(f"Error saving indicator {indicator['name']} for creator {indicator['creator']}: {str(e)}") print(f"Error saving indicator {indicator['name']} for creator {indicator['creator']}: {str(e)}")
def remove_indicator(self, indicator_name: str, user_name: str) -> None:
"""
Removes a specific indicator from the database and cache.
:param indicator_name: The name of the indicator to remove.
:param user_name: The name of the user who created the indicator.
"""
user_id = int(self.get_id(user_name))
self.data.remove_row(
filter_vals=('name', indicator_name),
additional_filter=('creator', user_id),
cache_name='indicators'
)
def get_chart_view(self, user_name: str, prop: str | None = None): def get_chart_view(self, user_name: str, prop: str | None = None):
""" """
Fetches the chart view or one specific property of it for a specific user. Fetches the chart view or one specific property of it for a specific user.

View File

@ -1,10 +1,10 @@
import json import json
import random import random
from typing import Any, Optional, Dict from typing import Any, Optional, Dict
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import talib import talib
import datetime as dt
# A dictionary to hold both indicator types and their corresponding classes. # A dictionary to hold both indicator types and their corresponding classes.
indicators_registry = {} indicators_registry = {}
@ -255,16 +255,35 @@ indicators_registry['MACD'] = MACD
class Indicators: class Indicators:
def __init__(self, candles, users): def __init__(self, candles, users, cache_manager):
# Object manages and serves price and candle data. # Object manages and serves price and candle data.
self.candles = candles self.candles = candles
# A connection to an object that handles user data. # A connection to an object that handles user data.
self.users = users self.users = users
# Collection of instantiated indicators objects # A connection to an object that handles all data.
self.indicators = pd.DataFrame(columns=['creator', 'name', 'visible', self.cache_manager = cache_manager
'kind', 'source', 'properties', 'ref'])
# Cache for storing instantiated indicator objects
cache_manager.create_cache(
name='indicators',
cache_type='table',
size_limit=100,
eviction_policy='deny',
default_expiration=dt.timedelta(days=1),
columns=['creator', 'name', 'visible', 'kind', 'source', 'properties', 'ref']
)
# Cache for storing calculated indicator data
cache_manager.create_cache('indicator_data', cache_type='row', size_limit=100,
default_expiration=dt.timedelta(days=7), eviction_policy='evict')
# Cache for storing display properties indicators
cache_manager.create_cache('user_display_properties', cache_type='row',
size_limit=100,
default_expiration=dt.timedelta(days=1),
eviction_policy='evict')
# Available indicator types and classes from a global indicators_registry. # Available indicator types and classes from a global indicators_registry.
self.indicator_registry = indicators_registry self.indicator_registry = indicators_registry
@ -341,27 +360,34 @@ class Indicators:
:return: dict - A dictionary of indicator names as keys and their attributes as values. :return: dict - A dictionary of indicator names as keys and their attributes as values.
""" """
user_id = self.users.get_id(username) user_id = self.users.get_id(username)
if not user_id: if not user_id:
raise ValueError(f"Invalid user_name: {username}") raise ValueError(f"Invalid user_name: {username}")
# Fetch indicators based on visibility status
if only_enabled: if only_enabled:
indicators_df = self.indicators.query("creator == @user_id and visible == 1") indicators_df = self.cache_manager.get_rows_from_datacache('indicators', [('creator', user_id), ('visible', 1)])
else: else:
indicators_df = self.indicators.query('creator == @user_id') indicators_df = self.cache_manager.get_rows_from_datacache('indicators', [('creator', user_id)])
if indicators_df.empty: # Check if the DataFrame is empty
# Attempt to load from storage. if indicators_df is None or indicators_df.empty:
self.load_indicators(user_name=username) return {} # Return an empty dictionary if no indicators are found
indicators_df = self.indicators.query('creator == @user_id')
# Create the dictionary
result = {} result = {}
# Iterate over the rows and construct the result dictionary
for _, row in indicators_df.iterrows(): for _, row in indicators_df.iterrows():
# Include all properties from the properties dictionary, not just a limited subset. # Ensure that row['properties'] is a dictionary
properties = row.get('properties', {})
if not isinstance(properties, dict):
properties = {}
# Construct the result dictionary for each indicator
result[row['name']] = { result[row['name']] = {
'type': row['kind'], 'type': row['kind'],
'visible': row['visible'], 'visible': row['visible'],
**row['properties'] # This will include all properties in the dictionary **properties # Merge in all properties from the properties field
} }
return result return result
@ -374,21 +400,21 @@ class Indicators:
:param indicator_names: List of indicator names to set as visible. :param indicator_names: List of indicator names to set as visible.
:return: None :return: None
""" """
indicators = self.cache_manager.get_rows_from_datacache('indicators', [('creator', user_id)])
# Validate inputs # Validate inputs
if user_id not in self.indicators['creator'].unique(): if indicators.empty:
# raise ValueError(f"Invalid user_name: {user_name}")
# Nothing may be loaded.
return return
# Set visibility for all indicators of the user
self.indicators.loc[self.indicators['creator'] == user_id, 'visible'] = 0
# Set visibility for the specified indicator names # Set visibility for all indicators off
self.indicators.loc[self.indicators['name'].isin(indicator_names), 'visible'] = 1 self.cache_manager.modify_datacache_item('indicators', [('creator', user_id)], field_name='visible', new_data=0)
# Set visibility for the specified indicators on
self.cache_manager.modify_datacache_item('indicators', [('creator', user_id), ('name', indicator_names)],
field_name='visible', new_data=1)
def edit_indicator(self, user_name: str, params: dict): def edit_indicator(self, user_name: str, params: dict):
""" """
Edits an existing indicator's properties. Edits an existing indicator's properties.
:param user_name: The name of the user. :param user_name: The name of the user.
:param params: The updated properties of the indicator. :param params: The updated properties of the indicator.
""" """
@ -398,33 +424,15 @@ class Indicators:
# Get the indicator from the user's indicator list # Get the indicator from the user's indicator list
user_id = self.users.get_id(user_name) user_id = self.users.get_id(user_name)
indicator_row = self.indicators.query('name == @indicator_name and creator == @user_id') indicator = self.cache_manager.get_rows_from_datacache('indicators', [('name', indicator_name), ('creator', user_id)])
if indicator_row.empty: if indicator.empty:
raise ValueError(f"Indicator '{indicator_name}' not found for user '{user_name}'.") raise ValueError(f"Indicator '{indicator_name}' not found for user '{user_name}'.")
# Update the top-level fields # Modify indicator.
top_level_keys = ['name', 'visible', 'kind'] # Top-level keys, expand this if needed self.cache_manager.modify_datacache_item('indicators',
for key, value in params.items(): [('creator', params.get('user_name')), ('name', params.get('name'))],
if key in top_level_keys and key in indicator_row.columns: field_name=params.get('setting'), new_data=params.get('value'))
self.indicators.at[indicator_row.index[0], key] = value
# Update 'source' dictionary fields
if 'source' in indicator_row.columns and isinstance(indicator_row['source'].iloc[0], dict):
source_dict = indicator_row['source'].iloc[0] # Direct reference, no need for reassignment later
for key, value in params.items():
if key in source_dict:
source_dict[key] = value
# Update 'properties' dictionary fields
if 'properties' in indicator_row.columns and isinstance(indicator_row['properties'].iloc[0], dict):
properties_dict = indicator_row['properties'].iloc[0] # No copy, modify directly
for key, value in params.items():
if key in properties_dict:
properties_dict[key] = value
# Save the updated indicator for the user in the database.
self.users.save_indicators(indicator_row)
def new_indicator(self, user_name: str, params) -> None: def new_indicator(self, user_name: str, params) -> None:
""" """
@ -457,14 +465,12 @@ class Indicators:
# Create indicator. # Create indicator.
self.create_indicator(creator=user_name, name=indcr, kind=indtyp, source=source, properties=properties) self.create_indicator(creator=user_name, name=indcr, kind=indtyp, source=source, properties=properties)
# Update the watch-list in config.
self.save_indicator(self.indicators.loc[self.indicators.name == indcr])
def process_indicator(self, indicator, num_results: int = 1) -> pd.DataFrame | None: def process_indicator(self, indicator, num_results: int = 1) -> pd.DataFrame | None:
""" """
Trigger execution of the indicator's analysis against an updated source. Trigger execution of the indicator's analysis against an updated source.
:param indicator: A named tuple containing indicator data. :param indicator: A named tuple or dict containing indicator data.
:param num_results: The number of results being requested. :param num_results: The number of results being requested.
:return: The results of the indicator analysis as a DataFrame. :return: The results of the indicator analysis as a DataFrame.
""" """
@ -472,17 +478,26 @@ class Indicators:
src = indicator.source src = indicator.source
symbol, timeframe, exchange_name = src['symbol'], src['timeframe'], src['exchange_name'] symbol, timeframe, exchange_name = src['symbol'], src['timeframe'], src['exchange_name']
# Retrieve necessary details to instantiate the indicator
name = indicator.name
kind = indicator.kind
properties = json.loads(indicator.properties)
# Adjust num_results to account for the lookup period if specified in the indicator properties. # Adjust num_results to account for the lookup period if specified in the indicator properties.
if 'period' in indicator.ref.properties: if 'period' in properties:
num_results += indicator.ref.properties['period'] num_results += properties['period']
# Request the data from the defined source. # Request the data from the defined source.
data = self.candles.get_last_n_candles(num_candles=num_results, data = self.candles.get_last_n_candles(num_candles=num_results,
asset=symbol, timeframe=timeframe, asset=symbol, timeframe=timeframe,
exchange=exchange_name, user_name=username) exchange=exchange_name, user_name=username)
# Calculate the indicator using the retrieved data. # Instantiate the indicator object based on the kind
return indicator.ref.calculate(candles=data, user_name=username, num_results=num_results) indicator_class = self.indicator_registry[kind]
indicator_obj = indicator_class(name=name, indicator_type=kind, properties=properties)
# Run the calculate method of the indicator
return indicator_obj.calculate(candles=data, user_name=username, num_results=num_results)
def get_indicator_data(self, user_name: str, source: dict = None, def get_indicator_data(self, user_name: str, source: dict = None,
visible_only: bool = True, start_ts: float = None, visible_only: bool = True, start_ts: float = None,
@ -500,43 +515,43 @@ class Indicators:
or None if no indicators matched the query. or None if no indicators matched the query.
""" """
if start_ts: if start_ts:
print("Warning: start_ts has not implemented in get_indicator_data()!") print("Warning: start_ts has not been implemented in get_indicator_data()!")
user_id = self.users.get_id(user_name=user_name) user_id = self.users.get_id(user_name=user_name)
# Construct the query based on user_name and visibility. visible = 1 if visible_only else 0
query = f"creator == {user_id}"
if visible_only:
query += " and visible == 1"
# Filter the indicators based on the query. # Filter the indicators based on the query.
indicators = self.indicators.loc[ indicators = self.cache_manager.get_rows_from_datacache('indicators', [('creator', user_id), ('visible', visible)])
(self.indicators['creator'] == user_id) & (self.indicators['visible'] == 1)]
# Return None if no indicators matched the query. # Return None if no indicators matched the query.
if indicators.empty:
# Attempt to re-load from db
self.load_indicators(user_name=user_name)
# query again.
indicators = self.indicators.loc[
(self.indicators['creator'] == user_id) & (self.indicators['visible'] == 1)]
if indicators.empty: if indicators.empty:
return None return None
if source: if source:
# Filter indicators by these source parameters. # Convert 'source' column to dictionaries if they are strings
if 'market' in source: indicators['source'] = indicators['source'].apply(lambda x: json.loads(x) if isinstance(x, str) else x)
symbol = source['market']['market']
timeframe = source['market']['timeframe'] # Extract relevant fields from the source's market
exchange = source['market']['exchange'] source_timeframe = source.get('market', {}).get('timeframe')
indicators = indicators[indicators.source.apply(lambda x: x['symbol'] == symbol and source_exchange = source.get('market', {}).get('exchange')
x['timeframe'] == timeframe and source_symbol = source.get('market', {}).get('market')
x['exchange_name'] == exchange)]
else: # Extract fields from indicators['source'] and compare directly
raise ValueError(f'No implementation for source: {source}') mask = (indicators['source'].apply(lambda s: s.get('timeframe')) == source_timeframe) & \
(indicators['source'].apply(lambda s: s.get('exchange_name')) == source_exchange) & \
(indicators['source'].apply(lambda s: s.get('symbol')) == source_symbol)
# Filter the DataFrame using the mask
filtered_indicators = indicators[mask]
# If no indicators match the filtered source, return None.
if indicators.empty:
return None
# Process each indicator, convert DataFrame to JSON-serializable format, and collect the results # Process each indicator, convert DataFrame to JSON-serializable format, and collect the results
json_ready_results = {} json_ready_results = {}
for indicator in indicators.itertuples(index=False): for indicator in indicators.itertuples(index=False):
indicator_results = self.process_indicator(indicator=indicator, num_results=num_results) indicator_results = self.process_indicator(indicator=indicator, num_results=num_results)
@ -561,12 +576,8 @@ class Indicators:
# Get the user ID to filter the indicators belonging to the user # Get the user ID to filter the indicators belonging to the user
user_id = self.users.get_id(user_name) user_id = self.users.get_id(user_name)
# Remove the indicator from the DataFrame where the name matches and the creator is the user identifying_values = [('name', indicator_name), ('creator', user_id)]
self.indicators = self.indicators[ self.cache_manager.remove_row_from_datacache(cache_name='indicators', filter_vals=identifying_values)
~((self.indicators['name'] == indicator_name) & (self.indicators['creator'] == user_id))
]
self.users.remove_indicator(indicator_name=indicator_name, user_name=user_name)
def create_indicator(self, creator: str, name: str, kind: str, def create_indicator(self, creator: str, name: str, kind: str,
source: dict, properties: dict, visible: bool = True): source: dict, properties: dict, visible: bool = True):
@ -583,36 +594,29 @@ class Indicators:
:param visible: Whether to display it in the chart view. :param visible: Whether to display it in the chart view.
:return: None :return: None
""" """
# Todo: Possible refactor to save without storing the indicator instance
self.indicators = self.indicators.reset_index(drop=True)
creator_id = self.users.get_id(creator) creator_id = self.users.get_id(creator)
# Check if an indicator with the same name already exists # Check if an indicator with the same name already exists
existing_indicator = self.indicators.query('name == @name and creator == @creator_id') indicators = self.cache_manager.get_rows_from_datacache('indicators', [('name', name), ('creator', creator_id)])
if not existing_indicator.empty: if not indicators.empty:
print(f"Indicator '{name}' already exists for user '{creator}'. Skipping creation.") print(f"Indicator '{name}' already exists for user '{creator}'. Skipping creation.")
return # Exit the method to prevent duplicate creation return # Exit the method to prevent duplicate creation
if kind not in self.indicator_registry: if kind not in self.indicator_registry:
raise ValueError(f"Requested an unsupported type of indicator: ({kind})") raise ValueError(f"Requested an unsupported type of indicator: ({kind})")
indicator_class = self.indicator_registry[kind]
# Create an instance of the indicator.
indicator = indicator_class(name, kind, properties)
# Add the new indicator to a pandas dataframe. # Add the new indicator to a pandas dataframe.
creator_id = self.users.get_id(creator) creator_id = self.users.get_id(creator)
row_data = { row_data = pd.DataFrame([{
'creator': creator_id, 'creator': creator_id,
'name': name, 'name': name,
'kind': kind, 'kind': kind,
'visible': visible, 'visible': visible,
'source': source, 'source': source,
'properties': properties, 'properties': properties
'ref': indicator }])
} self.cache_manager.insert_df_into_datacache(df=row_data, cache_name="users", skip_cache=False)
self.indicators = pd.concat([self.indicators, pd.DataFrame([row_data])], ignore_index=True)
# def update_indicators(self, user_name): # def update_indicators(self, user_name):
# """ # """

File diff suppressed because it is too large Load Diff