Files
vf_react/src/views/TradingSimulation/hooks/useTradingAccount.js
2025-10-20 21:25:33 +08:00

383 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
};
}