420 lines
14 KiB
Markdown
420 lines
14 KiB
Markdown
# チケット #016: Reddit時間制御とデータ整合性実装
|
||
|
||
## 概要
|
||
未来日のデータ取得を防止し、市場時間を考慮したデータ取得時間制御の実装(初期実装は米国市場のみ)
|
||
|
||
## 目的
|
||
- バックテストでの未来情報リーク防止
|
||
- 市場開始前のデータのみを使用
|
||
- タイムゾーンの適切な処理(初期実装: 米国東部時間のみ)
|
||
- データの時間的整合性の保証
|
||
|
||
## 実装要件
|
||
|
||
### 1. 時間検証クラス(初期実装: 米国市場のみ)
|
||
```python
|
||
from datetime import datetime, time, timezone, timedelta
|
||
import pytz
|
||
import pandas_market_calendars as mcal
|
||
|
||
class TemporalDataValidator:
|
||
def __init__(self):
|
||
# 初期実装: 米国市場のみ
|
||
self.market_timezone = pytz.timezone('America/New_York')
|
||
self.market_open = time(9, 30)
|
||
self.data_cutoff_time = time(9, 0) # 30分前
|
||
self.calendar = mcal.get_calendar('NYSE')
|
||
|
||
# 将来の拡張用(コメントアウト)
|
||
# self.market_configs = {
|
||
# 'US': {...},
|
||
# 'JP': {...},
|
||
# 'EU': {...},
|
||
# 'DE': {...}
|
||
# }
|
||
|
||
def validate_date_not_future(self, target_date: str) -> bool:
|
||
"""
|
||
対象日が未来日でないことを確認
|
||
|
||
Args:
|
||
target_date: YYYY-MM-DD形式の日付
|
||
|
||
Returns:
|
||
有効な場合True、未来日の場合False
|
||
|
||
Raises:
|
||
ValueError: 未来日を指定した場合
|
||
"""
|
||
target = datetime.strptime(target_date, "%Y-%m-%d").date()
|
||
today = datetime.now(self.market_timezone).date()
|
||
|
||
if target > today:
|
||
raise ValueError(
|
||
f"Cannot fetch data for future date: {target_date}. "
|
||
f"Today is {today} (EST)."
|
||
)
|
||
|
||
return True
|
||
|
||
def is_market_open_day(self, date: str) -> bool:
|
||
"""
|
||
指定日が米国市場の営業日かどうかをチェック
|
||
|
||
Args:
|
||
date: YYYY-MM-DD形式の日付
|
||
|
||
Returns:
|
||
営業日の場合True、休場日の場合False
|
||
"""
|
||
# 指定日が営業日かチェック
|
||
schedule = self.calendar.schedule(start_date=date, end_date=date)
|
||
return len(schedule) > 0
|
||
|
||
def get_next_market_day(self, date: str) -> str:
|
||
"""
|
||
次の営業日を取得(週末・祝日をスキップ)
|
||
"""
|
||
date_obj = datetime.strptime(date, "%Y-%m-%d")
|
||
|
||
# 次の営業日を探す
|
||
for i in range(1, 10): # 最大10日先まで検索
|
||
next_date = date_obj + timedelta(days=i)
|
||
next_date_str = next_date.strftime("%Y-%m-%d")
|
||
if self.is_market_open_day(next_date_str):
|
||
return next_date_str
|
||
|
||
raise ValueError(f"No market open day found within 10 days after {date}")
|
||
|
||
def get_data_cutoff_timestamp(self, target_date: str) -> int:
|
||
"""
|
||
米国市場の開始30分前のタイムスタンプを取得
|
||
|
||
Args:
|
||
target_date: YYYY-MM-DD形式の日付
|
||
|
||
Returns:
|
||
Unix timestamp (UTC)
|
||
"""
|
||
# 営業日チェック
|
||
if not self.is_market_open_day(target_date):
|
||
raise ValueError(
|
||
f"{target_date} is not a market open day. "
|
||
f"Next open day is {self.get_next_market_day(target_date)}"
|
||
)
|
||
|
||
date_obj = datetime.strptime(target_date, "%Y-%m-%d").date()
|
||
|
||
# タイムゾーンを考慮してカットオフ時刻を作成
|
||
# pytzは自動的にDST(サマータイム)を処理
|
||
cutoff_datetime = self.market_timezone.localize(
|
||
datetime.combine(date_obj, self.data_cutoff_time)
|
||
)
|
||
|
||
# UTCに変換してUnixタイムスタンプを返す
|
||
return int(cutoff_datetime.timestamp())
|
||
```
|
||
|
||
### 2. 時間フィルタリング機能
|
||
```python
|
||
class TemporalDataFilter:
|
||
def __init__(self, validator: TemporalDataValidator):
|
||
self.validator = validator
|
||
|
||
def filter_posts_by_cutoff(self,
|
||
posts: List[dict],
|
||
target_date: str) -> List[dict]:
|
||
"""
|
||
米国市場開始30分前までの投稿のみをフィルタリング
|
||
|
||
Args:
|
||
posts: Reddit投稿のリスト
|
||
target_date: 対象日付
|
||
|
||
Returns:
|
||
フィルタリング後の投稿リスト
|
||
"""
|
||
try:
|
||
cutoff_timestamp = self.validator.get_data_cutoff_timestamp(target_date)
|
||
except ValueError as e:
|
||
# 休場日の場合は空リストを返す
|
||
logging.info(str(e))
|
||
return []
|
||
|
||
filtered_posts = []
|
||
for post in posts:
|
||
post_timestamp = post.get('created_utc', 0)
|
||
|
||
# カットオフ時刻より前の投稿のみを含める
|
||
if post_timestamp < cutoff_timestamp:
|
||
filtered_posts.append(post)
|
||
else:
|
||
logging.debug(
|
||
f"Excluded post '{post.get('title')}' - "
|
||
f"posted after cutoff time"
|
||
)
|
||
|
||
logging.info(
|
||
f"Filtered {len(posts) - len(filtered_posts)} posts "
|
||
f"posted after {target_date} 09:00 EST"
|
||
)
|
||
|
||
return filtered_posts
|
||
```
|
||
|
||
### 3. 日付範囲の検証
|
||
```python
|
||
class DateRangeValidator:
|
||
def validate_date_range(self, start_date: str, end_date: str) -> dict:
|
||
"""
|
||
日付範囲の妥当性を検証
|
||
|
||
Returns:
|
||
{
|
||
"valid": bool,
|
||
"adjustments": {
|
||
"original_end": str,
|
||
"adjusted_end": str
|
||
},
|
||
"warnings": List[str]
|
||
}
|
||
"""
|
||
validator = TemporalDataValidator()
|
||
warnings = []
|
||
adjustments = {}
|
||
|
||
# 終了日が今日以降の場合は昨日に調整
|
||
today = datetime.now(validator.market_timezone).date()
|
||
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d").date()
|
||
|
||
if end_date_obj >= today:
|
||
yesterday = today - timedelta(days=1)
|
||
adjustments["original_end"] = end_date
|
||
adjustments["adjusted_end"] = yesterday.strftime("%Y-%m-%d")
|
||
warnings.append(
|
||
f"End date adjusted from {end_date} to {adjustments['adjusted_end']} "
|
||
f"to prevent future data access"
|
||
)
|
||
end_date = adjustments["adjusted_end"]
|
||
|
||
# 当日のデータ取得に関する警告
|
||
if end_date_obj == today:
|
||
current_time = datetime.now(validator.market_timezone).time()
|
||
if current_time < validator.data_cutoff_time:
|
||
warnings.append(
|
||
f"Today's data is not yet available. "
|
||
f"Data becomes available after 09:00 EST."
|
||
)
|
||
|
||
return {
|
||
"valid": True,
|
||
"adjustments": adjustments,
|
||
"warnings": warnings
|
||
}
|
||
```
|
||
|
||
### 4. 今後の拡張: マルチマーケット対応
|
||
```python
|
||
# 将来の実装予定: 複数市場のサポート
|
||
# 現在は米国市場のみをサポート
|
||
#
|
||
# class MultiMarketBatchProcessor:
|
||
# """
|
||
# 複数市場のティッカーを効率的に処理
|
||
# """
|
||
# pass
|
||
```
|
||
|
||
### 5. Reddit API取得時の時間制御
|
||
```python
|
||
class TimeAwareRedditFetcher(RedditDataFetcher):
|
||
def __init__(self, client, config):
|
||
super().__init__(client, config)
|
||
self.temporal_validator = TemporalDataValidator()
|
||
self.temporal_filter = TemporalDataFilter(self.temporal_validator)
|
||
|
||
def fetch_company_news(self,
|
||
ticker: str,
|
||
company_name: str,
|
||
date: str,
|
||
limit: int = 50) -> List[dict]:
|
||
"""
|
||
時間制御付きの企業ニュース取得(米国市場対応)
|
||
"""
|
||
# 未来日チェック
|
||
self.temporal_validator.validate_date_not_future(date)
|
||
|
||
# 市場営業日チェック
|
||
if not self.temporal_validator.is_market_open_day(date):
|
||
logging.info(
|
||
f"{date} is not a trading day. Skipping."
|
||
)
|
||
return []
|
||
|
||
# 通常の取得処理
|
||
posts = super().fetch_company_news(ticker, company_name, date, limit)
|
||
|
||
# 時間フィルタリング
|
||
filtered_posts = self.temporal_filter.filter_posts_by_cutoff(
|
||
posts, date
|
||
)
|
||
|
||
return filtered_posts
|
||
```
|
||
|
||
### 5. バックテスト実行時の検証
|
||
```python
|
||
class BacktestTimeValidator:
|
||
"""
|
||
バックテスト実行時の時間整合性チェック
|
||
"""
|
||
def validate_backtest_date(self, test_date: str) -> dict:
|
||
"""
|
||
バックテスト日付の妥当性検証
|
||
|
||
Returns:
|
||
{
|
||
"can_run": bool,
|
||
"reason": str,
|
||
"next_available_time": datetime
|
||
}
|
||
"""
|
||
validator = TemporalDataValidator()
|
||
now = datetime.now(validator.market_timezone)
|
||
|
||
# 今日の日付でバックテストする場合
|
||
if test_date == now.strftime("%Y-%m-%d"):
|
||
if now.time() < validator.data_cutoff_time:
|
||
next_available = datetime.combine(
|
||
now.date(),
|
||
validator.data_cutoff_time,
|
||
tzinfo=validator.market_timezone
|
||
)
|
||
return {
|
||
"can_run": False,
|
||
"reason": f"Today's data not yet available. Available after 09:00 EST.",
|
||
"next_available_time": next_available
|
||
}
|
||
|
||
return {
|
||
"can_run": True,
|
||
"reason": "Date is valid for backtesting",
|
||
"next_available_time": None
|
||
}
|
||
```
|
||
|
||
### 6. CLI統合
|
||
```python
|
||
# cli/commands/reddit.py への追加
|
||
|
||
@reddit.command()
|
||
@click.option('--force', is_flag=True, help='Force fetch without time validation')
|
||
def fetch_historical(force):
|
||
"""Fetch historical Reddit data with temporal validation"""
|
||
|
||
if not force:
|
||
# 日付範囲の検証
|
||
validator = DateRangeValidator()
|
||
validation_result = validator.validate_date_range(start_date, end_date)
|
||
|
||
if validation_result["warnings"]:
|
||
for warning in validation_result["warnings"]:
|
||
console.print(f"[yellow]Warning: {warning}[/yellow]")
|
||
|
||
if validation_result["adjustments"]:
|
||
console.print(
|
||
f"[cyan]Date range adjusted: "
|
||
f"{validation_result['adjustments']['original_end']} → "
|
||
f"{validation_result['adjustments']['adjusted_end']}[/cyan]"
|
||
)
|
||
|
||
if not click.confirm("Continue with adjusted dates?"):
|
||
return
|
||
```
|
||
|
||
### 7. 設定オプション
|
||
```python
|
||
# config/reddit_temporal_config.yaml
|
||
|
||
temporal_settings:
|
||
# タイムゾーン設定
|
||
market_timezone: "America/New_York"
|
||
|
||
# 市場時間
|
||
market_open: "09:30"
|
||
market_close: "16:00"
|
||
|
||
# データ取得カットオフ(市場開始何分前か)
|
||
data_cutoff_minutes_before_open: 30
|
||
|
||
# 検証設定
|
||
validation:
|
||
prevent_future_data: true
|
||
enforce_cutoff_time: true
|
||
allow_override: false # --forceオプションの許可
|
||
|
||
# 警告設定
|
||
warnings:
|
||
show_timezone_info: true
|
||
show_next_available_time: true
|
||
```
|
||
|
||
### 8. ログとモニタリング
|
||
```python
|
||
class TemporalAuditLogger:
|
||
"""
|
||
時間制御に関する監査ログ
|
||
"""
|
||
def log_temporal_filtering(self,
|
||
date: str,
|
||
total_posts: int,
|
||
filtered_posts: int,
|
||
cutoff_time: str):
|
||
"""
|
||
時間フィルタリングの結果を記録
|
||
"""
|
||
audit_entry = {
|
||
"timestamp": datetime.now().isoformat(),
|
||
"target_date": date,
|
||
"cutoff_time_est": cutoff_time,
|
||
"total_posts_fetched": total_posts,
|
||
"posts_after_cutoff": total_posts - filtered_posts,
|
||
"posts_included": filtered_posts,
|
||
"filter_ratio": (total_posts - filtered_posts) / total_posts if total_posts > 0 else 0
|
||
}
|
||
|
||
self.write_audit_log(audit_entry)
|
||
```
|
||
|
||
## 受け入れ条件
|
||
- [ ] 未来日のデータ取得が確実に防止される
|
||
- [ ] 米国市場の開始30分前(09:00 EST/EDT)でデータが切られる
|
||
- [ ] 週末・祝日が自動的にスキップされる
|
||
- [ ] サマータイムが自動的に処理される
|
||
- [ ] タイムゾーンが正しく処理される
|
||
- [ ] バックテストでの時間的整合性が保証される
|
||
- [ ] 適切な警告とログが出力される
|
||
- [ ] 既存システムとの互換性維持
|
||
- [ ] 単体テストの実装(モック使用)
|
||
|
||
## 依存関係
|
||
- pytz(タイムゾーン処理、DST対応)
|
||
- pandas_market_calendars(米国市場の営業日カレンダー)
|
||
- 既存のRedditDataFetcher
|
||
- CLI実装
|
||
|
||
## タスク
|
||
- [ ] 単体テストの作成(TDD)
|
||
- [ ] TemporalDataValidatorクラスの実装(米国市場のみ)
|
||
- [ ] TemporalDataFilterクラスの実装
|
||
- [ ] DateRangeValidatorの実装
|
||
- [ ] TimeAwareRedditFetcherの実装
|
||
- [ ] 米国市場カレンダーの統合(NYSE)
|
||
- [ ] バックテスト時間検証機能
|
||
- [ ] CLI統合(警告表示)
|
||
- [ ] 設定ファイルの追加
|
||
- [ ] 監査ログ機能
|
||
- [ ] タイムゾーン・DST処理のテスト
|
||
- [ ] 米国市場の休場日テスト
|
||
- [ ] 統合テスト
|
||
- [ ] ドキュメント更新 |