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 @@