Coverage for api/v1/endpoints/assets.py: 40.68%

177 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-13 18:58 +0000

1""" 

2资产相关 API 端点(修复版本) 

3""" 

4 

5from decimal import Decimal 

6from typing import List, Optional 

7 

8from fastapi import APIRouter, Depends, HTTPException, Query, status 

9from fastapi.responses import JSONResponse 

10from pydantic import BaseModel 

11 

12from core.models.asset import (AssetOverview, AssetType, CurrencyType, 

13 MarketType, SimulatedAssetCreate, 

14 SimulatedAssetOverview, SimulatedAssetResponse, 

15 SimulatedAssetUpdate, SimulatedPositionCreate, 

16 SimulatedPositionResponse, 

17 SimulatedPositionUpdate, SyncRequest, 

18 SyncResponse, UserAssetCreate, 

19 UserAssetResponse, UserAssetUpdate, 

20 UserPositionCreate, UserPositionResponse, 

21 UserPositionUpdate) 

22 

23 

24# 前端输入模型(不需要user_id和asset_type) 

25class SimulatedPositionInput(BaseModel): 

26 symbol: str 

27 symbol_name: str 

28 quantity: Decimal 

29 cost_price: Decimal 

30 current_price: Optional[Decimal] = None 

31 market: MarketType 

32 currency: CurrencyType 

33 

34 

35from core.data_source.adapters.asset_adapter import AssetAdapter 

36from core.data_source.adapters.data_adapter import DataAdapter 

37from core.middleware.auth_middleware import get_current_user 

38from core.models.user import UserResponse 

39from core.repositories.asset_repository import AssetRepository 

40from core.repositories.stock_repository import StockRepository 

41 

42router = APIRouter(prefix="/assets", tags=["assets"]) 

43 

44 

45# 用户资产相关端点 

46@router.post("/", response_model=UserAssetResponse, status_code=status.HTTP_201_CREATED) 

47def create_user_asset( 

48 asset: UserAssetCreate, current_user: UserResponse = Depends(get_current_user) 

49): 

50 """创建用户资产(使用新的统一架构)""" 

51 try: 

52 adapter = AssetAdapter(current_user.id) 

53 return adapter.create_user_asset(asset) 

54 except Exception as e: 

55 raise HTTPException( 

56 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

57 detail=f"创建用户资产失败: {str(e)}", 

58 ) 

59 

60 

61@router.get("/", response_model=Optional[UserAssetResponse]) 

62def get_user_asset(current_user: UserResponse = Depends(get_current_user)): 

63 """获取用户资产(使用新的统一架构)""" 

64 try: 

65 adapter = AssetAdapter(current_user.id) 

66 return adapter.get_user_asset(current_user.id) 

67 except Exception as e: 

68 raise HTTPException( 

69 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

70 detail=f"获取用户资产失败: {str(e)}", 

71 ) 

72 

73 

74@router.put("/", response_model=Optional[UserAssetResponse]) 

75def update_user_asset( 

76 asset_update: UserAssetUpdate, 

77 current_user: UserResponse = Depends(get_current_user), 

78): 

79 """更新用户资产(使用新的统一架构)""" 

80 try: 

81 adapter = AssetAdapter(current_user.id) 

82 return adapter.update_user_asset(current_user.id, asset_update) 

83 except Exception as e: 

84 raise HTTPException( 

85 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

86 detail=f"更新用户资产失败: {str(e)}", 

87 ) 

88 

89 

90@router.delete("/", status_code=status.HTTP_204_NO_CONTENT) 

91def delete_user_asset(current_user: UserResponse = Depends(get_current_user)): 

92 """删除用户资产(使用新的统一架构)""" 

93 try: 

94 adapter = AssetAdapter(current_user.id) 

95 success = adapter.delete_user_asset(current_user.id) 

96 if not success: 

97 raise HTTPException( 

98 status_code=status.HTTP_404_NOT_FOUND, detail="用户资产不存在" 

99 ) 

100 except Exception as e: 

101 raise HTTPException( 

102 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

103 detail=f"删除用户资产失败: {str(e)}", 

104 ) 

