feat: Refactor services architecture and update project structure
- Remove Docker-based microservices (docker-compose.yml, Makefile, Dockerfiles) - Update start-dev.sh to use backend.app:app entry point - Add shared schema and client modules for service communication - Add team coordination modules (messenger, registry, task_delegator, coordinator) - Add evaluation hooks and skill adaptation hooks - Add skill template and gateway server - Update frontend WebSocket URL configuration - Add explain components for insider and technical analysis Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
107
frontend/src/components/explain/ExplainInsiderSection.jsx
Normal file
107
frontend/src/components/explain/ExplainInsiderSection.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React from 'react';
|
||||
import { formatDateTime, formatNumber } from '../../utils/formatters';
|
||||
|
||||
export default function ExplainInsiderSection({
|
||||
insiderTrades,
|
||||
selectedSymbol,
|
||||
isOpen,
|
||||
onToggle,
|
||||
onRequest,
|
||||
}) {
|
||||
const handleRefresh = () => {
|
||||
if (onRequest) {
|
||||
onRequest(selectedSymbol);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">内部人交易</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ fontSize: 11, color: '#666666' }}>
|
||||
{insiderTrades.length} 笔内部人交易记录
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
style={{
|
||||
border: '1px solid #111111',
|
||||
background: '#ffffff',
|
||||
color: '#111111',
|
||||
padding: '5px 8px',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 10,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
刷新
|
||||
</button>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
border: '1px solid #111111',
|
||||
background: isOpen ? '#111111' : '#ffffff',
|
||||
color: isOpen ? '#ffffff' : '#111111',
|
||||
padding: '7px 10px',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{isOpen ? '收起' : `展开 ${insiderTrades.length}`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isOpen ? (
|
||||
<div className="empty-state">点击展开查看内部人交易详情</div>
|
||||
) : insiderTrades.length === 0 ? (
|
||||
<div className="empty-state">暂无内部人交易数据</div>
|
||||
) : (
|
||||
<div className="table-wrapper">
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>交易日期</th>
|
||||
<th>内部人</th>
|
||||
<th>职位</th>
|
||||
<th>方向</th>
|
||||
<th>股份数</th>
|
||||
<th>价格</th>
|
||||
<th>持仓变化</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{insiderTrades.slice(0, 20).map((trade, index) => {
|
||||
const isBuy = trade.is_buy;
|
||||
const holdingChange = trade.holding_change;
|
||||
return (
|
||||
<tr key={trade.transaction_date + '-' + trade.name + '-' + index}>
|
||||
<td>{trade.transaction_date || '-'}</td>
|
||||
<td>{trade.name || '-'}</td>
|
||||
<td>{trade.title || '-'}</td>
|
||||
<td style={{
|
||||
fontWeight: 700,
|
||||
color: isBuy === true ? '#00C853' : isBuy === false ? '#FF1744' : '#666666'
|
||||
}}>
|
||||
{isBuy === true ? '买入' : isBuy === false ? '卖出' : '-'}
|
||||
</td>
|
||||
<td>{trade.transaction_shares != null ? formatNumber(trade.transaction_shares) : '-'}</td>
|
||||
<td>${trade.transaction_price_per_share != null ? Number(trade.transaction_price_per_share).toFixed(2) : '-'}</td>
|
||||
<td style={{
|
||||
color: holdingChange != null ? (holdingChange > 0 ? '#00C853' : '#FF1744') : '#666666',
|
||||
fontWeight: holdingChange != null ? 700 : 400
|
||||
}}>
|
||||
{holdingChange != null ? (holdingChange > 0 ? '+' : '') + formatNumber(holdingChange) : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
309
frontend/src/components/explain/ExplainTechnicalSection.jsx
Normal file
309
frontend/src/components/explain/ExplainTechnicalSection.jsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import React from 'react';
|
||||
import { formatNumber } from '../../utils/formatters';
|
||||
|
||||
export default function ExplainTechnicalSection({
|
||||
technicalIndicators,
|
||||
selectedSymbol,
|
||||
isOpen,
|
||||
onToggle,
|
||||
}) {
|
||||
const formatPct = (value) => {
|
||||
if (value == null) return '-';
|
||||
return `${value >= 0 ? '+' : ''}${value.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
const formatPrice = (value) => {
|
||||
if (value == null) return '-';
|
||||
return `$${value.toFixed(2)}`;
|
||||
};
|
||||
|
||||
const rsiStatusColor = (status) => {
|
||||
if (status === 'oversold') return '#00C853';
|
||||
if (status === 'overbought') return '#FF1744';
|
||||
return '#666666';
|
||||
};
|
||||
|
||||
const riskColor = (level) => {
|
||||
if (level === 'HIGH RISK') return '#FF1744';
|
||||
if (level === 'MODERATE RISK') return '#FF9800';
|
||||
return '#00C853';
|
||||
};
|
||||
|
||||
if (!technicalIndicators) {
|
||||
return (
|
||||
<div className="section">
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">技术指标</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ fontSize: 11, color: '#666666' }}>
|
||||
加载中...
|
||||
</div>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
border: '1px solid #111111',
|
||||
background: isOpen ? '#111111' : '#ffffff',
|
||||
color: isOpen ? '#ffffff' : '#111111',
|
||||
padding: '7px 10px',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{isOpen ? '收起' : '展开'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="empty-state">正在加载技术指标数据...</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">技术指标</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ fontSize: 11, color: '#666666' }}>
|
||||
{technicalIndicators.trend} · {technicalIndicators.mean_reversion}
|
||||
</div>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
border: '1px solid #111111',
|
||||
background: isOpen ? '#111111' : '#ffffff',
|
||||
color: isOpen ? '#ffffff' : '#111111',
|
||||
padding: '7px 10px',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{isOpen ? '收起技术指标' : '展开技术指标'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isOpen ? (
|
||||
<div className="empty-state">点击展开查看技术指标详情</div>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 16 }}>
|
||||
{/* MA Section */}
|
||||
<div style={{ border: '1px solid #000000', background: '#fafafa', padding: 12 }}>
|
||||
<div style={{ fontSize: 11, color: '#666666', marginBottom: 10, textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
移动平均线
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: 6, fontSize: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>MA5</span>
|
||||
<span style={{ fontWeight: 600 }}>{formatPrice(technicalIndicators.ma?.ma5)}</span>
|
||||
<span style={{ color: technicalIndicators.ma?.distance?.ma5 > 0 ? '#00C853' : '#FF1744', fontWeight: 700 }}>
|
||||
{formatPct(technicalIndicators.ma?.distance?.ma5)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>MA10</span>
|
||||
<span style={{ fontWeight: 600 }}>{formatPrice(technicalIndicators.ma?.ma10)}</span>
|
||||
<span style={{ color: technicalIndicators.ma?.distance?.ma10 > 0 ? '#00C853' : '#FF1744', fontWeight: 700 }}>
|
||||
{formatPct(technicalIndicators.ma?.distance?.ma10)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>MA20</span>
|
||||
<span style={{ fontWeight: 600 }}>{formatPrice(technicalIndicators.ma?.ma20)}</span>
|
||||
<span style={{ color: technicalIndicators.ma?.distance?.ma20 > 0 ? '#00C853' : '#FF1744', fontWeight: 700 }}>
|
||||
{formatPct(technicalIndicators.ma?.distance?.ma20)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>MA50</span>
|
||||
<span style={{ fontWeight: 600 }}>{formatPrice(technicalIndicators.ma?.ma50)}</span>
|
||||
<span style={{ color: technicalIndicators.ma?.distance?.ma50 > 0 ? '#00C853' : '#FF1744', fontWeight: 700 }}>
|
||||
{formatPct(technicalIndicators.ma?.distance?.ma50)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>MA200</span>
|
||||
<span style={{ fontWeight: 600 }}>{formatPrice(technicalIndicators.ma?.ma200)}</span>
|
||||
<span style={{ color: technicalIndicators.ma?.distance?.ma200 > 0 ? '#00C853' : '#FF1744', fontWeight: 700 }}>
|
||||
{formatPct(technicalIndicators.ma?.distance?.ma200)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RSI Section */}
|
||||
<div style={{ border: '1px solid #000000', background: '#fafafa', padding: 12 }}>
|
||||
<div style={{ fontSize: 11, color: '#666666', marginBottom: 10, textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
RSI (14)
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ fontSize: 28, fontWeight: 700, color: rsiStatusColor(technicalIndicators.rsi?.status) }}>
|
||||
{technicalIndicators.rsi?.rsi14?.toFixed(1) || '-'}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{
|
||||
padding: '2px 8px',
|
||||
background: technicalIndicators.rsi?.status === 'oversold' ? '#E8F5E9' :
|
||||
technicalIndicators.rsi?.status === 'overbought' ? '#FFEBEE' : '#F5F5F5',
|
||||
color: rsiStatusColor(technicalIndicators.rsi?.status),
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
borderRadius: 4
|
||||
}}>
|
||||
{technicalIndicators.rsi?.status === 'oversold' ? '超卖' :
|
||||
technicalIndicators.rsi?.status === 'overbought' ? '超买' : '中性'}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#666666' }}>
|
||||
<30 超卖 >70 超买
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* RSI Gauge */}
|
||||
<div style={{ marginTop: 12, height: 8, background: '#E0E0E0', borderRadius: 4, position: 'relative' }}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
width: `${Math.min(100, Math.max(0, technicalIndicators.rsi?.rsi14 || 0))}%`,
|
||||
height: '100%',
|
||||
background: rsiStatusColor(technicalIndicators.rsi?.status),
|
||||
borderRadius: 4,
|
||||
transition: 'width 0.3s'
|
||||
}} />
|
||||
<div style={{ position: 'absolute', left: '30%', top: -4, width: 1, height: 16, background: '#00C853' }} />
|
||||
<div style={{ position: 'absolute', left: '70%', top: -4, width: 1, height: 16, background: '#FF1744' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MACD Section */}
|
||||
<div style={{ border: '1px solid #000000', background: '#fafafa', padding: 12 }}>
|
||||
<div style={{ fontSize: 11, color: '#666666', marginBottom: 10, textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
MACD
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: 6, fontSize: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666666' }}>MACD 线</span>
|
||||
<span style={{ fontWeight: 600, color: technicalIndicators.macd?.macd > 0 ? '#00C853' : '#FF1744' }}>
|
||||
{formatPrice(technicalIndicators.macd?.macd)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666666' }}>Signal 线</span>
|
||||
<span style={{ fontWeight: 600 }}>{formatPrice(technicalIndicators.macd?.signal)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666666' }}>柱状图</span>
|
||||
<span style={{ fontWeight: 600, color: technicalIndicators.macd?.histogram > 0 ? '#00C853' : '#FF1744' }}>
|
||||
{formatPrice(technicalIndicators.macd?.histogram)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bollinger Bands Section */}
|
||||
<div style={{ border: '1px solid #000000', background: '#fafafa', padding: 12 }}>
|
||||
<div style={{ fontSize: 11, color: '#666666', marginBottom: 10, textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
布林带
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: 6, fontSize: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666666' }}>上轨</span>
|
||||
<span style={{ fontWeight: 600, color: '#FF1744' }}>
|
||||
{formatPrice(technicalIndicators.bollinger?.upper)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666666' }}>中轨</span>
|
||||
<span style={{ fontWeight: 600 }}>{formatPrice(technicalIndicators.bollinger?.mid)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666666' }}>下轨</span>
|
||||
<span style={{ fontWeight: 600, color: '#00C853' }}>
|
||||
{formatPrice(technicalIndicators.bollinger?.lower)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Volatility Section */}
|
||||
<div style={{ border: '1px solid #000000', background: '#fafafa', padding: 12 }}>
|
||||
<div style={{ fontSize: 11, color: '#666666', marginBottom: 10, textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
波动率
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: 6, fontSize: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666666' }}>10日</span>
|
||||
<span style={{ fontWeight: 600 }}>{formatPct(technicalIndicators.volatility?.vol_10d)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666666' }}>20日</span>
|
||||
<span style={{ fontWeight: 600 }}>{formatPct(technicalIndicators.volatility?.vol_20d)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666666' }}>60日</span>
|
||||
<span style={{ fontWeight: 600 }}>{formatPct(technicalIndicators.volatility?.vol_60d)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 8, paddingTop: 8, borderTop: '1px solid #E0E0E0' }}>
|
||||
<span style={{ color: '#666666' }}>年化波动率</span>
|
||||
<span style={{ fontWeight: 700 }}>{formatPct(technicalIndicators.volatility?.annualized)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#666666' }}>风险等级</span>
|
||||
<span style={{
|
||||
fontWeight: 700,
|
||||
color: riskColor(technicalIndicators.volatility?.risk_level)
|
||||
}}>
|
||||
{technicalIndicators.volatility?.risk_level || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trend Summary */}
|
||||
<div style={{ border: '1px solid #000000', background: '#fafafa', padding: 12 }}>
|
||||
<div style={{ fontSize: 11, color: '#666666', marginBottom: 10, textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
趋势判断
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{
|
||||
padding: '4px 12px',
|
||||
background: technicalIndicators.trend?.includes('BULLISH') ? '#E8F5E9' :
|
||||
technicalIndicators.trend?.includes('BEARISH') ? '#FFEBEE' : '#F5F5F5',
|
||||
color: technicalIndicators.trend?.includes('BULLISH') ? '#00C853' :
|
||||
technicalIndicators.trend?.includes('BEARISH') ? '#FF1744' : '#666666',
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
borderRadius: 4
|
||||
}}>
|
||||
{technicalIndicators.trend || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{
|
||||
padding: '4px 12px',
|
||||
background: technicalIndicators.mean_reversion?.includes('OVERBOUGHT') ? '#FFEBEE' :
|
||||
technicalIndicators.mean_reversion?.includes('OVERSOLD') ? '#E8F5E9' : '#F5F5F5',
|
||||
color: technicalIndicators.mean_reversion?.includes('OVERBOUGHT') ? '#FF1744' :
|
||||
technicalIndicators.mean_reversion?.includes('OVERSOLD') ? '#00C853' : '#666666',
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
borderRadius: 4
|
||||
}}>
|
||||
{technicalIndicators.mean_reversion || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#666666', marginTop: 4 }}>
|
||||
当前价格: {formatPrice(technicalIndicators.current_price)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -130,7 +130,7 @@ export const CHART_MARGIN = { left: 60, right: 20, top: 20, bottom: 40 };
|
||||
export const AXIS_TICKS = 5;
|
||||
|
||||
// WebSocket configuration
|
||||
export const WS_URL = import.meta.env.VITE_WS_URL || "ws://localhost:8765";
|
||||
export const WS_URL = import.meta.env.VITE_WS_URL || "ws://localhost:8000";
|
||||
|
||||
// Initial ticker symbols for the production watchlist
|
||||
export const INITIAL_TICKERS = [
|
||||
|
||||
Reference in New Issue
Block a user