Initial commit
This commit is contained in:
383
src/views/TradingSimulation/hooks/useTradingAccount.js
Normal file
383
src/views/TradingSimulation/hooks/useTradingAccount.js
Normal file
@@ -0,0 +1,383 @@
|
||||
// src/views/TradingSimulation/hooks/useTradingAccount.js - 模拟盘账户管理 Hook
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useAuth } from '../../../contexts/AuthContext';
|
||||
|
||||
// API 基础URL - 修复HTTPS混合内容问题
|
||||
const API_BASE_URL = process.env.NODE_ENV === 'production'
|
||||
? '' // 生产环境使用相对路径(通过nginx代理)
|
||||
: (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
||||
|
||||
// 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') {
|
||||
console.log('🔧 调试模式:模拟股票搜索', 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) {
|
||||
console.error('搜索股票失败:', error);
|
||||
return [];
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// 刷新账户数据
|
||||
const refreshAccount = useCallback(async () => {
|
||||
// 调试模式:使用模拟数据(因为后端API可能有CORS问题)
|
||||
if (!user || user.id === 'demo') {
|
||||
console.log('🔧 调试模式:使用模拟账户数据');
|
||||
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) {
|
||||
console.error('刷新账户数据失败:', err);
|
||||
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') {
|
||||
console.log('🔧 调试模式:模拟买入', { 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') {
|
||||
console.log('🔧 调试模式:模拟卖出', { 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) {
|
||||
console.error('获取交易记录失败:', error);
|
||||
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) {
|
||||
console.error('获取资产历史失败:', error);
|
||||
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) {
|
||||
console.error('获取股票行情失败:', error);
|
||||
return {};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
account,
|
||||
positions,
|
||||
tradingHistory,
|
||||
isLoading,
|
||||
error,
|
||||
stockQuotes,
|
||||
buyStock,
|
||||
sellStock,
|
||||
cancelOrder,
|
||||
refreshAccount,
|
||||
searchStocks,
|
||||
getTransactions,
|
||||
getAssetHistory,
|
||||
getStockQuotes
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user