105 

106 

107# 用户持仓相关端点 

108@router.post( 

109 "/positions", 

110 response_model=UserPositionResponse, 

111 status_code=status.HTTP_201_CREATED, 

112) 

113def create_user_position( 

114 position: UserPositionCreate, current_user: UserResponse = Depends(get_current_user) 

115): 

116 """创建用户持仓(使用新的统一架构)""" 

117 try: 

118 adapter = AssetAdapter(current_user.id) 

119 return adapter.create_position(position) 

120 except Exception as e: 

121 raise HTTPException( 

122 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

123 detail=f"创建用户持仓失败: {str(e)}", 

124 ) 

125 

126 

127@router.get("/positions", response_model=List[UserPositionResponse]) 

128def get_user_positions(current_user: UserResponse = Depends(get_current_user)): 

129 """获取用户持仓列表(使用新的统一架构)""" 

130 try: 

131 adapter = AssetAdapter(current_user.id) 

132 return adapter.get_positions() 

133 except Exception as e: 

134 raise HTTPException( 

135 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

136 detail=f"获取用户持仓失败: {str(e)}", 

137 ) 

138 

139 

140@router.put("/positions/{position_id}", response_model=Optional[UserPositionResponse]) 

141def update_user_position( 

142 position_id: str, 

143 position_update: UserPositionUpdate, 

144 current_user: UserResponse = Depends(get_current_user), 

145): 

146 """更新用户持仓(使用新的统一架构)""" 

147 try: 

148 adapter = AssetAdapter(current_user.id) 

149 return adapter.update_position(position_id, position_update) 

150 except Exception as e: 

151 raise HTTPException( 

152 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

153 detail=f"更新用户持仓失败: {str(e)}", 

154 ) 

155 

156 

157@router.delete("/positions/{position_id}", status_code=status.HTTP_204_NO_CONTENT) 

158def delete_user_position( 

159 position_id: str, current_user: UserResponse = Depends(get_current_user) 

160): 

161 """删除用户持仓(使用新的统一架构)""" 

162 try: 

163 adapter = AssetAdapter(current_user.id) 

164 success = adapter.delete_position(position_id) 

165 if not success: 

166 raise HTTPException( 

167 status_code=status.HTTP_404_NOT_FOUND, detail="用户持仓不存在" 

168 ) 

169 except Exception as e: 

170 raise HTTPException( 

171 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

172 detail=f"删除用户持仓失败: {str(e)}", 

173 ) 

174 

175 

176# 资产概览端点 

177@router.get("/overview", response_model=Optional[AssetOverview]) 

178def get_asset_overview( 

179 force_refresh: bool = Query(False, description="是否强制刷新数据"), 

180 current_user: UserResponse = Depends(get_current_user), 

181): 

182 """获取资产概览(使用新的统一架构)""" 

183 try: 

184 # 使用新的业务适配器 

185 adapter = AssetAdapter(current_user.id) 

186 return adapter.get_or_create_asset_overview(force_refresh=force_refresh) 

187 except Exception as e: 

188 raise HTTPException( 

189 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

190 detail=f"获取资产概览失败: {str(e)}", 

191 ) 

192 

193 

194# 模拟资产相关端点 

195@router.post( 

196 "/simulated/", 

197 response_model=SimulatedAssetResponse, 

198 status_code=status.HTTP_201_CREATED, 

199) 

200def create_simulated_asset( 

201 asset: SimulatedAssetCreate, current_user: UserResponse = Depends(get_current_user) 

202): 

203 """创建模拟资产(使用新的统一架构)""" 

204 try: 

205 adapter = AssetAdapter(current_user.id) 

206 return adapter.create_simulated_asset(asset) 

207 except Exception as e: 

208 raise HTTPException( 

209 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

210 detail=f"创建模拟资产失败: {str(e)}", 

211 ) 

212 

213 

214@router.get("/simulated/", response_model=Optional[SimulatedAssetResponse]) 

215def get_simulated_asset(current_user: UserResponse = Depends(get_current_user)): 

