diff --git a/.claudeignore b/.claudeignore new file mode 100644 index 0000000..49eb2f2 --- /dev/null +++ b/.claudeignore @@ -0,0 +1,24 @@ +# Keep Claude Code focused on project source and tests. +# Exclude large local artifacts that can cause indexing slowdowns/freezes. + +.git/ +.idea/ +.pytest_cache/ +__pycache__/ +flask_session/ +venv/ +.venv/ + +# Local project data and databases +data/ +BrighterTrading.db + +# Symlink to external docs repo (outside this project) +docs/ +docs + +# Local Claude settings +.claude/ + +# Large vendored Blockly source checkout +src/static/blockly/ diff --git a/.gitignore b/.gitignore index aa480d3..9fb1953 100644 --- a/.gitignore +++ b/.gitignore @@ -14,10 +14,12 @@ data/*.db # Ignore configuration files config/ config.py +src/config.py config.yml # Ignore virtual environments venv/ +.venv/ ENV/ env/ @@ -32,3 +34,13 @@ __pycache__/ # Ignore system files .DS_Store Thumbs.db + +# Ignore local AI/tooling settings +.claude/ + +# Ignore local symlinked docs from centralized docs repo +docs/ +docs + +# Ignore local Blockly vendor checkout (large source + node_modules) +src/static/blockly/ diff --git a/src/DataCache_v3.py b/src/DataCache_v3.py index 32041d8..82f8d9b 100644 --- a/src/DataCache_v3.py +++ b/src/DataCache_v3.py @@ -1471,7 +1471,16 @@ class ServerInteractions(DatabaseInteractions): raise ValueError('Not a valid Target!') for fetch_method in resources: - result = fetch_method(**kwargs) + try: + result = fetch_method(**kwargs) + except Exception as e: + logger.error(f"Fetch step '{fetch_method.__name__}' failed: {e}") + if not combined_data.empty: + logger.warning( + "Returning partial candle history from cache/database because a downstream fetch failed." + ) + return combined_data.reset_index(drop=True) + continue if result is not None and not result.empty: # Drop the 'id' column if it exists in the result diff --git a/src/Database.py b/src/Database.py index e1198b3..911bf84 100644 --- a/src/Database.py +++ b/src/Database.py @@ -107,7 +107,7 @@ class Database: :param table_name: The name of the table. :return: DataFrame containing all rows. """ - sql_query = f"SELECT * FROM {table_name}" + sql_query = f'SELECT * FROM "{table_name}"' with SQLite(self.db_file) as conn: return pd.read_sql(sql_query, conn) diff --git a/src/Exchange.py b/src/Exchange.py index 702ce9b..bbd2471 100644 --- a/src/Exchange.py +++ b/src/Exchange.py @@ -57,15 +57,20 @@ class Exchange: 'apiKey': self.api_key, 'secret': self.api_key_secret, 'enableRateLimit': True, - 'verbose': False + 'verbose': False, + 'options': {'warnOnFetchOpenOrdersWithoutSymbol': False} }) else: return exchange_class({ 'enableRateLimit': True, - 'verbose': False + 'verbose': False, + 'options': {'warnOnFetchOpenOrdersWithoutSymbol': False} }) def _check_authentication(self): + if not (self.api_key and self.api_key_secret): + self.configured = False + return try: self.client.fetch_open_orders() # Much faster than fetch_balance self.configured = True diff --git a/src/ExchangeInterface.py b/src/ExchangeInterface.py index 8e88ef0..ccc0e40 100644 --- a/src/ExchangeInterface.py +++ b/src/ExchangeInterface.py @@ -1,7 +1,9 @@ import logging +import sqlite3 from typing import List, Any, Dict, TYPE_CHECKING import pandas as pd import ccxt +import config from Exchange import Exchange from DataCache_v3 import DataCache @@ -27,12 +29,47 @@ class ExchangeInterface: eviction_policy='deny', columns=['user', 'name', 'reference', 'balances'] ) + self.exchange_data_db_columns = self._ensure_exchange_data_schema() self.available_exchanges = self.get_ccxt_exchanges() self.default_ex_name = 'binance' self.default_exchange = None + @staticmethod + def _ensure_exchange_data_schema() -> set[str]: + """ + Ensure the on-disk exchange_data table has columns required by current code. + This handles older migrated databases that stored `saved_state` instead of + `reference`. + """ + try: + with sqlite3.connect(config.DB_FILE) as conn: + cur = conn.cursor() + cur.execute("PRAGMA table_info(exchange_data)") + table_info = cur.fetchall() + if not table_info: + return set() + + cols = {row[1] for row in table_info} + + if 'reference' not in cols: + cur.execute("ALTER TABLE exchange_data ADD COLUMN reference BLOB") + if 'saved_state' in cols: + cur.execute( + "UPDATE exchange_data SET reference = saved_state WHERE reference IS NULL" + ) + + if 'balances' not in cols: + cur.execute("ALTER TABLE exchange_data ADD COLUMN balances BLOB") + + conn.commit() + cur.execute("PRAGMA table_info(exchange_data)") + return {row[1] for row in cur.fetchall()} + except Exception as e: + logger.warning("exchange_data schema check skipped: %s", e) + return set() + def connect_default_exchange(self): if self.default_exchange is not None: return @@ -86,9 +123,18 @@ class ExchangeInterface: :param exchange: The Exchange object to add. """ try: - row = pd.DataFrame([{ - 'user': user_name, 'name': exchange.name, - 'reference': exchange, 'balances': exchange.balances}]) + row_data = { + 'user': user_name, + 'name': exchange.name, + 'reference': exchange, + 'balances': exchange.balances + } + # Backwards compatibility for migrated databases that still enforce + # a NOT NULL legacy `saved_state` column. + if 'saved_state' in self.exchange_data_db_columns: + row_data['saved_state'] = exchange + + row = pd.DataFrame([row_data]) self.cache_manager.serialized_datacache_insert(cache_name='exchange_data', data=row) except Exception as e: diff --git a/src/Users.py b/src/Users.py index 34b9213..a1a5ea5 100644 --- a/src/Users.py +++ b/src/Users.py @@ -2,7 +2,7 @@ import copy import datetime as dt import json import random -from passlib.hash import bcrypt +from passlib.hash import bcrypt, pbkdf2_sha256 import pandas as pd from typing import Any from DataCache_v3 import DataCache @@ -174,12 +174,16 @@ class UserAccountManagement(BaseUser): if not hashed_pass: return False - # Verify the password using bcrypt + # Verify the password using the legacy or current hash format. + # Some existing records (e.g., default guest) use pbkdf2_sha256 while + # newer accounts are created with bcrypt. try: return bcrypt.verify(password, hashed_pass) except ValueError: - # Handle any issues with the verification process - return False + try: + return pbkdf2_sha256.verify(password, hashed_pass) + except ValueError: + return False def log_in_user(self, username: str, password: str) -> bool: """ diff --git a/src/app.py b/src/app.py index 7ca628d..fef5f12 100644 --- a/src/app.py +++ b/src/app.py @@ -6,6 +6,7 @@ eventlet.monkey_patch() # noqa: E402 # Standard library imports import logging # noqa: E402 +import os # noqa: E402 # import json # noqa: E402 # import datetime as dt # noqa: E402 @@ -19,7 +20,11 @@ from email_validator import validate_email, EmailNotValidError # noqa: E402 from BrighterTrades import BrighterTrades # noqa: E402 # Set up logging -logging.basicConfig(level=logging.DEBUG) +log_level_name = os.getenv('BRIGHTER_LOG_LEVEL', 'INFO').upper() +log_level = getattr(logging, log_level_name, logging.INFO) +logging.basicConfig(level=log_level) +logging.getLogger('ccxt.base.exchange').setLevel(logging.WARNING) +logging.getLogger('urllib3').setLevel(logging.WARNING) # Create a Flask object named app that serves the html. app = Flask(__name__) @@ -68,7 +73,7 @@ def index(): raise else: flash('The system is too busy for another user.') - return + return redirect('/signup') # Ensure client session is assigned a user_name. session['user'] = user_name @@ -341,5 +346,4 @@ def indicator_init(): if __name__ == '__main__': - socketio.run(app, host='127.0.0.1', port=5000, debug=False, use_reloader=False) - + socketio.run(app, host='127.0.0.1', port=5002, debug=False, use_reloader=False) diff --git a/src/candles.py b/src/candles.py index f9bfe5b..3d2428d 100644 --- a/src/candles.py +++ b/src/candles.py @@ -57,6 +57,16 @@ class Candles: # Fetch records older than start_datetime. candles = self.data.get_records_since(start_datetime=start_datetime, ex_details=[asset, timeframe, exchange, user_name]) + + # If there are no recent records, fall back to the latest local candles in the market table. + if candles.empty: + try: + table_name = self.data._make_key([asset, timeframe, exchange, user_name]) + if self.data.db.table_exists(table_name): + candles = self.data.db.get_all_rows(table_name).sort_values(by='time').reset_index(drop=True) + except Exception as e: + print(f"candles[64]: Failed local fallback load for {asset} {timeframe} {exchange}: {e}") + if len(candles.index) < num_candles: timesince = dt.datetime.now(pytz.UTC) - start_datetime minutes_since = int(timesince.total_seconds() / 60) diff --git a/src/config.py b/src/config.py deleted file mode 100644 index 96a399c..0000000 --- a/src/config.py +++ /dev/null @@ -1,5 +0,0 @@ -BINANCE_API_KEY = 'rkp1Xflb5nnwt6jys0PG27KXcqwn0q9lKCLryKcSp4mKW2UOlkPRuAHPg45rQVgj' -BINANCE_API_SECRET = 'DiFhhYhF64nkPe5f3V7TRJX2bSVA7ZQZlozSdX7O7uYmBMdK985eA6Kp2B2zKvbK' -ALPACA_API_KEY = 'PKE4RD999SJ8L53OUI8O' -ALPACA_API_SECRET = 'buwlMoSSfZWGih8Er30quQt4d7brsBWdJXD1KB7C' -DB_FILE = "C:/Users/Rob/PycharmProjects/BrighterTrading/data/BrighterTrading.db" diff --git a/src/static/communication.js b/src/static/communication.js index e8adaa0..e8f6f94 100644 --- a/src/static/communication.js +++ b/src/static/communication.js @@ -19,6 +19,7 @@ class Comms { // Save the userName this.userName = userName; + this.baseUrl = window.location.origin; // Initialize the socket this._initializeSocket(); @@ -29,7 +30,7 @@ class Comms { */ _initializeSocket() { // Initialize Socket.IO client with query parameter - this.socket = io('http://127.0.0.1:5000', { + this.socket = io(this.baseUrl, { query: { 'user_name': this.userName }, transports: ['websocket'], // Optional: Force WebSocket transport autoConnect: true, @@ -164,7 +165,7 @@ class Comms { */ async getPriceHistory(userName) { try { - const response = await fetch('http://127.0.0.1:5000/api/history', { + const response = await fetch(`${this.baseUrl}/api/history`, { credentials: 'include', mode: 'cors', method: 'POST', @@ -194,7 +195,7 @@ class Comms { */ async getIndicatorData(userName) { try { - const response = await fetch('http://127.0.0.1:5000/api/indicator_init', { // Changed to use same host + const response = await fetch(`${this.baseUrl}/api/indicator_init`, { credentials: 'same-origin', mode: 'cors', method: 'POST', diff --git a/src/static/data.js b/src/static/data.js index 5c19235..a54d342 100644 --- a/src/static/data.js +++ b/src/static/data.js @@ -12,7 +12,7 @@ class Data { // The assets being traded. this.trading_pair = bt_data.trading_pair; // The time period of the charts. - this.interval = bt_data.interval; + this.interval = bt_data.timeframe || bt_data.interval || '5m'; // All the indicators available. this.indicators = bt_data.indicators; diff --git a/src/templates/index.html b/src/templates/index.html index 5bd5532..817799e 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -22,7 +22,7 @@ - + @@ -63,4 +63,4 @@ - \ No newline at end of file +