feat: 优化界面并补充网格成交分析
This commit is contained in:
@@ -3,8 +3,13 @@ from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from grid_trading.domain.calculations import CalculationError, calculate_positions, estimate_fees
|
||||
from grid_trading.domain.models import FeeRules, Instrument, QuoteSnapshot, Trade, TradeGroup, TradeSide
|
||||
from grid_trading.domain.calculations import (
|
||||
CalculationError,
|
||||
calculate_account_summary,
|
||||
calculate_positions,
|
||||
estimate_fees,
|
||||
)
|
||||
from grid_trading.domain.models import Account, FeeRules, Instrument, QuoteSnapshot, Trade, TradeGroup, TradeSide
|
||||
|
||||
|
||||
def make_trade(
|
||||
@@ -175,6 +180,39 @@ def test_missing_quote_does_not_use_manual_or_last_trade_price_for_current_price
|
||||
assert summary.floating_pnl is None
|
||||
|
||||
|
||||
def test_account_summary_caps_capital_usage_when_cash_is_negative():
|
||||
today = date(2026, 7, 8)
|
||||
account = Account(id=1, name="主账户", initial_cash=Decimal("100"))
|
||||
instrument = Instrument(id=1, code="600588", name="用友网络")
|
||||
trades = [
|
||||
make_trade(
|
||||
trade_id=1,
|
||||
trade_date=today,
|
||||
side=TradeSide.BUY,
|
||||
price="1.50",
|
||||
quantity=100,
|
||||
trade_group=TradeGroup.BASE,
|
||||
)
|
||||
]
|
||||
quotes = {
|
||||
1: QuoteSnapshot(
|
||||
symbol="sh600588",
|
||||
code="600588",
|
||||
name="用友网络",
|
||||
price=Decimal("1.00"),
|
||||
source="tencent",
|
||||
)
|
||||
}
|
||||
|
||||
positions = calculate_positions([instrument], trades, as_of=today, quote_snapshots=quotes)
|
||||
summary = calculate_account_summary(account, positions, [], trades)
|
||||
|
||||
assert summary.cash == Decimal("-50.00")
|
||||
assert summary.market_value == Decimal("100.00")
|
||||
assert summary.total_assets == Decimal("50.00")
|
||||
assert summary.capital_usage_rate == Decimal("1.0000")
|
||||
|
||||
|
||||
def test_sell_more_than_group_position_raises():
|
||||
today = date(2026, 7, 8)
|
||||
instrument = Instrument(id=1, code="159915", name="创业板ETF")
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from grid_trading.domain.models import Trade, TradeGroup, TradeSide
|
||||
from grid_trading.domain.open_grid_lots import calculate_open_grid_lots
|
||||
from grid_trading.domain.open_grid_lots import calculate_grid_trade_matches, calculate_open_grid_lots
|
||||
|
||||
|
||||
def make_trade(
|
||||
@@ -80,6 +80,52 @@ def test_calculate_open_grid_lots_fifo_matches_grid_sells_against_grid_buys():
|
||||
assert lot.status == "可卖"
|
||||
|
||||
|
||||
def test_calculate_grid_trade_matches_splits_sell_against_fifo_buy_lots():
|
||||
today = date(2026, 7, 9)
|
||||
trades = [
|
||||
make_trade(
|
||||
trade_id=1,
|
||||
trade_date=today - timedelta(days=3),
|
||||
side=TradeSide.BUY,
|
||||
price="10.00",
|
||||
quantity=300,
|
||||
),
|
||||
make_trade(
|
||||
trade_id=2,
|
||||
trade_date=today - timedelta(days=2),
|
||||
side=TradeSide.BUY,
|
||||
price="9.50",
|
||||
quantity=200,
|
||||
),
|
||||
make_trade(
|
||||
trade_id=3,
|
||||
trade_date=today - timedelta(days=1),
|
||||
side=TradeSide.SELL,
|
||||
price="10.30",
|
||||
quantity=350,
|
||||
),
|
||||
]
|
||||
|
||||
matches = calculate_grid_trade_matches(trades, as_of=today)
|
||||
|
||||
assert len(matches) == 2
|
||||
assert matches[0].sell_trade_id == 3
|
||||
assert matches[0].sell_date == today - timedelta(days=1)
|
||||
assert matches[0].sell_price == Decimal("10.30")
|
||||
assert matches[0].buy_trade_id == 1
|
||||
assert matches[0].buy_date == today - timedelta(days=3)
|
||||
assert matches[0].buy_price == Decimal("10.00")
|
||||
assert matches[0].matched_quantity == 300
|
||||
assert matches[0].buy_amount == Decimal("3000.00")
|
||||
assert matches[0].sell_amount == Decimal("3090.00")
|
||||
assert matches[0].gross_profit == Decimal("90.00")
|
||||
assert matches[1].buy_trade_id == 2
|
||||
assert matches[1].matched_quantity == 50
|
||||
assert matches[1].buy_amount == Decimal("475.00")
|
||||
assert matches[1].sell_amount == Decimal("515.00")
|
||||
assert matches[1].gross_profit == Decimal("40.00")
|
||||
|
||||
|
||||
def test_calculate_open_grid_lots_reports_not_reached_and_missing_quote_statuses():
|
||||
today = date(2026, 7, 9)
|
||||
trades = [
|
||||
|
||||
@@ -298,3 +298,55 @@ def test_service_returns_open_grid_lots_with_suggested_sell_price(tmp_path):
|
||||
assert lot.suggested_sell_price == Decimal("4.12")
|
||||
assert lot.current_price == Decimal("4.12")
|
||||
assert lot.status == "可卖"
|
||||
|
||||
|
||||
def test_service_returns_grid_trade_matches(tmp_path):
|
||||
service = TradingService(tmp_path / "grid.db")
|
||||
service.ensure_defaults()
|
||||
account = service.get_active_account()
|
||||
instrument = service.add_instrument(Instrument(id=None, code="510300", name="沪深300ETF", market="ETF"))
|
||||
service.save_trade(
|
||||
Trade(
|
||||
id=None,
|
||||
account_id=account.id,
|
||||
instrument_id=instrument.id,
|
||||
trade_date=date(2026, 7, 6),
|
||||
side=TradeSide.BUY,
|
||||
price=Decimal("10.00"),
|
||||
quantity=300,
|
||||
trade_group=TradeGroup.GRID,
|
||||
)
|
||||
)
|
||||
service.save_trade(
|
||||
Trade(
|
||||
id=None,
|
||||
account_id=account.id,
|
||||
instrument_id=instrument.id,
|
||||
trade_date=date(2026, 7, 7),
|
||||
side=TradeSide.BUY,
|
||||
price=Decimal("9.50"),
|
||||
quantity=200,
|
||||
trade_group=TradeGroup.GRID,
|
||||
)
|
||||
)
|
||||
service.save_trade(
|
||||
Trade(
|
||||
id=None,
|
||||
account_id=account.id,
|
||||
instrument_id=instrument.id,
|
||||
trade_date=date(2026, 7, 8),
|
||||
side=TradeSide.SELL,
|
||||
price=Decimal("10.30"),
|
||||
quantity=400,
|
||||
trade_group=TradeGroup.GRID,
|
||||
)
|
||||
)
|
||||
|
||||
matches = service.get_grid_trade_matches(instrument.id, as_of=date(2026, 7, 9))
|
||||
|
||||
assert len(matches) == 2
|
||||
assert matches[0].buy_trade_id is not None
|
||||
assert matches[0].matched_quantity == 300
|
||||
assert matches[0].gross_profit == Decimal("90.00")
|
||||
assert matches[1].matched_quantity == 100
|
||||
assert matches[1].gross_profit == Decimal("80.00")
|
||||
|
||||
@@ -17,7 +17,7 @@ def test_formatters_render_money_percent_and_empty_values():
|
||||
def test_main_window_can_be_constructed_offscreen(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QPushButton
|
||||
from PySide6.QtWidgets import QApplication, QLabel, QPushButton
|
||||
|
||||
from grid_trading.services.trading_service import TradingService
|
||||
from grid_trading.ui.main_window import MainWindow
|
||||
@@ -29,6 +29,9 @@ def test_main_window_can_be_constructed_offscreen(tmp_path, monkeypatch):
|
||||
assert window.windowTitle() == "Grid Trading Manager"
|
||||
assert window.holdings_table.columnCount() > 0
|
||||
assert any(button.text() == "刷新行情" for button in window.findChildren(QPushButton))
|
||||
summary_titles = [label.text() for label in window.findChildren(QLabel)]
|
||||
assert "账户权益" in summary_titles
|
||||
assert "总资产" not in summary_titles
|
||||
|
||||
window.close()
|
||||
service.close()
|
||||
@@ -49,7 +52,7 @@ def test_main_window_contains_grid_level_table(tmp_path, monkeypatch):
|
||||
|
||||
assert window.grid_levels_table.columnCount() == 7
|
||||
tab_titles = _tab_titles(window.details_tabs)
|
||||
assert tab_titles == ["网格档位", "待卖网格", "最近成交"]
|
||||
assert tab_titles == ["网格档位", "待卖网格", "配对明细", "最近成交"]
|
||||
assert isinstance(window.details_tabs, QTabWidget)
|
||||
|
||||
window.close()
|
||||
@@ -71,6 +74,82 @@ def test_main_window_contains_open_grid_lots_table(tmp_path, monkeypatch):
|
||||
|
||||
assert window.open_grid_lots_table.columnCount() == 8
|
||||
assert "待卖网格" in _tab_titles(window.details_tabs)
|
||||
assert window.grid_matches_table.columnCount() == 9
|
||||
assert "配对明细" in _tab_titles(window.details_tabs)
|
||||
|
||||
window.close()
|
||||
service.close()
|
||||
app.processEvents()
|
||||
|
||||
|
||||
def test_main_window_applies_light_theme(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from grid_trading.services.trading_service import TradingService
|
||||
from grid_trading.ui.main_window import MainWindow
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
service = TradingService(tmp_path / "grid.db")
|
||||
window = MainWindow(service)
|
||||
|
||||
assert window.centralWidget().objectName() == "appRoot"
|
||||
assert window.holdings_table.alternatingRowColors()
|
||||
assert "QMainWindow" in window.styleSheet()
|
||||
|
||||
window.close()
|
||||
service.close()
|
||||
app.processEvents()
|
||||
|
||||
|
||||
def test_profit_and_loss_cells_use_a_share_colors(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from grid_trading.domain.models import PositionSummary
|
||||
from grid_trading.services.trading_service import TradingService
|
||||
from grid_trading.ui.main_window import MainWindow
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
service = TradingService(tmp_path / "grid.db")
|
||||
window = MainWindow(service)
|
||||
window._positions = [
|
||||
PositionSummary(
|
||||
instrument_id=1,
|
||||
code="510300",
|
||||
name="沪深300ETF",
|
||||
market="ETF",
|
||||
current_price=Decimal("10.50"),
|
||||
price_source="quote",
|
||||
total_quantity=100,
|
||||
available_quantity=100,
|
||||
base_quantity=0,
|
||||
grid_quantity=100,
|
||||
other_quantity=0,
|
||||
remaining_cost=Decimal("1000"),
|
||||
cost_price=Decimal("10"),
|
||||
position_breakeven_price=Decimal("10"),
|
||||
account_breakeven_price=Decimal("10"),
|
||||
realized_pnl=Decimal("12.34"),
|
||||
grid_profit=Decimal("-5.67"),
|
||||
floating_pnl=Decimal("50"),
|
||||
market_value=Decimal("1050"),
|
||||
)
|
||||
]
|
||||
|
||||
window.holdings_table.blockSignals(True)
|
||||
window._fill_holdings_table()
|
||||
window.holdings_table.blockSignals(False)
|
||||
|
||||
profit_item = window.holdings_table.item(0, 10)
|
||||
loss_item = window.holdings_table.item(0, 11)
|
||||
assert profit_item.foreground().color() == QColor("#c62828")
|
||||
assert profit_item.font().bold()
|
||||
assert loss_item.foreground().color() == QColor("#2e7d32")
|
||||
assert loss_item.font().bold()
|
||||
|
||||
window.close()
|
||||
service.close()
|
||||
|
||||
Reference in New Issue
Block a user