216 """获取模拟资产(使用新的统一架构)""" 

217 try: 

218 adapter = AssetAdapter(current_user.id) 

219 return adapter.get_simulated_asset() 

220 except Exception as e: 

221 raise HTTPException( 

222 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

223 detail=f"获取模拟资产失败: {str(e)}", 

224 ) 

225 

226 

227@router.put("/simulated/", response_model=Optional[SimulatedAssetResponse]) 

228def update_simulated_asset( 

229 asset_update: SimulatedAssetUpdate, 

230 current_user: UserResponse = Depends(get_current_user), 

231 # asset_service: AssetService = Depends() # 已经迁移到BusinessAssetAdapter 

232): 

233 """更新模拟资产""" 

234 try: 

235 adapter = AssetAdapter(current_user.id) 

236 return adapter.update_simulated_asset(current_user.id, asset_update) 

237 except Exception as e: 

238 raise HTTPException( 

239 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

240 detail=f"更新模拟资产失败: {str(e)}", 

241 ) 

242 

243 

244# 模拟持仓相关端点 

245@router.post( 

246 "/simulated/positions", 

247 response_model=SimulatedPositionResponse, 

248 status_code=status.HTTP_201_CREATED, 

249) 

250def create_simulated_position( 

251 position_input: SimulatedPositionInput, 

252 current_user: UserResponse = Depends(get_current_user), 

253 # asset_service: AssetService = Depends() # 已经迁移到BusinessAssetAdapter 

254): 

255 """创建模拟持仓""" 

256 try: 

257 # 从输入构建完整的持仓对象 

258 position = SimulatedPositionCreate( 

259 user_id=current_user.id, 

260 symbol=position_input.symbol, 

261 symbol_name=position_input.symbol_name, 

262 asset_type=AssetType.STOCK, # 默认设置为股票类型 

263 quantity=position_input.quantity, 

264 cost_price=position_input.cost_price, 

265 current_price=position_input.current_price, 

266 market=position_input.market, 

267 currency=position_input.currency, 

268 ) 

269 adapter = AssetAdapter(current_user.id) 

270 return adapter.create_simulated_position(position) 

271 except Exception as e: 

272 raise HTTPException( 

273 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

274 detail=f"创建模拟持仓失败: {str(e)}", 

275 ) 

276 

277 

278@router.get("/simulated/positions", response_model=List[SimulatedPositionResponse]) 

279def get_simulated_positions( 

280 current_user: UserResponse = Depends(get_current_user), 

281 # asset_service: AssetService = Depends() # 已经迁移到BusinessAssetAdapter 

282): 

283 """获取模拟持仓列表""" 

284 try: 

285 adapter = AssetAdapter(current_user.id) 

286 return adapter.get_simulated_positions(current_user.id) 

287 except Exception as e: 

288 raise HTTPException( 

289 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

290 detail=f"获取模拟持仓失败: {str(e)}", 

291 ) 

292 

293 

294@router.put( 

295 "/simulated/positions/{position_id}", 

296 response_model=Optional[SimulatedPositionResponse], 

297) 

298def update_simulated_position( 

299 position_id: str, 

300 position_update: SimulatedPositionUpdate, 

301 current_user: UserResponse = Depends(get_current_user), 

302 # asset_service: AssetService = Depends() # 已经迁移到BusinessAssetAdapter 

303): 

304 """更新模拟持仓""" 

305 try: 

306 adapter = AssetAdapter(current_user.id) 

307 return adapter.update_simulated_position(position_id, position_update) 

308 except Exception as e: 

309 raise HTTPException( 

310 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

311 detail=f"更新模拟持仓失败: {str(e)}", 

312 ) 

313 

314 

315@router.delete( 

316 "/simulated/positions/{position_id}", status_code=status.HTTP_204_NO_CONTENT 

317) 

318def delete_simulated_position( 

319 position_id: str, 

320 current_user: UserResponse = Depends(get_current_user), 

321 # asset_service: AssetService = Depends() # 已经迁移到BusinessAssetAdapter 

322): 

