Merge pull request 'feat: EOD 집계 시장 필터 추가 (issue #86)' (#117) from feature/issue-86-eod-market-filter into main
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
Reviewed-on: #117
This commit was merged in pull request #117.
This commit is contained in:
@@ -18,16 +18,33 @@ class ContextAggregator:
|
|||||||
self.conn = conn
|
self.conn = conn
|
||||||
self.store = ContextStore(conn)
|
self.store = ContextStore(conn)
|
||||||
|
|
||||||
def aggregate_daily_from_trades(self, date: str | None = None) -> None:
|
def aggregate_daily_from_trades(
|
||||||
|
self, date: str | None = None, market: str | None = None
|
||||||
|
) -> None:
|
||||||
"""Aggregate L6 (daily) context from trades table.
|
"""Aggregate L6 (daily) context from trades table.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
date: Date in YYYY-MM-DD format. If None, uses today.
|
date: Date in YYYY-MM-DD format. If None, uses today.
|
||||||
|
market: Market code filter (e.g., "KR", "US"). If None, aggregates all markets.
|
||||||
"""
|
"""
|
||||||
if date is None:
|
if date is None:
|
||||||
date = datetime.now(UTC).date().isoformat()
|
date = datetime.now(UTC).date().isoformat()
|
||||||
|
|
||||||
# Calculate daily metrics from trades
|
if market is None:
|
||||||
|
cursor = self.conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT DISTINCT market
|
||||||
|
FROM trades
|
||||||
|
WHERE DATE(timestamp) = ?
|
||||||
|
""",
|
||||||
|
(date,),
|
||||||
|
)
|
||||||
|
markets = [row[0] for row in cursor.fetchall() if row[0]]
|
||||||
|
else:
|
||||||
|
markets = [market]
|
||||||
|
|
||||||
|
for market_code in markets:
|
||||||
|
# Calculate daily metrics from trades for the market
|
||||||
cursor = self.conn.execute(
|
cursor = self.conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
@@ -41,29 +58,43 @@ class ContextAggregator:
|
|||||||
SUM(CASE WHEN pnl > 0 THEN 1 ELSE 0 END) as wins,
|
SUM(CASE WHEN pnl > 0 THEN 1 ELSE 0 END) as wins,
|
||||||
SUM(CASE WHEN pnl < 0 THEN 1 ELSE 0 END) as losses
|
SUM(CASE WHEN pnl < 0 THEN 1 ELSE 0 END) as losses
|
||||||
FROM trades
|
FROM trades
|
||||||
WHERE DATE(timestamp) = ?
|
WHERE DATE(timestamp) = ? AND market = ?
|
||||||
""",
|
""",
|
||||||
(date,),
|
(date, market_code),
|
||||||
)
|
)
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
|
|
||||||
if row and row[0] > 0: # At least one trade
|
if row and row[0] > 0: # At least one trade
|
||||||
trade_count, buys, sells, holds, avg_conf, total_pnl, stocks, wins, losses = row
|
trade_count, buys, sells, holds, avg_conf, total_pnl, stocks, wins, losses = row
|
||||||
|
|
||||||
# Store daily metrics in L6
|
key_suffix = f"_{market_code}"
|
||||||
self.store.set_context(ContextLayer.L6_DAILY, date, "trade_count", trade_count)
|
|
||||||
self.store.set_context(ContextLayer.L6_DAILY, date, "buys", buys)
|
# Store daily metrics in L6 with market suffix
|
||||||
self.store.set_context(ContextLayer.L6_DAILY, date, "sells", sells)
|
|
||||||
self.store.set_context(ContextLayer.L6_DAILY, date, "holds", holds)
|
|
||||||
self.store.set_context(
|
self.store.set_context(
|
||||||
ContextLayer.L6_DAILY, date, "avg_confidence", round(avg_conf, 2)
|
ContextLayer.L6_DAILY, date, f"trade_count{key_suffix}", trade_count
|
||||||
|
)
|
||||||
|
self.store.set_context(ContextLayer.L6_DAILY, date, f"buys{key_suffix}", buys)
|
||||||
|
self.store.set_context(ContextLayer.L6_DAILY, date, f"sells{key_suffix}", sells)
|
||||||
|
self.store.set_context(ContextLayer.L6_DAILY, date, f"holds{key_suffix}", holds)
|
||||||
|
self.store.set_context(
|
||||||
|
ContextLayer.L6_DAILY,
|
||||||
|
date,
|
||||||
|
f"avg_confidence{key_suffix}",
|
||||||
|
round(avg_conf, 2),
|
||||||
)
|
)
|
||||||
self.store.set_context(
|
self.store.set_context(
|
||||||
ContextLayer.L6_DAILY, date, "total_pnl", round(total_pnl, 2)
|
ContextLayer.L6_DAILY,
|
||||||
|
date,
|
||||||
|
f"total_pnl{key_suffix}",
|
||||||
|
round(total_pnl, 2),
|
||||||
|
)
|
||||||
|
self.store.set_context(
|
||||||
|
ContextLayer.L6_DAILY, date, f"unique_stocks{key_suffix}", stocks
|
||||||
)
|
)
|
||||||
self.store.set_context(ContextLayer.L6_DAILY, date, "unique_stocks", stocks)
|
|
||||||
win_rate = round(wins / max(wins + losses, 1) * 100, 2)
|
win_rate = round(wins / max(wins + losses, 1) * 100, 2)
|
||||||
self.store.set_context(ContextLayer.L6_DAILY, date, "win_rate", win_rate)
|
self.store.set_context(
|
||||||
|
ContextLayer.L6_DAILY, date, f"win_rate{key_suffix}", win_rate
|
||||||
|
)
|
||||||
|
|
||||||
def aggregate_weekly_from_daily(self, week: str | None = None) -> None:
|
def aggregate_weekly_from_daily(self, week: str | None = None) -> None:
|
||||||
"""Aggregate L5 (weekly) context from L6 (daily).
|
"""Aggregate L5 (weekly) context from L6 (daily).
|
||||||
@@ -92,14 +123,25 @@ class ContextAggregator:
|
|||||||
daily_data[row[0]].append(json.loads(row[1]))
|
daily_data[row[0]].append(json.loads(row[1]))
|
||||||
|
|
||||||
if daily_data:
|
if daily_data:
|
||||||
# Sum all PnL values
|
# Sum all PnL values (market-specific if suffixed)
|
||||||
if "total_pnl" in daily_data:
|
if "total_pnl" in daily_data:
|
||||||
total_pnl = sum(daily_data["total_pnl"])
|
total_pnl = sum(daily_data["total_pnl"])
|
||||||
self.store.set_context(
|
self.store.set_context(
|
||||||
ContextLayer.L5_WEEKLY, week, "weekly_pnl", round(total_pnl, 2)
|
ContextLayer.L5_WEEKLY, week, "weekly_pnl", round(total_pnl, 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Average all confidence values
|
for key, values in daily_data.items():
|
||||||
|
if key.startswith("total_pnl_"):
|
||||||
|
market_code = key.split("total_pnl_", 1)[1]
|
||||||
|
total_pnl = sum(values)
|
||||||
|
self.store.set_context(
|
||||||
|
ContextLayer.L5_WEEKLY,
|
||||||
|
week,
|
||||||
|
f"weekly_pnl_{market_code}",
|
||||||
|
round(total_pnl, 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Average all confidence values (market-specific if suffixed)
|
||||||
if "avg_confidence" in daily_data:
|
if "avg_confidence" in daily_data:
|
||||||
conf_values = daily_data["avg_confidence"]
|
conf_values = daily_data["avg_confidence"]
|
||||||
avg_conf = sum(conf_values) / len(conf_values)
|
avg_conf = sum(conf_values) / len(conf_values)
|
||||||
@@ -107,6 +149,17 @@ class ContextAggregator:
|
|||||||
ContextLayer.L5_WEEKLY, week, "avg_confidence", round(avg_conf, 2)
|
ContextLayer.L5_WEEKLY, week, "avg_confidence", round(avg_conf, 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for key, values in daily_data.items():
|
||||||
|
if key.startswith("avg_confidence_"):
|
||||||
|
market_code = key.split("avg_confidence_", 1)[1]
|
||||||
|
avg_conf = sum(values) / len(values)
|
||||||
|
self.store.set_context(
|
||||||
|
ContextLayer.L5_WEEKLY,
|
||||||
|
week,
|
||||||
|
f"avg_confidence_{market_code}",
|
||||||
|
round(avg_conf, 2),
|
||||||
|
)
|
||||||
|
|
||||||
def aggregate_monthly_from_weekly(self, month: str | None = None) -> None:
|
def aggregate_monthly_from_weekly(self, month: str | None = None) -> None:
|
||||||
"""Aggregate L4 (monthly) context from L5 (weekly).
|
"""Aggregate L4 (monthly) context from L5 (weekly).
|
||||||
|
|
||||||
@@ -135,8 +188,16 @@ class ContextAggregator:
|
|||||||
|
|
||||||
if weekly_data:
|
if weekly_data:
|
||||||
# Sum all weekly PnL values
|
# Sum all weekly PnL values
|
||||||
|
total_pnl_values: list[float] = []
|
||||||
if "weekly_pnl" in weekly_data:
|
if "weekly_pnl" in weekly_data:
|
||||||
total_pnl = sum(weekly_data["weekly_pnl"])
|
total_pnl_values.extend(weekly_data["weekly_pnl"])
|
||||||
|
|
||||||
|
for key, values in weekly_data.items():
|
||||||
|
if key.startswith("weekly_pnl_"):
|
||||||
|
total_pnl_values.extend(values)
|
||||||
|
|
||||||
|
if total_pnl_values:
|
||||||
|
total_pnl = sum(total_pnl_values)
|
||||||
self.store.set_context(
|
self.store.set_context(
|
||||||
ContextLayer.L4_MONTHLY, month, "monthly_pnl", round(total_pnl, 2)
|
ContextLayer.L4_MONTHLY, month, "monthly_pnl", round(total_pnl, 2)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from src.brain.gemini_client import GeminiClient, TradeDecision
|
|||||||
from src.broker.kis_api import KISBroker
|
from src.broker.kis_api import KISBroker
|
||||||
from src.broker.overseas import OverseasBroker
|
from src.broker.overseas import OverseasBroker
|
||||||
from src.config import Settings
|
from src.config import Settings
|
||||||
|
from src.context.aggregator import ContextAggregator
|
||||||
from src.context.layer import ContextLayer
|
from src.context.layer import ContextLayer
|
||||||
from src.context.store import ContextStore
|
from src.context.store import ContextStore
|
||||||
from src.core.criticality import CriticalityAssessor
|
from src.core.criticality import CriticalityAssessor
|
||||||
@@ -706,6 +707,7 @@ async def run(settings: Settings) -> None:
|
|||||||
db_conn = init_db(settings.DB_PATH)
|
db_conn = init_db(settings.DB_PATH)
|
||||||
decision_logger = DecisionLogger(db_conn)
|
decision_logger = DecisionLogger(db_conn)
|
||||||
context_store = ContextStore(db_conn)
|
context_store = ContextStore(db_conn)
|
||||||
|
context_aggregator = ContextAggregator(db_conn)
|
||||||
|
|
||||||
# V2 proactive strategy components
|
# V2 proactive strategy components
|
||||||
context_selector = ContextSelector(context_store)
|
context_selector = ContextSelector(context_store)
|
||||||
@@ -990,6 +992,13 @@ async def run(settings: Settings) -> None:
|
|||||||
market_info = MARKETS.get(market_code)
|
market_info = MARKETS.get(market_code)
|
||||||
if market_info:
|
if market_info:
|
||||||
await telegram.notify_market_close(market_info.name, 0.0)
|
await telegram.notify_market_close(market_info.name, 0.0)
|
||||||
|
market_date = datetime.now(
|
||||||
|
market_info.timezone
|
||||||
|
).date().isoformat()
|
||||||
|
context_aggregator.aggregate_daily_from_trades(
|
||||||
|
date=market_date,
|
||||||
|
market=market_code,
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Market close notification failed: %s", exc)
|
logger.warning("Market close notification failed: %s", exc)
|
||||||
_market_states[market_code] = False
|
_market_states[market_code] = False
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class TestContextAggregator:
|
|||||||
self, aggregator: ContextAggregator, db_conn: sqlite3.Connection
|
self, aggregator: ContextAggregator, db_conn: sqlite3.Connection
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test aggregating daily metrics from trades."""
|
"""Test aggregating daily metrics from trades."""
|
||||||
date = "2026-02-04"
|
date = datetime.now(UTC).date().isoformat()
|
||||||
|
|
||||||
# Create sample trades
|
# Create sample trades
|
||||||
log_trade(db_conn, "005930", "BUY", 85, "Good signal", quantity=10, price=70000, pnl=500)
|
log_trade(db_conn, "005930", "BUY", 85, "Good signal", quantity=10, price=70000, pnl=500)
|
||||||
@@ -175,36 +175,44 @@ class TestContextAggregator:
|
|||||||
db_conn.commit()
|
db_conn.commit()
|
||||||
|
|
||||||
# Aggregate
|
# Aggregate
|
||||||
aggregator.aggregate_daily_from_trades(date)
|
aggregator.aggregate_daily_from_trades(date, market="KR")
|
||||||
|
|
||||||
# Verify L6 contexts
|
# Verify L6 contexts
|
||||||
store = aggregator.store
|
store = aggregator.store
|
||||||
assert store.get_context(ContextLayer.L6_DAILY, date, "trade_count") == 3
|
assert store.get_context(ContextLayer.L6_DAILY, date, "trade_count_KR") == 3
|
||||||
assert store.get_context(ContextLayer.L6_DAILY, date, "buys") == 1
|
assert store.get_context(ContextLayer.L6_DAILY, date, "buys_KR") == 1
|
||||||
assert store.get_context(ContextLayer.L6_DAILY, date, "sells") == 1
|
assert store.get_context(ContextLayer.L6_DAILY, date, "sells_KR") == 1
|
||||||
assert store.get_context(ContextLayer.L6_DAILY, date, "holds") == 1
|
assert store.get_context(ContextLayer.L6_DAILY, date, "holds_KR") == 1
|
||||||
assert store.get_context(ContextLayer.L6_DAILY, date, "total_pnl") == 2000.0
|
assert store.get_context(ContextLayer.L6_DAILY, date, "total_pnl_KR") == 2000.0
|
||||||
assert store.get_context(ContextLayer.L6_DAILY, date, "unique_stocks") == 3
|
assert store.get_context(ContextLayer.L6_DAILY, date, "unique_stocks_KR") == 3
|
||||||
# 2 wins, 0 losses
|
# 2 wins, 0 losses
|
||||||
assert store.get_context(ContextLayer.L6_DAILY, date, "win_rate") == 100.0
|
assert store.get_context(ContextLayer.L6_DAILY, date, "win_rate_KR") == 100.0
|
||||||
|
|
||||||
def test_aggregate_weekly_from_daily(self, aggregator: ContextAggregator) -> None:
|
def test_aggregate_weekly_from_daily(self, aggregator: ContextAggregator) -> None:
|
||||||
"""Test aggregating weekly metrics from daily."""
|
"""Test aggregating weekly metrics from daily."""
|
||||||
week = "2026-W06"
|
week = "2026-W06"
|
||||||
|
|
||||||
# Set daily contexts
|
# Set daily contexts
|
||||||
aggregator.store.set_context(ContextLayer.L6_DAILY, "2026-02-02", "total_pnl", 100.0)
|
aggregator.store.set_context(
|
||||||
aggregator.store.set_context(ContextLayer.L6_DAILY, "2026-02-03", "total_pnl", 200.0)
|
ContextLayer.L6_DAILY, "2026-02-02", "total_pnl_KR", 100.0
|
||||||
aggregator.store.set_context(ContextLayer.L6_DAILY, "2026-02-02", "avg_confidence", 80.0)
|
)
|
||||||
aggregator.store.set_context(ContextLayer.L6_DAILY, "2026-02-03", "avg_confidence", 85.0)
|
aggregator.store.set_context(
|
||||||
|
ContextLayer.L6_DAILY, "2026-02-03", "total_pnl_KR", 200.0
|
||||||
|
)
|
||||||
|
aggregator.store.set_context(
|
||||||
|
ContextLayer.L6_DAILY, "2026-02-02", "avg_confidence_KR", 80.0
|
||||||
|
)
|
||||||
|
aggregator.store.set_context(
|
||||||
|
ContextLayer.L6_DAILY, "2026-02-03", "avg_confidence_KR", 85.0
|
||||||
|
)
|
||||||
|
|
||||||
# Aggregate
|
# Aggregate
|
||||||
aggregator.aggregate_weekly_from_daily(week)
|
aggregator.aggregate_weekly_from_daily(week)
|
||||||
|
|
||||||
# Verify L5 contexts
|
# Verify L5 contexts
|
||||||
store = aggregator.store
|
store = aggregator.store
|
||||||
weekly_pnl = store.get_context(ContextLayer.L5_WEEKLY, week, "weekly_pnl")
|
weekly_pnl = store.get_context(ContextLayer.L5_WEEKLY, week, "weekly_pnl_KR")
|
||||||
avg_conf = store.get_context(ContextLayer.L5_WEEKLY, week, "avg_confidence")
|
avg_conf = store.get_context(ContextLayer.L5_WEEKLY, week, "avg_confidence_KR")
|
||||||
|
|
||||||
assert weekly_pnl == 300.0
|
assert weekly_pnl == 300.0
|
||||||
assert avg_conf == 82.5
|
assert avg_conf == 82.5
|
||||||
@@ -214,9 +222,15 @@ class TestContextAggregator:
|
|||||||
month = "2026-02"
|
month = "2026-02"
|
||||||
|
|
||||||
# Set weekly contexts
|
# Set weekly contexts
|
||||||
aggregator.store.set_context(ContextLayer.L5_WEEKLY, "2026-W05", "weekly_pnl", 100.0)
|
aggregator.store.set_context(
|
||||||
aggregator.store.set_context(ContextLayer.L5_WEEKLY, "2026-W06", "weekly_pnl", 200.0)
|
ContextLayer.L5_WEEKLY, "2026-W05", "weekly_pnl_KR", 100.0
|
||||||
aggregator.store.set_context(ContextLayer.L5_WEEKLY, "2026-W07", "weekly_pnl", 150.0)
|
)
|
||||||
|
aggregator.store.set_context(
|
||||||
|
ContextLayer.L5_WEEKLY, "2026-W06", "weekly_pnl_KR", 200.0
|
||||||
|
)
|
||||||
|
aggregator.store.set_context(
|
||||||
|
ContextLayer.L5_WEEKLY, "2026-W07", "weekly_pnl_KR", 150.0
|
||||||
|
)
|
||||||
|
|
||||||
# Aggregate
|
# Aggregate
|
||||||
aggregator.aggregate_monthly_from_weekly(month)
|
aggregator.aggregate_monthly_from_weekly(month)
|
||||||
@@ -285,7 +299,7 @@ class TestContextAggregator:
|
|||||||
self, aggregator: ContextAggregator, db_conn: sqlite3.Connection
|
self, aggregator: ContextAggregator, db_conn: sqlite3.Connection
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test running all aggregations from L7 to L1."""
|
"""Test running all aggregations from L7 to L1."""
|
||||||
date = "2026-02-04"
|
date = datetime.now(UTC).date().isoformat()
|
||||||
|
|
||||||
# Create sample trades
|
# Create sample trades
|
||||||
log_trade(db_conn, "005930", "BUY", 85, "Good signal", quantity=10, price=70000, pnl=1000)
|
log_trade(db_conn, "005930", "BUY", 85, "Good signal", quantity=10, price=70000, pnl=1000)
|
||||||
@@ -299,12 +313,12 @@ class TestContextAggregator:
|
|||||||
|
|
||||||
# Verify data exists in each layer
|
# Verify data exists in each layer
|
||||||
store = aggregator.store
|
store = aggregator.store
|
||||||
assert store.get_context(ContextLayer.L6_DAILY, date, "total_pnl") == 1000.0
|
assert store.get_context(ContextLayer.L6_DAILY, date, "total_pnl_KR") == 1000.0
|
||||||
from datetime import date as date_cls
|
from datetime import date as date_cls
|
||||||
trade_date = date_cls.fromisoformat(date)
|
trade_date = date_cls.fromisoformat(date)
|
||||||
iso_year, iso_week, _ = trade_date.isocalendar()
|
iso_year, iso_week, _ = trade_date.isocalendar()
|
||||||
trade_week = f"{iso_year}-W{iso_week:02d}"
|
trade_week = f"{iso_year}-W{iso_week:02d}"
|
||||||
assert store.get_context(ContextLayer.L5_WEEKLY, trade_week, "weekly_pnl") is not None
|
assert store.get_context(ContextLayer.L5_WEEKLY, trade_week, "weekly_pnl_KR") is not None
|
||||||
trade_month = f"{trade_date.year}-{trade_date.month:02d}"
|
trade_month = f"{trade_date.year}-{trade_date.month:02d}"
|
||||||
trade_quarter = f"{trade_date.year}-Q{(trade_date.month - 1) // 3 + 1}"
|
trade_quarter = f"{trade_date.year}-Q{(trade_date.month - 1) // 3 + 1}"
|
||||||
trade_year = str(trade_date.year)
|
trade_year = str(trade_date.year)
|
||||||
|
|||||||
Reference in New Issue
Block a user