ifieryarrows commited on
Commit
da9fe4b
·
verified ·
1 Parent(s): 9c39ba1

Sync from GitHub (tests passed)

Browse files
Files changed (4) hide show
  1. app/ai_engine.py +1 -1
  2. app/commentary.py +6 -31
  3. app/models.py +15 -10
  4. backtest/runner.py +3 -3
app/ai_engine.py CHANGED
@@ -2165,7 +2165,7 @@ def save_model_metadata_to_db(
2165
  existing.importance_json = json.dumps(importance)
2166
  existing.features_json = json.dumps(features)
2167
  existing.metrics_json = json.dumps(metrics)
2168
- existing.trained_at = datetime.utcnow()
2169
  logger.info(f"Updated model metadata in DB for {symbol}")
2170
  else:
2171
  new_record = ModelMetadata(
 
2165
  existing.importance_json = json.dumps(importance)
2166
  existing.features_json = json.dumps(features)
2167
  existing.metrics_json = json.dumps(metrics)
2168
+ existing.trained_at = datetime.now(timezone.utc)
2169
  logger.info(f"Updated model metadata in DB for {symbol}")
2170
  else:
2171
  new_record = ModelMetadata(
app/commentary.py CHANGED
@@ -7,7 +7,7 @@ from __future__ import annotations
7
 
8
  import json
9
  import logging
10
- from datetime import datetime
11
  from typing import Optional
12
 
13
  from .openrouter_client import OpenRouterError, create_chat_completion
@@ -18,20 +18,7 @@ logger = logging.getLogger(__name__)
18
  VALID_STANCES = {"BULLISH", "NEUTRAL", "BEARISH"}
19
 
20
  COMMENTARY_RESPONSE_FORMAT = {
21
- "type": "json_schema",
22
- "json_schema": {
23
- "name": "commentary_with_stance",
24
- "strict": True,
25
- "schema": {
26
- "type": "object",
27
- "properties": {
28
- "stance": {"type": "string", "enum": ["BULLISH", "NEUTRAL", "BEARISH"]},
29
- "commentary": {"type": "string"},
30
- },
31
- "required": ["stance", "commentary"],
32
- "additionalProperties": False,
33
- },
34
- },
35
  }
36
 
37
 
@@ -281,11 +268,9 @@ Data:
281
  "title": "CopperMind Commentary",
282
  }
283
 
284
- async def _request_commentary(strict_schema: bool) -> str:
285
  kwargs = dict(base_request_kwargs)
286
- if strict_schema:
287
- kwargs["response_format"] = COMMENTARY_RESPONSE_FORMAT
288
- kwargs["provider"] = {"require_parameters": True}
289
  data = await create_chat_completion(**kwargs)
290
  content = _extract_chat_message_content(data)
291
  if not content:
@@ -323,17 +308,7 @@ Data:
323
  return repaired
324
 
325
  try:
326
- try:
327
- content = await _request_commentary(strict_schema=True)
328
- except OpenRouterError as exc:
329
- message = str(exc).lower()
330
- if exc.status_code == 404 and "no endpoints found" in message:
331
- logger.warning(
332
- "Structured commentary request unsupported by provider routing; retrying relaxed."
333
- )
334
- content = await _request_commentary(strict_schema=False)
335
- else:
336
- raise
337
 
338
  try:
339
  stance, commentary = _parse_commentary_payload(content)
@@ -400,7 +375,7 @@ def save_commentary_to_db(
400
  existing.predicted_return = predicted_return
401
  existing.sentiment_label = sentiment_label
402
  existing.ai_stance = ai_stance
403
- existing.generated_at = datetime.utcnow()
404
  existing.model_name = settings.resolved_commentary_model
405
  logger.info("Updated AI commentary for %s (stance: %s)", symbol, ai_stance)
406
  else:
 
7
 
8
  import json
9
  import logging
10
+ from datetime import datetime, timezone
11
  from typing import Optional
12
 
13
  from .openrouter_client import OpenRouterError, create_chat_completion
 
18
  VALID_STANCES = {"BULLISH", "NEUTRAL", "BEARISH"}
19
 
20
  COMMENTARY_RESPONSE_FORMAT = {
21
+ "type": "json_object",
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
24
 
 
268
  "title": "CopperMind Commentary",
269
  }
270
 
271
+ async def _request_commentary() -> str:
272
  kwargs = dict(base_request_kwargs)
273
+ kwargs["response_format"] = COMMENTARY_RESPONSE_FORMAT
 
 
274
  data = await create_chat_completion(**kwargs)
275
  content = _extract_chat_message_content(data)
276
  if not content:
 
308
  return repaired
309
 
310
  try:
311
+ content = await _request_commentary()
 
 
 
 
 
 
 
 
 
 
312
 
313
  try:
314
  stance, commentary = _parse_commentary_payload(content)
 
375
  existing.predicted_return = predicted_return
376
  existing.sentiment_label = sentiment_label
377
  existing.ai_stance = ai_stance
378
+ existing.generated_at = datetime.now(timezone.utc)
379
  existing.model_name = settings.resolved_commentary_model
380
  logger.info("Updated AI commentary for %s (stance: %s)", symbol, ai_stance)
381
  else:
app/models.py CHANGED
@@ -9,9 +9,14 @@ Tables:
9
  - AnalysisSnapshot: Cached analysis reports
10
  """
11
 
12
- from datetime import datetime
13
  from typing import Optional
14
 
 
 
 
 
 
15
  from sqlalchemy import (
16
  Column,
17
  Integer,
@@ -59,7 +64,7 @@ class NewsArticle(Base):
59
 
60
  # Timestamps
61
  published_at = Column(DateTime(timezone=True), nullable=False, index=True)
62
- fetched_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow)
63
 
64
  # Relationships
65
  sentiment = relationship("NewsSentiment", back_populates="article", uselist=False)
@@ -91,7 +96,7 @@ class PriceBar(Base):
91
  adj_close = Column(Float, nullable=True)
92
 
93
  # When this record was fetched
94
- fetched_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow)
95
 
96
  __table_args__ = (
97
  UniqueConstraint("symbol", "date", name="uq_price_symbol_date"),
@@ -136,7 +141,7 @@ class NewsSentiment(Base):
136
  model_name = Column(String(100), default="google/gemini-2.0-flash-exp:free")
137
 
138
  # When scored
139
- scored_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow)
140
 
141
  # Relationship
142
  article = relationship("NewsArticle", back_populates="sentiment")
@@ -169,7 +174,7 @@ class DailySentiment(Base):
169
  weighting_method = Column(String(50), default="recency_exponential")
170
 
171
  # When aggregated
172
- aggregated_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow)
173
 
174
  def __repr__(self):
175
  return f"<DailySentiment(date={self.date}, index={self.sentiment_index:.3f}, news={self.news_count})>"
@@ -191,7 +196,7 @@ class AnalysisSnapshot(Base):
191
  report_json = Column(JSON, nullable=False)
192
 
193
  # When this snapshot was generated
194
- generated_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow, index=True)
195
 
196
  # Model version used
197
  model_version = Column(String(100), nullable=True)
@@ -230,7 +235,7 @@ class AICommentary(Base):
230
  ai_stance = Column(String(20), nullable=True, default="NEUTRAL")
231
 
232
  # When generated
233
- generated_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow, index=True)
234
 
235
  # Model used
236
  model_name = Column(String(100), nullable=True)
@@ -262,7 +267,7 @@ class ModelMetadata(Base):
262
  metrics_json = Column(Text, nullable=True)
263
 
264
  # When the model was trained
265
- trained_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow, index=True)
266
 
267
  def __repr__(self):
268
  return f"<ModelMetadata(symbol={self.symbol}, trained_at={self.trained_at})>"
@@ -467,7 +472,7 @@ class NewsSentimentV2(Base):
467
  reasoning_json = Column(Text, nullable=True)
468
  model_fast = Column(String(100), nullable=True)
469
  model_reliable = Column(String(100), nullable=True)
470
- scored_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow, index=True)
471
 
472
  processed = relationship("NewsProcessed", back_populates="sentiment_v2_items")
473
 
@@ -498,7 +503,7 @@ class DailySentimentV2(Base):
498
  avg_confidence = Column(Float, nullable=True)
499
  avg_relevance = Column(Float, nullable=True)
500
  source_version = Column(String(20), nullable=False, default="v2")
501
- aggregated_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow, index=True)
502
 
503
  def __repr__(self):
504
  return (
 
9
  - AnalysisSnapshot: Cached analysis reports
10
  """
11
 
12
+ from datetime import datetime, timezone
13
  from typing import Optional
14
 
15
+
16
+ def _utcnow() -> datetime:
17
+ """Timezone-aware UTC now, replacing deprecated datetime.utcnow()."""
18
+ return datetime.now(timezone.utc)
19
+
20
  from sqlalchemy import (
21
  Column,
22
  Integer,
 
64
 
65
  # Timestamps
66
  published_at = Column(DateTime(timezone=True), nullable=False, index=True)
67
+ fetched_at = Column(DateTime(timezone=True), nullable=False, default=_utcnow)
68
 
69
  # Relationships
70
  sentiment = relationship("NewsSentiment", back_populates="article", uselist=False)
 
96
  adj_close = Column(Float, nullable=True)
97
 
98
  # When this record was fetched
99
+ fetched_at = Column(DateTime(timezone=True), nullable=False, default=_utcnow)
100
 
101
  __table_args__ = (
102
  UniqueConstraint("symbol", "date", name="uq_price_symbol_date"),
 
141
  model_name = Column(String(100), default="google/gemini-2.0-flash-exp:free")
142
 
143
  # When scored
144
+ scored_at = Column(DateTime(timezone=True), nullable=False, default=_utcnow)
145
 
146
  # Relationship
147
  article = relationship("NewsArticle", back_populates="sentiment")
 
174
  weighting_method = Column(String(50), default="recency_exponential")
175
 
176
  # When aggregated
177
+ aggregated_at = Column(DateTime(timezone=True), nullable=False, default=_utcnow)
178
 
179
  def __repr__(self):
180
  return f"<DailySentiment(date={self.date}, index={self.sentiment_index:.3f}, news={self.news_count})>"
 
196
  report_json = Column(JSON, nullable=False)
197
 
198
  # When this snapshot was generated
199
+ generated_at = Column(DateTime(timezone=True), nullable=False, default=_utcnow, index=True)
200
 
201
  # Model version used
202
  model_version = Column(String(100), nullable=True)
 
235
  ai_stance = Column(String(20), nullable=True, default="NEUTRAL")
236
 
237
  # When generated
238
+ generated_at = Column(DateTime(timezone=True), nullable=False, default=_utcnow, index=True)
239
 
240
  # Model used
241
  model_name = Column(String(100), nullable=True)
 
267
  metrics_json = Column(Text, nullable=True)
268
 
269
  # When the model was trained
270
+ trained_at = Column(DateTime(timezone=True), nullable=False, default=_utcnow, index=True)
271
 
272
  def __repr__(self):
273
  return f"<ModelMetadata(symbol={self.symbol}, trained_at={self.trained_at})>"
 
472
  reasoning_json = Column(Text, nullable=True)
473
  model_fast = Column(String(100), nullable=True)
474
  model_reliable = Column(String(100), nullable=True)
475
+ scored_at = Column(DateTime(timezone=True), nullable=False, default=_utcnow, index=True)
476
 
477
  processed = relationship("NewsProcessed", back_populates="sentiment_v2_items")
478
 
 
503
  avg_confidence = Column(Float, nullable=True)
504
  avg_relevance = Column(Float, nullable=True)
505
  source_version = Column(String(20), nullable=False, default="v2")
506
+ aggregated_at = Column(DateTime(timezone=True), nullable=False, default=_utcnow, index=True)
507
 
508
  def __repr__(self):
509
  return (
backtest/runner.py CHANGED
@@ -17,7 +17,7 @@ import hashlib
17
  import json
18
  import logging
19
  from dataclasses import dataclass, asdict
20
- from datetime import datetime, timedelta
21
  from pathlib import Path
22
  from typing import Optional
23
 
@@ -166,7 +166,7 @@ class BacktestRunner:
166
 
167
  def __init__(self, config: BacktestConfig):
168
  self.config = config
169
- self.run_id = f"backtest-{datetime.utcnow().strftime('%Y%m%d-%H%M%S')}"
170
 
171
  def fetch_prices(self, symbols: list[str], start: str, end: str) -> pd.DataFrame:
172
  """
@@ -476,7 +476,7 @@ class BacktestRunner:
476
 
477
  result = BacktestResult(
478
  run_id=self.run_id,
479
- generated_at=datetime.utcnow().isoformat() + "Z",
480
  config=self.config,
481
  champion={
482
  "symbol_set": asdict(champion_set),
 
17
  import json
18
  import logging
19
  from dataclasses import dataclass, asdict
20
+ from datetime import datetime, timedelta, timezone
21
  from pathlib import Path
22
  from typing import Optional
23
 
 
166
 
167
  def __init__(self, config: BacktestConfig):
168
  self.config = config
169
+ self.run_id = f"backtest-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}"
170
 
171
  def fetch_prices(self, symbols: list[str], start: str, end: str) -> pd.DataFrame:
172
  """
 
476
 
477
  result = BacktestResult(
478
  run_id=self.run_id,
479
+ generated_at=datetime.now(timezone.utc).isoformat() + "Z",
480
  config=self.config,
481
  champion={
482
  "symbol_set": asdict(champion_set),