feat: add structured manual trading journal
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from database import ReviewDatabase
|
||||
from trade_journal import TradeJournalService
|
||||
|
||||
|
||||
class TradeJournalTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.database = ReviewDatabase(Path(self.temp.name) / "review.db")
|
||||
self.owner = self.database.create_user("trade_owner", "salt", "hash")
|
||||
self.other = self.database.create_user("trade_other", "salt", "hash")
|
||||
self.service = TradeJournalService(self.database)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temp.cleanup()
|
||||
|
||||
@staticmethod
|
||||
def payload(**overrides):
|
||||
data = {
|
||||
"trade_date": "2026-07-22",
|
||||
"code": "002141",
|
||||
"name": "贤丰控股",
|
||||
"action": "buy",
|
||||
"price": 10.25,
|
||||
"quantity": 1000,
|
||||
"position_pct": 20,
|
||||
"pnl_amount": "",
|
||||
"pnl_pct": "",
|
||||
"thesis": "修复期低吸",
|
||||
"execution": "按计划成交",
|
||||
"emotion": "calm",
|
||||
"tags": "计划内,低吸",
|
||||
}
|
||||
data.update(overrides)
|
||||
return data
|
||||
|
||||
def test_unrealized_entry_does_not_enter_win_rate(self):
|
||||
self.service.save(self.owner["id"], self.payload())
|
||||
result = self.service.list_entries(self.owner["id"])
|
||||
|
||||
self.assertEqual(result["summary"]["total"], 1)
|
||||
self.assertEqual(result["summary"]["realized"], 0)
|
||||
self.assertIsNone(result["summary"]["win_rate"])
|
||||
self.assertEqual(result["items"][0]["tags"], ["计划内", "低吸"])
|
||||
|
||||
def test_realized_entries_build_summary(self):
|
||||
self.service.save(
|
||||
self.owner["id"], self.payload(action="sell", pnl_amount=500, pnl_pct=5)
|
||||
)
|
||||
self.service.save(
|
||||
self.owner["id"],
|
||||
self.payload(code="600000", name="浦发银行", action="sell", pnl_amount=-200, pnl_pct=-2, position_pct=40),
|
||||
)
|
||||
result = self.service.list_entries(self.owner["id"])
|
||||
|
||||
self.assertEqual(result["summary"]["realized"], 2)
|
||||
self.assertEqual(result["summary"]["win_rate"], 50.0)
|
||||
self.assertEqual(result["summary"]["pnl_amount"], 300.0)
|
||||
self.assertEqual(result["summary"]["average_position"], 30.0)
|
||||
|
||||
def test_update_and_delete_require_ownership(self):
|
||||
trade_id = self.service.save(self.owner["id"], self.payload())
|
||||
with self.assertRaises(ValueError):
|
||||
self.service.save(
|
||||
self.other["id"], self.payload(id=trade_id, thesis="越权修改")
|
||||
)
|
||||
self.assertFalse(self.database.delete_trade_entry(self.other["id"], trade_id))
|
||||
|
||||
self.service.save(
|
||||
self.owner["id"], self.payload(id=trade_id, thesis="复盘后修订")
|
||||
)
|
||||
self.assertEqual(
|
||||
self.service.list_entries(self.owner["id"])["items"][0]["thesis"],
|
||||
"复盘后修订",
|
||||
)
|
||||
self.assertTrue(self.database.delete_trade_entry(self.owner["id"], trade_id))
|
||||
self.assertEqual(self.service.list_entries(self.owner["id"])["summary"]["total"], 0)
|
||||
|
||||
def test_entries_are_isolated_between_accounts(self):
|
||||
self.service.save(self.owner["id"], self.payload())
|
||||
self.assertEqual(self.service.list_entries(self.other["id"])["items"], [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user