323 """删除模拟持仓""" 

324 try: 

325 adapter = AssetAdapter(current_user.id) 

326 success = adapter.delete_simulated_position(position_id) 

327 if not success: 

328 raise HTTPException( 

329 status_code=status.HTTP_404_NOT_FOUND, detail="模拟持仓不存在" 

330 ) 

331 except Exception as e: 

332 raise HTTPException( 

333 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

334 detail=f"删除模拟持仓失败: {str(e)}", 

335 ) 

336 

337 

338# 模拟资产概览端点 

339@router.get("/simulated/overview", response_model=Optional[SimulatedAssetOverview]) 

340def get_simulated_asset_overview( 

341 current_user: UserResponse = Depends(get_current_user), 

342) -> Optional[SimulatedAssetOverview]: 

343 """获取模拟资产概览(使用新的统一架构)""" 

344 try: 

345 # 获取模拟持仓数据 

346 simulated_positions = AssetAdapter(current_user.id).get_simulated_positions( 

347 current_user.id 

348 ) 

349 

350 # 从repository获取模拟资产概览 

351 asset_repo = AssetRepository() 

352 overview = asset_repo.get_simulated_asset_overview(current_user.id) 

353 

354 if not overview: 

355 # 如果没有概览,创建默认的 

356 overview = SimulatedAssetOverview( 

357 positions=simulated_positions, 

358 positions_by_currency={}, 

359 total_assets_by_currency={}, 

360 total_assets=Decimal("0"), 

361 cash_assets=Decimal("0"), 

362 position_assets=Decimal("0"), 

363 today_pnl=Decimal("0"), 

364 ) 

365 

366 return overview 

367 

368 except Exception as e: 

369 raise HTTPException( 

370 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

371 detail=f"获取模拟资产概览失败: {str(e)}", 

372 ) 

373 

374 

375# 同步相关端点 

376@router.post("/sync", response_model=SyncResponse) 

377def sync_from_broker( 

378 sync_request: SyncRequest, 

379 current_user: UserResponse = Depends(get_current_user), 

380 # asset_service: AssetService = Depends() # 已经迁移到BusinessAssetAdapter 

381): 

382 """从券商同步资产数据""" 

383 try: 

384 adapter = AssetAdapter(current_user.id) 

385 success = adapter.sync_from_broker(current_user.id) 

386 return SyncResponse( 

387 success=success, message="同步成功" if success else "同步失败" 

388 ) 

389 except Exception as e: 

390 raise HTTPException( 

391 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

392 detail=f"同步资产数据失败: {str(e)}", 

393 ) 

394 

395 

396@router.post("/sync-to-simulated", response_model=SyncResponse) 

397def sync_to_simulated( 

398 current_user: UserResponse = Depends(get_current_user), 

399 # asset_service: AssetService = Depends() # 已经迁移到BusinessAssetAdapter 

400): 

401 """同步真实资产到模拟资产""" 

402 try: 

403 adapter = AssetAdapter(current_user.id) 

404 success = adapter.sync_to_simulated(current_user.id) 

405 return SyncResponse( 

406 success=success, message="同步成功" if success else "同步失败" 

407 ) 

408 except Exception as e: 

409 raise HTTPException( 

410 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

411 detail=f"同步到模拟资产失败: {str(e)}", 

412 ) 

413 

414 

415@router.post("/sync-from-longport", response_model=SyncResponse) 

416def sync_from_longport(current_user: UserResponse = Depends(get_current_user)): 

417 """从长桥API手动同步真实资产数据(使用新的统一架构)""" 

418 try: 

419 # 使用新的业务适配器 

420 adapter = AssetAdapter(current_user.id) 

421 result = adapter.sync_from_longport(force_refresh=True) 

422 

423 if result: 

424 return SyncResponse(success=True, message="从长桥API同步成功") 

425 else: 

426 return SyncResponse(success=False, message="从长桥API同步失败") 

427 except Exception as e: 

428 raise HTTPException( 

429 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

430 detail=f"从长桥API同步失败: {str(e)}", 

431 )