feat: instrument analysis
This commit is contained in:
@@ -20,8 +20,11 @@ chat context always has fresh slope/energy/ridge state, not just firing
|
|||||||
events (see ai_chat_context.py:_block_wavelet_signals).
|
events (see ai_chat_context.py:_block_wavelet_signals).
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _compute_slope(series: List[float]) -> List[float]:
|
def _compute_slope(series: List[float]) -> List[float]:
|
||||||
n = len(series)
|
n = len(series)
|
||||||
@@ -249,11 +252,18 @@ def _fetch_close_series(ticker: str, saxo_symbol: Optional[str]):
|
|||||||
bars = get_price_history(saxo_symbol, asset_type, days=400)
|
bars = get_price_history(saxo_symbol, asset_type, days=400)
|
||||||
return [b["close"] for b in bars], [b["date"] for b in bars]
|
return [b["close"] for b in bars], [b["date"] for b in bars]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import logging
|
logger.warning(f"[wavelet_signals] Saxo fetch failed for '{saxo_symbol}', falling back to yfinance: {e}")
|
||||||
logging.getLogger(__name__).warning(f"[wavelet_signals] Saxo fetch failed for '{saxo_symbol}', falling back to yfinance: {e}")
|
|
||||||
|
|
||||||
from services.data_fetcher import get_historical
|
from services.data_fetcher import get_historical
|
||||||
hist = get_historical(ticker, period="1y", interval="1d")
|
yf_ticker = ticker.upper()
|
||||||
|
# Bare 6-letter FX pairs (EURUSD, GBPUSD...) are a common Watchlist ticker convention
|
||||||
|
# here but not a real yfinance symbol (needs the "=X" suffix) — without this, any
|
||||||
|
# Saxo-linked FX pair whose Saxo fetch fails falls through to a yfinance call that's
|
||||||
|
# guaranteed to return nothing, permanently keeping it out of the wavelet cache no
|
||||||
|
# matter how many refreshes run.
|
||||||
|
if len(yf_ticker) == 6 and yf_ticker.isalpha():
|
||||||
|
yf_ticker += "=X"
|
||||||
|
hist = get_historical(yf_ticker, period="1y", interval="1d")
|
||||||
return [h["close"] for h in hist], [h["date"] for h in hist]
|
return [h["close"] for h in hist], [h["date"] for h in hist]
|
||||||
|
|
||||||
|
|
||||||
@@ -299,6 +309,10 @@ def scan_watchlist_wavelet_signals(run_id: Optional[str] = None) -> List[Dict]:
|
|||||||
try:
|
try:
|
||||||
values, dates = _fetch_close_series(ticker, item.get("saxo_quote_symbol"))
|
values, dates = _fetch_close_series(ticker, item.get("saxo_quote_symbol"))
|
||||||
if len(values) < lookback + 32:
|
if len(values) < lookback + 32:
|
||||||
|
logger.warning(
|
||||||
|
f"[wavelet_signals] Skipping '{ticker}': only {len(values)} price points "
|
||||||
|
f"fetched, need >= {lookback + 32} (lookback={lookback}) — no cache written this pass."
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
start_idx = max(lookback, len(values) - 60)
|
start_idx = max(lookback, len(values) - 60)
|
||||||
decomposed = decomposer(
|
decomposed = decomposer(
|
||||||
@@ -307,6 +321,7 @@ def scan_watchlist_wavelet_signals(run_id: Optional[str] = None) -> List[Dict]:
|
|||||||
num_levels=num_levels, wavelet=wavelet, step=1,
|
num_levels=num_levels, wavelet=wavelet, step=1,
|
||||||
)
|
)
|
||||||
if not decomposed["dates"]:
|
if not decomposed["dates"]:
|
||||||
|
logger.warning(f"[wavelet_signals] Skipping '{ticker}': decomposition produced no output dates — no cache written this pass.")
|
||||||
continue
|
continue
|
||||||
save_wavelet_decomposition_cache(ticker, run_id, method, wavelet, num_levels, lookback, decomposed)
|
save_wavelet_decomposition_cache(ticker, run_id, method, wavelet, num_levels, lookback, decomposed)
|
||||||
price_at_signal = decomposed["original"][-1]
|
price_at_signal = decomposed["original"][-1]
|
||||||
@@ -399,8 +414,9 @@ def scan_watchlist_wavelet_signals(run_id: Optional[str] = None) -> List[Dict]:
|
|||||||
"ridge_period_days": ridge_series[-1],
|
"ridge_period_days": ridge_series[-1],
|
||||||
"params_json": json.dumps(ridge_params) if ridge_params else None,
|
"params_json": json.dumps(ridge_params) if ridge_params else None,
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception as e:
|
||||||
continue # one bad ticker must not abort the whole scan
|
logger.warning(f"[wavelet_signals] Skipping '{ticker}': {e}") # one bad ticker must not abort the whole scan
|
||||||
|
continue
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|||||||
@@ -140,8 +140,15 @@ function NormalRoutes() {
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
if (KEEP_ALIVE_PATHS.has(location.pathname)) return null
|
if (KEEP_ALIVE_PATHS.has(location.pathname)) return null
|
||||||
|
|
||||||
// On instrument paths the keep-alive layer renders the UI; this Routes just registers the tab
|
// On instrument paths the keep-alive layer renders the UI; this Routes just registers the tab.
|
||||||
const isInstrument = /^\/instruments\/\w/.test(location.pathname)
|
// Was `/^\/instruments\/\w/` (required a word char right after the slash) — encoded ids
|
||||||
|
// that start with a character encodeURIComponent escapes (^GSPC -> %5EGSPC) begin with
|
||||||
|
// "%", which isn't \w, so this evaluated false for that tab. isInstrument then picked
|
||||||
|
// 'flex-1 overflow-auto' instead of 'hidden' for this otherwise-empty div, and — since
|
||||||
|
// InstrumentKeepAlive's own div is ALSO flex-1 in the same flex column — the two split
|
||||||
|
// the available height roughly 50/50 instead of the instrument page getting all of it,
|
||||||
|
// which is exactly the "page only fills half the screen" symptom.
|
||||||
|
const isInstrument = location.pathname.startsWith('/instruments/')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={isInstrument ? 'hidden' : 'flex-1 overflow-auto'}>
|
<div className={isInstrument ? 'hidden' : 'flex-1 overflow-auto'}>
|
||||||
|
|||||||
@@ -69,7 +69,13 @@ export default function TabBar() {
|
|||||||
{/* Dynamic instrument tabs */}
|
{/* Dynamic instrument tabs */}
|
||||||
{instrumentIds.map(id => {
|
{instrumentIds.map(id => {
|
||||||
const path = `/instruments/${id}`
|
const path = `/instruments/${id}`
|
||||||
const isActive = activePath.toLowerCase() === path.toLowerCase()
|
// activePath keeps its percent-encoding (EURUSD=X -> /instruments/EURUSD%3DX)
|
||||||
|
// while `id` is already decoded — same mismatch fixed in App.tsx's
|
||||||
|
// InstrumentKeepAlive, needed again here or this tab never lights up as active
|
||||||
|
// for any id containing a character encodeURIComponent escapes ("=").
|
||||||
|
let decodedActivePath = activePath
|
||||||
|
try { decodedActivePath = decodeURIComponent(activePath) } catch { /* malformed sequence, compare as-is */ }
|
||||||
|
const isActive = decodedActivePath.toLowerCase() === path.toLowerCase()
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={id}
|
key={id}
|
||||||
|
|||||||
Reference in New Issue
Block a user