Fix chart loading and stabilize local runtime

This commit is contained in:
rob 2026-02-28 15:15:56 -04:00
parent ee51ab1d8c
commit f1a184b131
13 changed files with 136 additions and 26 deletions

24
.claudeignore Normal file
View File

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

12
.gitignore vendored
View File

@ -14,10 +14,12 @@ data/*.db
# Ignore configuration files # Ignore configuration files
config/ config/
config.py config.py
src/config.py
config.yml config.yml
# Ignore virtual environments # Ignore virtual environments
venv/ venv/
.venv/
ENV/ ENV/
env/ env/
@ -32,3 +34,13 @@ __pycache__/
# Ignore system files # Ignore system files
.DS_Store .DS_Store
Thumbs.db 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/

View File

@ -1471,7 +1471,16 @@ class ServerInteractions(DatabaseInteractions):
raise ValueError('Not a valid Target!') raise ValueError('Not a valid Target!')
for fetch_method in resources: for fetch_method in resources:
try:
result = fetch_method(**kwargs) 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: if result is not None and not result.empty:
# Drop the 'id' column if it exists in the result # Drop the 'id' column if it exists in the result

View File

@ -107,7 +107,7 @@ class Database:
:param table_name: The name of the table. :param table_name: The name of the table.
:return: DataFrame containing all rows. :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: with SQLite(self.db_file) as conn:
return pd.read_sql(sql_query, conn) return pd.read_sql(sql_query, conn)

View File

@ -57,15 +57,20 @@ class Exchange:
'apiKey': self.api_key, 'apiKey': self.api_key,
'secret': self.api_key_secret, 'secret': self.api_key_secret,
'enableRateLimit': True, 'enableRateLimit': True,
'verbose': False 'verbose': False,
'options': {'warnOnFetchOpenOrdersWithoutSymbol': False}
}) })
else: else:
return exchange_class({ return exchange_class({
'enableRateLimit': True, 'enableRateLimit': True,
'verbose': False 'verbose': False,
'options': {'warnOnFetchOpenOrdersWithoutSymbol': False}
}) })
def _check_authentication(self): def _check_authentication(self):
if not (self.api_key and self.api_key_secret):
self.configured = False
return
try: try:
self.client.fetch_open_orders() # Much faster than fetch_balance self.client.fetch_open_orders() # Much faster than fetch_balance
self.configured = True self.configured = True

View File

@ -1,7 +1,9 @@
import logging import logging
import sqlite3
from typing import List, Any, Dict, TYPE_CHECKING from typing import List, Any, Dict, TYPE_CHECKING
import pandas as pd import pandas as pd
import ccxt import ccxt
import config
from Exchange import Exchange from Exchange import Exchange
from DataCache_v3 import DataCache from DataCache_v3 import DataCache
@ -27,12 +29,47 @@ class ExchangeInterface:
eviction_policy='deny', eviction_policy='deny',
columns=['user', 'name', 'reference', 'balances'] columns=['user', 'name', 'reference', 'balances']
) )
self.exchange_data_db_columns = self._ensure_exchange_data_schema()
self.available_exchanges = self.get_ccxt_exchanges() self.available_exchanges = self.get_ccxt_exchanges()
self.default_ex_name = 'binance' self.default_ex_name = 'binance'
self.default_exchange = None 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): def connect_default_exchange(self):
if self.default_exchange is not None: if self.default_exchange is not None:
return return
@ -86,9 +123,18 @@ class ExchangeInterface:
:param exchange: The Exchange object to add. :param exchange: The Exchange object to add.
""" """
try: try:
row = pd.DataFrame([{ row_data = {
'user': user_name, 'name': exchange.name, 'user': user_name,
'reference': exchange, 'balances': exchange.balances}]) '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) self.cache_manager.serialized_datacache_insert(cache_name='exchange_data', data=row)
except Exception as e: except Exception as e:

View File

@ -2,7 +2,7 @@ import copy
import datetime as dt import datetime as dt
import json import json
import random import random
from passlib.hash import bcrypt from passlib.hash import bcrypt, pbkdf2_sha256
import pandas as pd import pandas as pd
from typing import Any from typing import Any
from DataCache_v3 import DataCache from DataCache_v3 import DataCache
@ -174,11 +174,15 @@ class UserAccountManagement(BaseUser):
if not hashed_pass: if not hashed_pass:
return False 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: try:
return bcrypt.verify(password, hashed_pass) return bcrypt.verify(password, hashed_pass)
except ValueError: except ValueError:
# Handle any issues with the verification process try:
return pbkdf2_sha256.verify(password, hashed_pass)
except ValueError:
return False return False
def log_in_user(self, username: str, password: str) -> bool: def log_in_user(self, username: str, password: str) -> bool:

View File

@ -6,6 +6,7 @@ eventlet.monkey_patch() # noqa: E402
# Standard library imports # Standard library imports
import logging # noqa: E402 import logging # noqa: E402
import os # noqa: E402
# import json # noqa: E402 # import json # noqa: E402
# import datetime as dt # 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 from BrighterTrades import BrighterTrades # noqa: E402
# Set up logging # 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. # Create a Flask object named app that serves the html.
app = Flask(__name__) app = Flask(__name__)
@ -68,7 +73,7 @@ def index():
raise raise
else: else:
flash('The system is too busy for another user.') flash('The system is too busy for another user.')
return return redirect('/signup')
# Ensure client session is assigned a user_name. # Ensure client session is assigned a user_name.
session['user'] = user_name session['user'] = user_name
@ -341,5 +346,4 @@ def indicator_init():
if __name__ == '__main__': 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)

View File

@ -57,6 +57,16 @@ class Candles:
# Fetch records older than start_datetime. # Fetch records older than start_datetime.
candles = self.data.get_records_since(start_datetime=start_datetime, candles = self.data.get_records_since(start_datetime=start_datetime,
ex_details=[asset, timeframe, exchange, user_name]) 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: if len(candles.index) < num_candles:
timesince = dt.datetime.now(pytz.UTC) - start_datetime timesince = dt.datetime.now(pytz.UTC) - start_datetime
minutes_since = int(timesince.total_seconds() / 60) minutes_since = int(timesince.total_seconds() / 60)

View File

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

View File

@ -19,6 +19,7 @@ class Comms {
// Save the userName // Save the userName
this.userName = userName; this.userName = userName;
this.baseUrl = window.location.origin;
// Initialize the socket // Initialize the socket
this._initializeSocket(); this._initializeSocket();
@ -29,7 +30,7 @@ class Comms {
*/ */
_initializeSocket() { _initializeSocket() {
// Initialize Socket.IO client with query parameter // 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 }, query: { 'user_name': this.userName },
transports: ['websocket'], // Optional: Force WebSocket transport transports: ['websocket'], // Optional: Force WebSocket transport
autoConnect: true, autoConnect: true,
@ -164,7 +165,7 @@ class Comms {
*/ */
async getPriceHistory(userName) { async getPriceHistory(userName) {
try { try {
const response = await fetch('http://127.0.0.1:5000/api/history', { const response = await fetch(`${this.baseUrl}/api/history`, {
credentials: 'include', credentials: 'include',
mode: 'cors', mode: 'cors',
method: 'POST', method: 'POST',
@ -194,7 +195,7 @@ class Comms {
*/ */
async getIndicatorData(userName) { async getIndicatorData(userName) {
try { 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', credentials: 'same-origin',
mode: 'cors', mode: 'cors',
method: 'POST', method: 'POST',

View File

@ -12,7 +12,7 @@ class Data {
// The assets being traded. // The assets being traded.
this.trading_pair = bt_data.trading_pair; this.trading_pair = bt_data.trading_pair;
// The time period of the charts. // The time period of the charts.
this.interval = bt_data.interval; this.interval = bt_data.timeframe || bt_data.interval || '5m';
// All the indicators available. // All the indicators available.
this.indicators = bt_data.indicators; this.indicators = bt_data.indicators;

View File

@ -22,7 +22,7 @@
<script src="{{ url_for('static', filename='exchanges.js') }}"></script> <script src="{{ url_for('static', filename='exchanges.js') }}"></script>
<script src="{{ url_for('static', filename='user.js') }}"></script> <script src="{{ url_for('static', filename='user.js') }}"></script>
<script src="{{ url_for('static', filename='Alerts.js') }}"></script> <script src="{{ url_for('static', filename='Alerts.js') }}"></script>
<script src="{{ url_for('static', filename='strategies.js') }}"></script> <script src="{{ url_for('static', filename='Strategies.js') }}"></script>
<script src="{{ url_for('static', filename='data.js') }}"></script> <script src="{{ url_for('static', filename='data.js') }}"></script>
<script src="{{ url_for('static', filename='indicators.js') }}"></script> <script src="{{ url_for('static', filename='indicators.js') }}"></script>
<script src="{{ url_for('static', filename='charts.js') }}"></script> <script src="{{ url_for('static', filename='charts.js') }}"></script>