383 lines
12 KiB
JavaScript
383 lines
12 KiB
JavaScript
// src/views/TradingSimulation/hooks/useTradingAccount.js - 模拟盘账户管理 Hook
|
||
import { useState, useCallback } from 'react';
|
||
import { useAuth } from '../../../contexts/AuthContext';
|
||
import { logger } from '../../../utils/logger';
|
||
import { getApiBase } from '../../../utils/apiConfig';
|
||
|
||
// API 基础URL - 修复HTTPS混合内容问题
|
||
const API_BASE_URL = getApiBase();
|
||
|
||
// API 请求封装
|
||
const apiRequest = async (url, options = {}) => {
|
||
const response = await fetch(`${API_BASE_URL}${url}`, {
|
||
credentials: 'include', // 包含session cookie
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...options.headers
|
||
},
|
||
...options
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json().catch(() => ({}));
|
||
throw new Error(errorData.error || `HTTP ${response.status}`);
|
||
}
|
||
|
||
return response.json();
|
||
};
|
||
|
||
// 数据字段映射函数
|
||
const mapAccountData = (backendData) => {
|
||
return {
|
||
id: backendData.account_id,
|
||
accountName: backendData.account_name,
|
||
initialCash: backendData.initial_capital,
|
||
availableCash: backendData.available_cash,
|
||
frozenCash: backendData.frozen_cash,
|
||
marketValue: backendData.position_value,
|
||
totalAssets: backendData.total_assets,
|
||
totalProfit: backendData.total_profit,
|
||
totalProfitPercent: backendData.total_profit_rate,
|
||
dailyProfit: backendData.daily_profit,
|
||
dailyProfitRate: backendData.daily_profit_rate,
|
||
riskLevel: 'MEDIUM', // 默认值
|
||
marginBalance: 0,
|
||
shortBalance: 0,
|
||
lastUpdated: backendData.updated_at,
|
||
createdAt: backendData.created_at
|
||
};
|
||
};
|
||
|
||
const mapPositionData = (backendPositions) => {
|
||
return backendPositions.map(pos => ({
|
||
id: pos.id,
|
||
stockCode: pos.stock_code,
|
||
stockName: pos.stock_name,
|
||
quantity: pos.position_qty,
|
||
availableQuantity: pos.available_qty,
|
||
frozenQuantity: pos.frozen_qty,
|
||
avgPrice: pos.avg_cost,
|
||
currentPrice: pos.current_price,
|
||
totalCost: pos.position_qty * pos.avg_cost,
|
||
marketValue: pos.market_value,
|
||
profit: pos.profit,
|
||
profitRate: pos.profit_rate,
|
||
todayProfit: pos.today_profit,
|
||
todayProfitRate: pos.today_profit_rate,
|
||
updatedAt: pos.updated_at
|
||
}));
|
||
};
|
||
|
||
const mapOrderData = (backendOrders) => {
|
||
return backendOrders.map(order => ({
|
||
id: order.id,
|
||
orderId: order.order_no,
|
||
stockCode: order.stock_code,
|
||
stockName: order.stock_name,
|
||
type: order.order_type, // 添加 type 字段
|
||
orderType: order.order_type,
|
||
priceType: order.price_type,
|
||
orderPrice: order.order_price,
|
||
quantity: order.order_qty,
|
||
filledQuantity: order.filled_qty,
|
||
price: order.filled_price, // 添加 price 字段
|
||
filledPrice: order.filled_price,
|
||
totalAmount: order.filled_amount, // 添加 totalAmount 字段
|
||
filledAmount: order.filled_amount,
|
||
commission: order.commission,
|
||
stampTax: order.stamp_tax,
|
||
transferFee: order.transfer_fee,
|
||
totalFee: order.total_fee,
|
||
status: order.status,
|
||
rejectReason: order.reject_reason,
|
||
createdAt: order.order_time,
|
||
filledAt: order.filled_time
|
||
}));
|
||
};
|
||
|
||
export function useTradingAccount() {
|
||
const { user } = useAuth();
|
||
const [account, setAccount] = useState(null);
|
||
const [positions, setPositions] = useState([]);
|
||
const [tradingHistory, setTradingHistory] = useState([]);
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [error, setError] = useState(null);
|
||
const [stockQuotes, setStockQuotes] = useState({});
|
||
|
||
// 搜索股票
|
||
const searchStocks = useCallback(async (keyword) => {
|
||
// 调试模式:返回模拟数据
|
||
if (!user || user.id === 'demo') {
|
||
logger.debug('useTradingAccount', '调试模式:模拟股票搜索', { keyword });
|
||
const mockStocks = [
|
||
{
|
||
stock_code: '000001',
|
||
stock_name: '平安银行',
|
||
current_price: 12.50,
|
||
pinyin_abbr: 'payh',
|
||
security_type: 'A股',
|
||
exchange: '深交所'
|
||
},
|
||
{
|
||
stock_code: '600036',
|
||
stock_name: '招商银行',
|
||
current_price: 42.80,
|
||
pinyin_abbr: 'zsyh',
|
||
security_type: 'A股',
|
||
exchange: '上交所'
|
||
},
|
||
{
|
||
stock_code: '688256',
|
||
stock_name: '寒武纪',
|
||
current_price: 1394.94,
|
||
pinyin_abbr: 'hwj',
|
||
security_type: 'A股',
|
||
exchange: '上交所科创板'
|
||
}
|
||
];
|
||
|
||
return mockStocks.filter(stock =>
|
||
stock.stock_code.includes(keyword) ||
|
||
stock.stock_name.includes(keyword) ||
|
||
stock.pinyin_abbr.includes(keyword.toLowerCase())
|
||
);
|
||
}
|
||
|
||
try {
|
||
const response = await apiRequest(`/api/stocks/search?q=${encodeURIComponent(keyword)}&limit=10`);
|
||
return response.data || [];
|
||
} catch (error) {
|
||
logger.error('useTradingAccount', 'searchStocks', error, { keyword });
|
||
return [];
|
||
}
|
||
}, [user]);
|
||
|
||
// 刷新账户数据
|
||
const refreshAccount = useCallback(async () => {
|
||
// 调试模式:使用模拟数据(因为后端API可能有CORS问题)
|
||
if (!user || user.id === 'demo') {
|
||
logger.debug('useTradingAccount', '调试模式:使用模拟账户数据', { userId: user?.id });
|
||
setAccount({
|
||
id: 'demo',
|
||
accountName: '演示账户',
|
||
initialCash: 1000000,
|
||
availableCash: 950000,
|
||
frozenCash: 0,
|
||
marketValue: 50000,
|
||
totalAssets: 1000000,
|
||
totalProfit: 0,
|
||
totalProfitPercent: 0,
|
||
dailyProfit: 0,
|
||
dailyProfitRate: 0,
|
||
riskLevel: 'MEDIUM',
|
||
marginBalance: 0,
|
||
shortBalance: 0,
|
||
lastUpdated: new Date().toISOString(),
|
||
createdAt: new Date().toISOString()
|
||
});
|
||
setPositions([]);
|
||
setTradingHistory([]);
|
||
setIsLoading(false);
|
||
return;
|
||
}
|
||
|
||
setIsLoading(true);
|
||
setError(null);
|
||
|
||
try {
|
||
// 获取账户信息
|
||
const accountResponse = await apiRequest('/api/simulation/account');
|
||
setAccount(mapAccountData(accountResponse.data));
|
||
|
||
// 获取持仓信息
|
||
const positionsResponse = await apiRequest('/api/simulation/positions');
|
||
setPositions(mapPositionData(positionsResponse.data || []));
|
||
|
||
// 获取交易历史
|
||
const ordersResponse = await apiRequest('/api/simulation/orders?limit=100');
|
||
setTradingHistory(mapOrderData(ordersResponse.data || []));
|
||
|
||
} catch (err) {
|
||
logger.error('useTradingAccount', 'refreshAccount', err, { userId: user?.id });
|
||
setError('加载账户数据失败: ' + err.message);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}, [user]);
|
||
|
||
// 买入股票
|
||
const buyStock = useCallback(async (stockCode, quantity, orderType = 'MARKET', limitPrice = null) => {
|
||
if (!account) {
|
||
throw new Error('账户未初始化');
|
||
}
|
||
|
||
// 调试模式:模拟买入成功
|
||
if (!user || user.id === 'demo') {
|
||
logger.debug('useTradingAccount', '调试模式:模拟买入', { stockCode, quantity, orderType });
|
||
return { success: true, orderId: 'demo_' + Date.now() };
|
||
}
|
||
|
||
setIsLoading(true);
|
||
try {
|
||
const requestData = {
|
||
stock_code: stockCode,
|
||
order_type: 'BUY',
|
||
order_qty: quantity,
|
||
price_type: orderType
|
||
};
|
||
|
||
const response = await apiRequest('/api/simulation/place-order', {
|
||
method: 'POST',
|
||
body: JSON.stringify(requestData)
|
||
});
|
||
|
||
// 刷新账户数据
|
||
await refreshAccount();
|
||
|
||
return { success: true, orderId: response.data.order_no };
|
||
|
||
} catch (err) {
|
||
throw new Error('买入失败: ' + err.message);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}, [account, user, refreshAccount]);
|
||
|
||
// 卖出股票
|
||
const sellStock = useCallback(async (stockCode, quantity, orderType = 'MARKET', limitPrice = null) => {
|
||
if (!account) {
|
||
throw new Error('账户未初始化');
|
||
}
|
||
|
||
// 调试模式:模拟卖出成功
|
||
if (!user || user.id === 'demo') {
|
||
logger.debug('useTradingAccount', '调试模式:模拟卖出', { stockCode, quantity, orderType });
|
||
return { success: true, orderId: 'demo_' + Date.now() };
|
||
}
|
||
|
||
setIsLoading(true);
|
||
try {
|
||
const requestData = {
|
||
stock_code: stockCode,
|
||
order_type: 'SELL',
|
||
order_qty: quantity,
|
||
price_type: orderType
|
||
};
|
||
|
||
const response = await apiRequest('/api/simulation/place-order', {
|
||
method: 'POST',
|
||
body: JSON.stringify(requestData)
|
||
});
|
||
|
||
// 刷新账户数据
|
||
await refreshAccount();
|
||
|
||
return { success: true, orderId: response.data.order_no };
|
||
|
||
} catch (err) {
|
||
throw new Error('卖出失败: ' + err.message);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}, [account, user, refreshAccount]);
|
||
|
||
// 撤销订单
|
||
const cancelOrder = useCallback(async (orderId) => {
|
||
setIsLoading(true);
|
||
try {
|
||
const response = await apiRequest(`/api/simulation/cancel-order/${orderId}`, {
|
||
method: 'POST'
|
||
});
|
||
|
||
// 刷新交易历史
|
||
const ordersResponse = await apiRequest('/api/simulation/orders?limit=100');
|
||
setTradingHistory(mapOrderData(ordersResponse.data || []));
|
||
|
||
return { success: true };
|
||
} catch (err) {
|
||
throw new Error('撤单失败: ' + err.message);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
// 获取交易记录
|
||
const getTransactions = useCallback(async (options = {}) => {
|
||
try {
|
||
const params = new URLSearchParams();
|
||
if (options.limit) params.append('limit', options.limit);
|
||
if (options.date) params.append('date', options.date);
|
||
|
||
const response = await apiRequest(`/api/simulation/transactions?${params.toString()}`);
|
||
return response.data || [];
|
||
} catch (error) {
|
||
logger.error('useTradingAccount', 'getTransactions', error, options);
|
||
return [];
|
||
}
|
||
}, []);
|
||
|
||
// 获取资产历史
|
||
const getAssetHistory = useCallback(async (days = 30) => {
|
||
// 调试模式:demo用户返回模拟数据,避免CORS
|
||
if (!user || user.id === 'demo') {
|
||
const now = Date.now();
|
||
const data = Array.from({ length: days }, (_, i) => {
|
||
const date = new Date(now - (days - 1 - i) * 24 * 3600 * 1000);
|
||
// 简单生成一条平滑的收益曲线
|
||
const value = Math.sin(i / 5) * 0.01 + 0.001 * i;
|
||
return { date: date.toISOString().slice(0, 10), value };
|
||
});
|
||
return data;
|
||
}
|
||
|
||
try {
|
||
const response = await apiRequest(`/api/simulation/statistics?days=${days}`);
|
||
return response.data?.daily_returns || [];
|
||
} catch (error) {
|
||
logger.error('useTradingAccount', 'getAssetHistory', error, { days, userId: user?.id });
|
||
return [];
|
||
}
|
||
}, [user]);
|
||
|
||
// 获取股票实时行情(如果需要的话)
|
||
const getStockQuotes = useCallback(async (stockCodes) => {
|
||
try {
|
||
// 这里可以调用自选股实时行情接口
|
||
const response = await apiRequest('/api/account/watchlist/quotes');
|
||
if (response.success) {
|
||
const quotes = {};
|
||
response.data.forEach(item => {
|
||
quotes[item.stock_code] = {
|
||
name: item.stock_name,
|
||
price: item.current_price,
|
||
change: item.change,
|
||
changePercent: item.change_percent
|
||
};
|
||
});
|
||
setStockQuotes(quotes);
|
||
return quotes;
|
||
}
|
||
return {};
|
||
} catch (error) {
|
||
logger.error('useTradingAccount', 'getStockQuotes', error, { stockCodes });
|
||
return {};
|
||
}
|
||
}, []);
|
||
|
||
return {
|
||
account,
|
||
positions,
|
||
tradingHistory,
|
||
isLoading,
|
||
error,
|
||
stockQuotes,
|
||
buyStock,
|
||
sellStock,
|
||
cancelOrder,
|
||
refreshAccount,
|
||
searchStocks,
|
||
getTransactions,
|
||
getAssetHistory,
|
||
getStockQuotes
|
||
};
|
||
} |