feat: Notion 거래 이력 저장 모듈 구현
Made-with: Cursor
This commit is contained in:
72
src/database.py
Normal file
72
src/database.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from notion_client import Client
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class TradeRepository:
|
||||
"""Notion 데이터베이스에 거래 이력을 저장하는 레포지토리."""
|
||||
|
||||
def __init__(self, token: str, database_id: str):
|
||||
self.client = Client(auth=token)
|
||||
self.database_id = database_id
|
||||
|
||||
def save_trade(
|
||||
self,
|
||||
symbol: str,
|
||||
side: str,
|
||||
entry_price: float,
|
||||
quantity: float,
|
||||
leverage: int,
|
||||
signal_data: dict = None,
|
||||
) -> dict:
|
||||
properties = {
|
||||
"Symbol": {"title": [{"text": {"content": symbol}}]},
|
||||
"Side": {"select": {"name": side}},
|
||||
"Entry Price": {"number": entry_price},
|
||||
"Quantity": {"number": quantity},
|
||||
"Leverage": {"number": leverage},
|
||||
"Status": {"select": {"name": "OPEN"}},
|
||||
"Signal Data": {
|
||||
"rich_text": [
|
||||
{"text": {"content": json.dumps(signal_data or {}, ensure_ascii=False)}}
|
||||
]
|
||||
},
|
||||
"Opened At": {
|
||||
"date": {"start": datetime.now(timezone.utc).isoformat()}
|
||||
},
|
||||
}
|
||||
result = self.client.pages.create(
|
||||
parent={"database_id": self.database_id},
|
||||
properties=properties,
|
||||
)
|
||||
logger.info(f"거래 저장: {result['id']}")
|
||||
return result
|
||||
|
||||
def close_trade(self, trade_id: str, exit_price: float, pnl: float) -> dict:
|
||||
properties = {
|
||||
"Exit Price": {"number": exit_price},
|
||||
"PnL": {"number": pnl},
|
||||
"Status": {"select": {"name": "CLOSED"}},
|
||||
"Closed At": {
|
||||
"date": {"start": datetime.now(timezone.utc).isoformat()}
|
||||
},
|
||||
}
|
||||
result = self.client.pages.update(
|
||||
page_id=trade_id,
|
||||
properties=properties,
|
||||
)
|
||||
logger.info(f"거래 종료: {trade_id}, PnL: {pnl:.4f}")
|
||||
return result
|
||||
|
||||
def get_open_trades(self, symbol: str) -> list[dict]:
|
||||
response = self.client.databases.query(
|
||||
database_id=self.database_id,
|
||||
filter={
|
||||
"and": [
|
||||
{"property": "Symbol", "title": {"equals": symbol}},
|
||||
{"property": "Status", "select": {"equals": "OPEN"}},
|
||||
]
|
||||
},
|
||||
)
|
||||
return response.get("results", [])
|
||||
42
tests/test_database.py
Normal file
42
tests/test_database.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from src.database import TradeRepository
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_repo():
|
||||
with patch("src.database.Client") as mock_client_cls:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
repo = TradeRepository(token="secret_test", database_id="db_test")
|
||||
repo.client = mock_client
|
||||
yield repo
|
||||
|
||||
|
||||
def test_save_trade(mock_repo):
|
||||
mock_repo.client.pages.create.return_value = {
|
||||
"id": "abc123",
|
||||
"properties": {},
|
||||
}
|
||||
result = mock_repo.save_trade(
|
||||
symbol="XRPUSDT",
|
||||
side="LONG",
|
||||
entry_price=0.5,
|
||||
quantity=400.0,
|
||||
leverage=10,
|
||||
signal_data={"rsi": 32, "macd_hist": 0.001},
|
||||
)
|
||||
assert result["id"] == "abc123"
|
||||
|
||||
|
||||
def test_close_trade(mock_repo):
|
||||
mock_repo.client.pages.update.return_value = {
|
||||
"id": "abc123",
|
||||
"properties": {
|
||||
"Status": {"select": {"name": "CLOSED"}},
|
||||
},
|
||||
}
|
||||
result = mock_repo.close_trade(
|
||||
trade_id="abc123", exit_price=0.55, pnl=20.0
|
||||
)
|
||||
assert result["id"] == "abc123"
|
||||
Reference in New Issue
Block a user