feat: 添加新闻增量刷新和前端组件修复
- 新增 refresh_news_incremental/refresh_news_for_symbols 函数支持增量新闻获取 - 在 live cycle 中集成新闻刷新逻辑 - AgentFeed 支持 agentProfilesByAgent 显示模型信息 - StatisticsView 修复 stats 计算逻辑,使用 portfolioData 作为 fallback - StockExplainView 修复 useEffect 依赖项问题 - AppShell/RoomView 传递 agentProfilesByAgent 属性 - start-dev.sh 调整日志级别为 warning 减少噪音 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ from typing import Iterable
|
|||||||
|
|
||||||
from backend.data.market_store import MarketStore
|
from backend.data.market_store import MarketStore
|
||||||
from backend.data.news_alignment import align_news_for_symbol
|
from backend.data.news_alignment import align_news_for_symbol
|
||||||
|
from backend.data.provider_router import ProviderRouter
|
||||||
from backend.data.polygon_client import (
|
from backend.data.polygon_client import (
|
||||||
fetch_news,
|
fetch_news,
|
||||||
fetch_ohlc,
|
fetch_ohlc,
|
||||||
@@ -24,6 +25,35 @@ def _default_start(years: int = 2) -> str:
|
|||||||
return (datetime.now(timezone.utc).date() - timedelta(days=years * 366)).isoformat()
|
return (datetime.now(timezone.utc).date() - timedelta(days=years * 366)).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_provider_news_rows(ticker: str, news_items: Iterable[Any]) -> list[dict]:
|
||||||
|
rows: list[dict] = []
|
||||||
|
for item in news_items:
|
||||||
|
payload = item.model_dump() if hasattr(item, "model_dump") else dict(item or {})
|
||||||
|
related = payload.get("related")
|
||||||
|
if isinstance(related, str):
|
||||||
|
related_list = [value.strip().upper() for value in related.split(",") if value.strip()]
|
||||||
|
elif isinstance(related, list):
|
||||||
|
related_list = [str(value).strip().upper() for value in related if str(value).strip()]
|
||||||
|
else:
|
||||||
|
related_list = []
|
||||||
|
if ticker not in related_list:
|
||||||
|
related_list.append(ticker)
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"title": payload.get("title"),
|
||||||
|
"description": payload.get("summary"),
|
||||||
|
"summary": payload.get("summary"),
|
||||||
|
"article_url": payload.get("url"),
|
||||||
|
"published_utc": payload.get("date"),
|
||||||
|
"publisher": payload.get("source"),
|
||||||
|
"tickers": related_list,
|
||||||
|
"category": payload.get("category"),
|
||||||
|
"raw_json": payload,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def ingest_ticker_history(
|
def ingest_ticker_history(
|
||||||
symbol: str,
|
symbol: str,
|
||||||
*,
|
*,
|
||||||
@@ -114,6 +144,80 @@ def update_ticker_incremental(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_news_incremental(
|
||||||
|
symbol: str,
|
||||||
|
*,
|
||||||
|
end_date: str | None = None,
|
||||||
|
store: MarketStore | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Incrementally fetch company news using the configured provider router."""
|
||||||
|
ticker = normalize_symbol(symbol)
|
||||||
|
market_store = store or MarketStore()
|
||||||
|
watermarks = market_store.get_ticker_watermarks(ticker)
|
||||||
|
end = end_date or _today_utc()
|
||||||
|
start_news = (
|
||||||
|
(datetime.fromisoformat(watermarks["last_news_fetch"]) + timedelta(days=1)).date().isoformat()
|
||||||
|
if watermarks.get("last_news_fetch")
|
||||||
|
else _default_start()
|
||||||
|
)
|
||||||
|
|
||||||
|
if start_news > end:
|
||||||
|
return {
|
||||||
|
"symbol": ticker,
|
||||||
|
"start_news_date": start_news,
|
||||||
|
"end_date": end,
|
||||||
|
"news": 0,
|
||||||
|
"aligned": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
router = ProviderRouter()
|
||||||
|
news_items, source = router.get_company_news(
|
||||||
|
ticker=ticker,
|
||||||
|
start_date=start_news,
|
||||||
|
end_date=end,
|
||||||
|
limit=1000,
|
||||||
|
)
|
||||||
|
news_rows = _normalize_provider_news_rows(ticker, news_items)
|
||||||
|
news_count = market_store.upsert_news(ticker, news_rows, source=source) if news_rows else 0
|
||||||
|
aligned_count = align_news_for_symbol(market_store, ticker)
|
||||||
|
market_store.update_fetch_watermark(
|
||||||
|
symbol=ticker,
|
||||||
|
news_date=end if news_rows or watermarks.get("last_news_fetch") else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"symbol": ticker,
|
||||||
|
"start_news_date": start_news,
|
||||||
|
"end_date": end,
|
||||||
|
"news": news_count,
|
||||||
|
"aligned": aligned_count,
|
||||||
|
"source": source,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_news_for_symbols(
|
||||||
|
symbols: Iterable[str],
|
||||||
|
*,
|
||||||
|
end_date: str | None = None,
|
||||||
|
store: MarketStore | None = None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Incrementally refresh company news for a list of tickers."""
|
||||||
|
market_store = store or MarketStore()
|
||||||
|
results = []
|
||||||
|
for symbol in symbols:
|
||||||
|
ticker = normalize_symbol(symbol)
|
||||||
|
if not ticker:
|
||||||
|
continue
|
||||||
|
results.append(
|
||||||
|
refresh_news_incremental(
|
||||||
|
ticker,
|
||||||
|
end_date=end_date,
|
||||||
|
store=market_store,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def ingest_symbols(
|
def ingest_symbols(
|
||||||
symbols: Iterable[str],
|
symbols: Iterable[str],
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from backend.data.market_ingest import ingest_symbols
|
from backend.data.market_ingest import ingest_symbols, refresh_news_for_symbols
|
||||||
from backend.domains import trading as trading_domain
|
from backend.domains import trading as trading_domain
|
||||||
from backend.utils.msg_adapter import FrontendAdapter
|
from backend.utils.msg_adapter import FrontendAdapter
|
||||||
|
|
||||||
@@ -200,6 +200,23 @@ async def run_live_cycle(gateway: Any, date: str, tickers: list[str]) -> None:
|
|||||||
trading_date = gateway.market_service.get_live_trading_date()
|
trading_date = gateway.market_service.get_live_trading_date()
|
||||||
logger.info("Live cycle: triggered=%s, trading_date=%s", date, trading_date)
|
logger.info("Live cycle: triggered=%s, trading_date=%s", date, trading_date)
|
||||||
|
|
||||||
|
try:
|
||||||
|
news_refresh = await asyncio.to_thread(
|
||||||
|
refresh_news_for_symbols,
|
||||||
|
tickers,
|
||||||
|
end_date=trading_date,
|
||||||
|
store=gateway.storage.market_store,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"News refresh complete: %s",
|
||||||
|
", ".join(
|
||||||
|
f"{item['symbol']} news={item['news']}"
|
||||||
|
for item in news_refresh
|
||||||
|
) or "no symbols",
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Live cycle news refresh failed: %s", exc)
|
||||||
|
|
||||||
await gateway.state_sync.on_cycle_start(trading_date)
|
await gateway.state_sync.on_cycle_start(trading_date)
|
||||||
gateway._dashboard.update(date=trading_date, status="Analyzing...")
|
gateway._dashboard.update(date=trading_date, status="Analyzing...")
|
||||||
|
|
||||||
|
|||||||
@@ -466,6 +466,7 @@ export default function LiveTradingApp() {
|
|||||||
currentDate={currentDate}
|
currentDate={currentDate}
|
||||||
stockRequests={stockRequests}
|
stockRequests={stockRequests}
|
||||||
agentRequests={agentRequests}
|
agentRequests={agentRequests}
|
||||||
|
agentProfilesByAgent={agentProfilesByAgent}
|
||||||
leftWidth={leftWidth}
|
leftWidth={leftWidth}
|
||||||
isResizing={isResizing}
|
isResizing={isResizing}
|
||||||
onMouseDown={() => useUIStore.getState().setIsResizing(true)}
|
onMouseDown={() => useUIStore.getState().setIsResizing(true)}
|
||||||
|
|||||||
@@ -35,14 +35,22 @@ const stripMarkdown = (text) => {
|
|||||||
.replace(/^[-=]+$/gm, '');
|
.replace(/^[-=]+$/gm, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const AgentFeed = forwardRef(({ feed, leaderboard }, ref) => {
|
const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref) => {
|
||||||
const feedContentRef = useRef(null);
|
const feedContentRef = useRef(null);
|
||||||
const [highlightedId, setHighlightedId] = useState(null);
|
const [highlightedId, setHighlightedId] = useState(null);
|
||||||
const [selectedAgent, setSelectedAgent] = useState('all');
|
const [selectedAgent, setSelectedAgent] = useState('all');
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
|
||||||
const getAgentModelInfo = (agentId) => {
|
const getAgentModelInfo = (agentId) => {
|
||||||
if (!leaderboard || !agentId) return { modelName: null, modelProvider: null };
|
if (!agentId) return { modelName: null, modelProvider: null };
|
||||||
|
const profile = agentProfilesByAgent?.[agentId];
|
||||||
|
if (profile?.model_name) {
|
||||||
|
return {
|
||||||
|
modelName: profile.model_name,
|
||||||
|
modelProvider: profile.model_provider
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!leaderboard) return { modelName: null, modelProvider: null };
|
||||||
const agentData = leaderboard.find(lb => lb.id === agentId || lb.agentId === agentId);
|
const agentData = leaderboard.find(lb => lb.id === agentId || lb.agentId === agentId);
|
||||||
return {
|
return {
|
||||||
modelName: agentData?.modelName,
|
modelName: agentData?.modelName,
|
||||||
@@ -52,7 +60,17 @@ const AgentFeed = forwardRef(({ feed, leaderboard }, ref) => {
|
|||||||
|
|
||||||
// Get agent info by name
|
// Get agent info by name
|
||||||
const getAgentInfoByName = (agentName) => {
|
const getAgentInfoByName = (agentName) => {
|
||||||
if (!leaderboard || !agentName) return null;
|
if (!agentName) return null;
|
||||||
|
const agentConfig = AGENTS.find((agent) => agent.name === agentName);
|
||||||
|
const profile = agentConfig ? agentProfilesByAgent?.[agentConfig.id] : null;
|
||||||
|
if (agentConfig && profile?.model_name) {
|
||||||
|
return {
|
||||||
|
agentId: agentConfig.id,
|
||||||
|
modelName: profile.model_name,
|
||||||
|
modelProvider: profile.model_provider
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!leaderboard) return null;
|
||||||
const agentData = leaderboard.find(lb => lb.name === agentName || lb.agentName === agentName);
|
const agentData = leaderboard.find(lb => lb.name === agentName || lb.agentName === agentName);
|
||||||
if (!agentData) return null;
|
if (!agentData) return null;
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export default function AppShell({
|
|||||||
stockRequests,
|
stockRequests,
|
||||||
// Agent request handlers
|
// Agent request handlers
|
||||||
agentRequests,
|
agentRequests,
|
||||||
|
agentProfilesByAgent,
|
||||||
// Layout
|
// Layout
|
||||||
leftWidth,
|
leftWidth,
|
||||||
isResizing,
|
isResizing,
|
||||||
@@ -440,6 +441,7 @@ export default function AppShell({
|
|||||||
bubbles={bubbles}
|
bubbles={bubbles}
|
||||||
bubbleFor={bubbleFor}
|
bubbleFor={bubbleFor}
|
||||||
leaderboard={leaderboard}
|
leaderboard={leaderboard}
|
||||||
|
agentProfilesByAgent={agentProfilesByAgent}
|
||||||
feed={feed}
|
feed={feed}
|
||||||
onJumpToMessage={handleJumpToMessage}
|
onJumpToMessage={handleJumpToMessage}
|
||||||
onOpenLaunchConfig={() => setIsRuntimeSettingsOpen(true)}
|
onOpenLaunchConfig={() => setIsRuntimeSettingsOpen(true)}
|
||||||
@@ -518,6 +520,7 @@ export default function AppShell({
|
|||||||
trades={trades}
|
trades={trades}
|
||||||
holdings={holdings}
|
holdings={holdings}
|
||||||
stats={stats}
|
stats={stats}
|
||||||
|
portfolioData={portfolioData}
|
||||||
baseline_vw={portfolioData.baseline_vw}
|
baseline_vw={portfolioData.baseline_vw}
|
||||||
equity={portfolioData.equity}
|
equity={portfolioData.equity}
|
||||||
leaderboard={leaderboard}
|
leaderboard={leaderboard}
|
||||||
@@ -535,7 +538,7 @@ export default function AppShell({
|
|||||||
{/* Right Panel: Agent Feed */}
|
{/* Right Panel: Agent Feed */}
|
||||||
<div className="right-panel" style={{ width: `${100 - leftWidth}%` }}>
|
<div className="right-panel" style={{ width: `${100 - leftWidth}%` }}>
|
||||||
<Suspense fallback={<ViewLoadingFallback label="加载消息流..." />}>
|
<Suspense fallback={<ViewLoadingFallback label="加载消息流..." />}>
|
||||||
<AgentFeed ref={agentFeedRef} feed={feed} leaderboard={leaderboard} />
|
<AgentFeed ref={agentFeedRef} feed={feed} leaderboard={leaderboard} agentProfilesByAgent={agentProfilesByAgent} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function getRankMedal(rank) {
|
|||||||
* Supports click and hover (1.5s) to show agent performance cards
|
* Supports click and hover (1.5s) to show agent performance cards
|
||||||
* Supports replay mode - completely independent from live mode
|
* Supports replay mode - completely independent from live mode
|
||||||
*/
|
*/
|
||||||
export default function RoomView({ bubbles, bubbleFor, leaderboard, feed, onJumpToMessage, onOpenLaunchConfig }) {
|
export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfilesByAgent, feed, onJumpToMessage, onOpenLaunchConfig }) {
|
||||||
const canvasRef = useRef(null);
|
const canvasRef = useRef(null);
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
@@ -162,11 +162,14 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, feed, onJump
|
|||||||
const getAgentData = (agentId) => {
|
const getAgentData = (agentId) => {
|
||||||
const agent = AGENTS.find(a => a.id === agentId);
|
const agent = AGENTS.find(a => a.id === agentId);
|
||||||
if (!agent) return null;
|
if (!agent) return null;
|
||||||
|
const profile = agentProfilesByAgent?.[agentId] || null;
|
||||||
|
|
||||||
// If no leaderboard data, return agent with default stats
|
// If no leaderboard data, return agent with default stats
|
||||||
if (!leaderboard || !Array.isArray(leaderboard)) {
|
if (!leaderboard || !Array.isArray(leaderboard)) {
|
||||||
return {
|
return {
|
||||||
...agent,
|
...agent,
|
||||||
|
modelName: profile?.model_name || null,
|
||||||
|
modelProvider: profile?.model_provider || null,
|
||||||
bull: { n: 0, win: 0, unknown: 0 },
|
bull: { n: 0, win: 0, unknown: 0 },
|
||||||
bear: { n: 0, win: 0, unknown: 0 },
|
bear: { n: 0, win: 0, unknown: 0 },
|
||||||
winRate: null,
|
winRate: null,
|
||||||
@@ -181,6 +184,8 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, feed, onJump
|
|||||||
if (!leaderboardData) {
|
if (!leaderboardData) {
|
||||||
return {
|
return {
|
||||||
...agent,
|
...agent,
|
||||||
|
modelName: profile?.model_name || null,
|
||||||
|
modelProvider: profile?.model_provider || null,
|
||||||
bull: { n: 0, win: 0, unknown: 0 },
|
bull: { n: 0, win: 0, unknown: 0 },
|
||||||
bear: { n: 0, win: 0, unknown: 0 },
|
bear: { n: 0, win: 0, unknown: 0 },
|
||||||
winRate: null,
|
winRate: null,
|
||||||
@@ -193,6 +198,8 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, feed, onJump
|
|||||||
return {
|
return {
|
||||||
...agent,
|
...agent,
|
||||||
...leaderboardData,
|
...leaderboardData,
|
||||||
|
modelName: profile?.model_name || leaderboardData.modelName || null,
|
||||||
|
modelProvider: profile?.model_provider || leaderboardData.modelProvider || null,
|
||||||
avatar: agent.avatar // Always use the frontend's avatar URL
|
avatar: agent.avatar // Always use the frontend's avatar URL
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,12 +8,36 @@ import { formatNumber, formatDateTime } from '../utils/formatters';
|
|||||||
* Left: Performance Overview (35%) | Right: Holdings + Trades (65%)
|
* Left: Performance Overview (35%) | Right: Holdings + Trades (65%)
|
||||||
* No scrolling - content fits within viewport with pagination
|
* No scrolling - content fits within viewport with pagination
|
||||||
*/
|
*/
|
||||||
export default function StatisticsView({ trades, holdings, stats, baseline_vw, equity, leaderboard }) {
|
export default function StatisticsView({ trades, holdings, stats, baseline_vw, equity, leaderboard, portfolioData }) {
|
||||||
const [holdingsPage, setHoldingsPage] = useState(1);
|
const [holdingsPage, setHoldingsPage] = useState(1);
|
||||||
const [tradesPage, setTradesPage] = useState(1);
|
const [tradesPage, setTradesPage] = useState(1);
|
||||||
const holdingsPerPage = 5;
|
const holdingsPerPage = 5;
|
||||||
const tradesPerPage = 8;
|
const tradesPerPage = 8;
|
||||||
|
|
||||||
|
const effectiveStats = React.useMemo(() => {
|
||||||
|
const base = stats && typeof stats === 'object' ? stats : {};
|
||||||
|
const netValue = Number(portfolioData?.netValue ?? 0);
|
||||||
|
const pnl = Number(portfolioData?.pnl ?? 0);
|
||||||
|
const hasPortfolioValue = Number.isFinite(netValue) && netValue > 0;
|
||||||
|
const hasMeaningfulStats = Number(base?.totalAssetValue ?? 0) > 0;
|
||||||
|
|
||||||
|
if (hasMeaningfulStats || !hasPortfolioValue) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cashHolding = Array.isArray(holdings)
|
||||||
|
? holdings.find((item) => String(item?.ticker || '').toUpperCase() === 'CASH')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
totalAssetValue: netValue,
|
||||||
|
totalReturn: pnl,
|
||||||
|
cashPosition: Number(cashHolding?.marketValue ?? cashHolding?.currentPrice ?? 0),
|
||||||
|
totalTrades: Array.isArray(trades) ? trades.length : 0,
|
||||||
|
};
|
||||||
|
}, [holdings, portfolioData, stats, trades]);
|
||||||
|
|
||||||
// Calculate pagination for holdings
|
// Calculate pagination for holdings
|
||||||
const totalHoldingsPages = Math.ceil(holdings.length / holdingsPerPage);
|
const totalHoldingsPages = Math.ceil(holdings.length / holdingsPerPage);
|
||||||
const holdingsStartIndex = (holdingsPage - 1) * holdingsPerPage;
|
const holdingsStartIndex = (holdingsPage - 1) * holdingsPerPage;
|
||||||
@@ -28,12 +52,12 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
|||||||
|
|
||||||
// Calculate excess return (Evatraders return - benchmark value-weighted return)
|
// Calculate excess return (Evatraders return - benchmark value-weighted return)
|
||||||
const calculateExcessReturn = () => {
|
const calculateExcessReturn = () => {
|
||||||
if (!stats || !baseline_vw || baseline_vw.length === 0) {
|
if (!effectiveStats || !baseline_vw || baseline_vw.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Evatraders return from stats
|
// Get Evatraders return from stats
|
||||||
const evatradersReturn = stats.totalReturn || 0; // Already in percentage
|
const evatradersReturn = effectiveStats.totalReturn || 0; // Already in percentage
|
||||||
|
|
||||||
// Calculate benchmark return from baseline_vw
|
// Calculate benchmark return from baseline_vw
|
||||||
// baseline_vw format: [{t: timestamp, v: value}, ...] or [value, ...]
|
// baseline_vw format: [{t: timestamp, v: value}, ...] or [value, ...]
|
||||||
@@ -130,7 +154,7 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
|||||||
borderRight: '2px solid #e0e0e0',
|
borderRight: '2px solid #e0e0e0',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}>
|
}}>
|
||||||
{stats ? (
|
{effectiveStats ? (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '24px',
|
padding: '24px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -179,7 +203,7 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
|||||||
fontFamily: '"Courier New", monospace',
|
fontFamily: '"Courier New", monospace',
|
||||||
lineHeight: 1
|
lineHeight: 1
|
||||||
}}>
|
}}>
|
||||||
${formatNumber(stats.totalAssetValue || 0)}
|
${formatNumber(effectiveStats.totalAssetValue || 0)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -272,10 +296,10 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
|||||||
<div style={{
|
<div style={{
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: (stats.totalReturn || 0) >= 0 ? '#00C853' : '#FF1744',
|
color: (effectiveStats.totalReturn || 0) >= 0 ? '#00C853' : '#FF1744',
|
||||||
fontFamily: '"Courier New", monospace'
|
fontFamily: '"Courier New", monospace'
|
||||||
}}>
|
}}>
|
||||||
{(stats.totalReturn || 0) >= 0 ? '+' : ''}{(stats.totalReturn || 0).toFixed(2)}%
|
{(effectiveStats.totalReturn || 0) >= 0 ? '+' : ''}{(effectiveStats.totalReturn || 0).toFixed(2)}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -304,7 +328,7 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
|||||||
color: '#000000',
|
color: '#000000',
|
||||||
fontFamily: '"Courier New", monospace'
|
fontFamily: '"Courier New", monospace'
|
||||||
}}>
|
}}>
|
||||||
${formatNumber(stats.cashPosition || 0)}
|
${formatNumber(effectiveStats.cashPosition || 0)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -330,7 +354,7 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
|||||||
color: '#000000',
|
color: '#000000',
|
||||||
fontFamily: '"Courier New", monospace'
|
fontFamily: '"Courier New", monospace'
|
||||||
}}>
|
}}>
|
||||||
{stats.totalTrades || 0}
|
{effectiveStats.totalTrades || 0}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export default function StockExplainView({
|
|||||||
if (!selectedSymbol || !selectedEventDate || !onRequestNewsForDate) {
|
if (!selectedSymbol || !selectedEventDate || !onRequestNewsForDate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Array.isArray(newsSnapshot?.byDate?.[selectedEventDate]) && newsSnapshot.byDate[selectedEventDate].length > 0) {
|
if (Object.prototype.hasOwnProperty.call(newsSnapshot?.byDate || {}, selectedEventDate)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onRequestNewsForDate(selectedSymbol, selectedEventDate);
|
onRequestNewsForDate(selectedSymbol, selectedEventDate);
|
||||||
@@ -185,21 +185,21 @@ export default function StockExplainView({
|
|||||||
if (!selectedSymbol || !onRequestStory || !currentDate) {
|
if (!selectedSymbol || !onRequestStory || !currentDate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selectedStory?.story) {
|
if (Object.prototype.hasOwnProperty.call(newsSnapshot?.storyCache || {}, currentDate)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onRequestStory(selectedSymbol, currentDate);
|
onRequestStory(selectedSymbol, currentDate);
|
||||||
}, [currentDate, onRequestStory, selectedStory, selectedSymbol]);
|
}, [currentDate, newsSnapshot, onRequestStory, selectedStory, selectedSymbol]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedSymbol || !selectedEventDate || !onRequestSimilarDays) {
|
if (!selectedSymbol || !selectedEventDate || !onRequestSimilarDays) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selectedSimilarDays?.items?.length) {
|
if (Object.prototype.hasOwnProperty.call(newsSnapshot?.similarDaysCache || {}, selectedEventDate)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onRequestSimilarDays(selectedSymbol, selectedEventDate);
|
onRequestSimilarDays(selectedSymbol, selectedEventDate);
|
||||||
}, [onRequestSimilarDays, selectedEventDate, selectedSimilarDays, selectedSymbol]);
|
}, [newsSnapshot, onRequestSimilarDays, selectedEventDate, selectedSimilarDays, selectedSymbol]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedSymbol || !onRequestTechnicalIndicators) {
|
if (!selectedSymbol || !onRequestTechnicalIndicators) {
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ start_service() {
|
|||||||
--port "${port}" \
|
--port "${port}" \
|
||||||
--reload \
|
--reload \
|
||||||
--reload-dir backend \
|
--reload-dir backend \
|
||||||
--log-level info &
|
--log-level warning \
|
||||||
|
--no-access-log &
|
||||||
PIDS+=($!)
|
PIDS+=($!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user