Fix detail panel: 5 bugs causing all fields to show dashes

- _derive_row_extras -> _prepare_row_dict (undefined function call)
- Define _GREEN_KEYS (NameError crashed _color_for_key loop)
- Fix Q_ARG(object,...) in chart, search, insider trades, hedge funds
- Fix total equity 100x overstatement (de_ratio already a true ratio)
- Launch maximized on primary monitor instead of centered/split

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nolan Kovacs 2026-04-08 19:40:32 -04:00
parent df032593bd
commit d756bc99ce
1 changed files with 41 additions and 22 deletions

63
dist/screener_gui.py vendored
View File

@ -99,6 +99,14 @@ QToolTip {{ background: #1c1c1c; color: {fg}; border: 1px solid #333333; padding
# ---------------------------------------------------------------------------
def _nan(v): return v is None or (isinstance(v, float) and math.isnan(v))
_GREEN_KEYS = {
"ret_1m", "ret_3m", "ret_6m", "ret_12m",
"revenue_growth", "earnings_growth",
"roe", "roa", "fcf_yield", "dividend_yield",
"analyst_upside", "profit_margin", "operating_margin",
"eps_forward", "eps_trailing",
}
def _pct(v): return "" if _nan(v) else f"{v * 100:.1f}%"
def _flt(v, d=2): return "" if _nan(v) else f"{v:.{d}f}"
def _price(v): return "" if _nan(v) else f"${v:,.2f}"
@ -1197,14 +1205,19 @@ class ChartWidget(QWidget):
QMetaObject.invokeMethod(self._placeholder, "setText", Qt.QueuedConnection,
Q_ARG(str, f"No price history found for {ticker}."))
return
QMetaObject.invokeMethod(self, "_render_chart", Qt.QueuedConnection,
Q_ARG(object, hist), Q_ARG(str, ticker))
self._pending_hist = hist
self._pending_ticker = ticker
QMetaObject.invokeMethod(self, "_render_chart", Qt.QueuedConnection)
except Exception as e:
QMetaObject.invokeMethod(self._placeholder, "setText", Qt.QueuedConnection,
Q_ARG(str, f"Chart error: {e}"))
@Slot(object, str)
def _render_chart(self, hist, ticker):
@Slot()
def _render_chart(self):
hist = getattr(self, "_pending_hist", None)
ticker = getattr(self, "_pending_ticker", "")
if hist is None:
return
if not _MATPLOTLIB_OK: return
import pandas as pd
# Clear old canvas
@ -1362,7 +1375,7 @@ def _prepare_row_dict(row):
ns = d.get("news_sentiment")
if not _nan(total_debt) and not _nan(de_ratio) and de_ratio != 0:
d["_equity_fmt"] = _mcap(total_debt / (de_ratio / 100.0))
d["_equity_fmt"] = _mcap(total_debt / de_ratio)
else:
d["_equity_fmt"] = ""
@ -1624,7 +1637,7 @@ class DetailPanel(QWidget):
else:
self._no_data_bar.hide()
d = _derive_row_extras(row)
d = _prepare_row_dict(row)
# Update metric rows
for key, row_w in self._all_rows.items():
@ -2401,8 +2414,8 @@ class SearchPage(QWidget):
if row is not None:
import pandas as pd
if isinstance(row, pd.Series): row = row.to_dict()
QMetaObject.invokeMethod(self, "_show_result", Qt.QueuedConnection,
Q_ARG(object, row), Q_ARG(str, f"Found in screener data: {row['ticker']}"))
self._pending_result = (row, f"Found in screener data: {row['ticker']}")
QMetaObject.invokeMethod(self, "_show_result", Qt.QueuedConnection)
return
QMetaObject.invokeMethod(self._search_status, "setText", Qt.QueuedConnection, Q_ARG(str, "Fetching data…"))
@ -2435,8 +2448,8 @@ class SearchPage(QWidget):
if col in s: live_row[col] = s[col]
except Exception: pass
QMetaObject.invokeMethod(self, "_show_result", Qt.QueuedConnection,
Q_ARG(object, live_row), Q_ARG(str, f"Live data: {live_row['ticker']}"))
self._pending_result = (live_row, f"Live data: {live_row['ticker']}")
QMetaObject.invokeMethod(self, "_show_result", Qt.QueuedConnection)
def _find_in_df(self, query):
if self._df is None or self._df.empty: return None
@ -2523,8 +2536,11 @@ class SearchPage(QWidget):
}
except: return None
@Slot(object, str)
def _show_result(self, row, status_msg):
@Slot()
def _show_result(self):
row, status_msg = getattr(self, "_pending_result", (None, ""))
if row is None:
return
self._last_row = row
self._search_status.setText(status_msg)
self._status2.setText(status_msg)
@ -2644,12 +2660,14 @@ class InsiderTable(QWidget):
def _worker():
try: trades = fetch_insider_trades(days_back=days, progress_cb=_progress)
except: trades = []
QMetaObject.invokeMethod(self, "_on_fetched", Qt.QueuedConnection, Q_ARG(object, trades))
self._pending_trades = trades
QMetaObject.invokeMethod(self, "_on_fetched", Qt.QueuedConnection)
threading.Thread(target=_worker, daemon=True).start()
@Slot(object)
def _on_fetched(self, trades):
@Slot()
def _on_fetched(self):
trades = getattr(self, "_pending_trades", [])
self._loading = False; self._has_loaded = True
self._all_trades = trades
self._refresh_btn.setEnabled(True)
@ -2801,12 +2819,14 @@ class HedgeFundTable(QWidget):
def _worker():
try: holdings = fetch_hedge_fund_filings(days_back=days, progress_cb=_progress)
except: holdings = []
QMetaObject.invokeMethod(self, "_on_fetched", Qt.QueuedConnection, Q_ARG(object, holdings))
self._pending_holdings = holdings
QMetaObject.invokeMethod(self, "_on_fetched", Qt.QueuedConnection)
threading.Thread(target=_worker, daemon=True).start()
@Slot(object)
def _on_fetched(self, holdings):
@Slot()
def _on_fetched(self):
holdings = getattr(self, "_pending_holdings", [])
self._loading = False; self._has_loaded = True
self._all_holdings = holdings; self._refresh_btn.setEnabled(True)
self._apply_filter()
@ -2958,12 +2978,11 @@ def main():
app = QApplication(sys.argv)
app.setStyleSheet(_QSS)
win = ScreenerApp()
win.show()
# Force window onto primary screen
# Move to primary screen before maximizing so it doesn't span to secondary
primary = app.primaryScreen()
geo = primary.availableGeometry()
win.move(geo.x() + (geo.width() - win.width()) // 2,
geo.y() + (geo.height() - win.height()) // 2)
win.move(geo.x(), geo.y())
win.showMaximized()
sys.exit(app.exec())