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:
2026-03-20 00:57:09 +08:00
parent 4b5ac86b83
commit 5b925fbe02
27 changed files with 4213 additions and 1 deletions

View 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>
);
}

View 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' }}>
&lt;30 超卖 &gt;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>
);
}

View File

@@ -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 = [