Compare commits
2 Commits
972bbbe706
...
d700bc1dfd
| Author | SHA1 | Date | |
|---|---|---|---|
| d700bc1dfd | |||
| 4f9e0231bd |
4
.env
4
.env
@@ -8,8 +8,8 @@ DINGTALK_WEBHOOK_1=https://oapi.dingtalk.com/robot/send?access_token=fb70c1561d8
|
|||||||
DINGTALK_SECRET_1=SEC1ae7cd2f1a6f9da3611af37da3e7d954c1e8533fc073c6c8cc5e5af3b6e5926b
|
DINGTALK_SECRET_1=SEC1ae7cd2f1a6f9da3611af37da3e7d954c1e8533fc073c6c8cc5e5af3b6e5926b
|
||||||
|
|
||||||
# 钉钉机器人配置 - 群2
|
# 钉钉机器人配置 - 群2
|
||||||
DINGTALK_WEBHOOK_2=https://oapi.dingtalk.com/robot/send?access_token=87c7abfcdd69b699c32da4e4f5981cd2ca6b0445474fc6ffb36f2ed0f6262fbb
|
# DINGTALK_WEBHOOK_2=https://oapi.dingtalk.com/robot/send?access_token=87c7abfcdd69b699c32da4e4f5981cd2ca6b0445474fc6ffb36f2ed0f6262fbb
|
||||||
DINGTALK_SECRET_2=SECf3d6b43f2f8a87ab91feffd052e71ec314fbf57a1842e483fe07af3c0a0e5aa6
|
# DINGTALK_SECRET_2=SECf3d6b43f2f8a87ab91feffd052e71ec314fbf57a1842e483fe07af3c0a0e5aa6
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,15 @@ class FlaskAPIDataSource:
|
|||||||
standard_cols = ['code'] + standard_cols
|
standard_cols = ['code'] + standard_cols
|
||||||
df = df[standard_cols]
|
df = df[standard_cols]
|
||||||
|
|
||||||
|
# 过滤 yfinance 返回的不完整数据(未收盘日 close=NaN, volume=0)
|
||||||
|
nan_count = df['close'].isna().sum()
|
||||||
|
if nan_count > 0:
|
||||||
|
df = df.dropna(subset=['close'])
|
||||||
|
actual_count = len(df)
|
||||||
|
if actual_count == 0:
|
||||||
|
print(f"⚠ {code}: 所有数据 close 均为 NaN")
|
||||||
|
return None
|
||||||
|
|
||||||
# 使用 API 返回的实际数据范围(而非请求参数)
|
# 使用 API 返回的实际数据范围(而非请求参数)
|
||||||
actual_start = validated.date_range.start if validated.date_range else start_date
|
actual_start = validated.date_range.start if validated.date_range else start_date
|
||||||
actual_end = validated.date_range.end if validated.date_range else end_date
|
actual_end = validated.date_range.end if validated.date_range else end_date
|
||||||
|
|||||||
@@ -44,6 +44,25 @@ class YFinanceSource:
|
|||||||
self.use_ssh_tunnel = use_ssh_tunnel
|
self.use_ssh_tunnel = use_ssh_tunnel
|
||||||
self._delay = 0.5 # 请求延迟(避免限流)
|
self._delay = 0.5 # 请求延迟(避免限流)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_index(index: pd.DatetimeIndex) -> pd.DatetimeIndex:
|
||||||
|
"""
|
||||||
|
标准化日期索引,保留交易所本地日期
|
||||||
|
|
||||||
|
yfinance 返回的时间戳带有交易所本地时区:
|
||||||
|
- 美股: 00:00-04:00 (US/Eastern) → 剥 tz 后 00:00,日期不变
|
||||||
|
- 日本: 00:00+09:00 (Asia/Tokyo) → 剥 tz 后 00:00,日期不变
|
||||||
|
- 欧洲: 00:00+02:00 (CET) → 剥 tz 后 00:00,日期不变
|
||||||
|
|
||||||
|
关键:直接 tz_localize(None) 剥除时区,不做 UTC 转换。
|
||||||
|
错误示范:pd.to_datetime(idx, utc=True) 会先把日本 00:00+09:00 转成
|
||||||
|
前一天 15:00 UTC,导致日期回退一天。
|
||||||
|
"""
|
||||||
|
if index.tz is not None:
|
||||||
|
# tz_localize(None) 直接剥除时区,保留本地时间部分
|
||||||
|
index = index.tz_localize(None)
|
||||||
|
return index.normalize()
|
||||||
|
|
||||||
def fetch(self, code: str, start_date: str, end_date: str, adj: str = 'raw') -> Optional[pd.DataFrame]:
|
def fetch(self, code: str, start_date: str, end_date: str, adj: str = 'raw') -> Optional[pd.DataFrame]:
|
||||||
"""
|
"""
|
||||||
获取数据(支持 adj 参数)
|
获取数据(支持 adj 参数)
|
||||||
@@ -108,10 +127,17 @@ class YFinanceSource:
|
|||||||
"Volume": "volume",
|
"Volume": "volume",
|
||||||
})
|
})
|
||||||
|
|
||||||
# 确保索引是日期格式
|
# 确保索引是日期格式(保留交易所本地日期,避免 UTC 转换导致跨日偏移)
|
||||||
df.index = pd.to_datetime(df.index, utc=True).tz_localize(None).normalize()
|
df.index = self._normalize_index(df.index)
|
||||||
df.index.name = "date"
|
df.index.name = "date"
|
||||||
|
|
||||||
|
# 过滤 yfinance 返回的不完整数据(未收盘日 close=NaN, volume=0)
|
||||||
|
nan_mask = df['close'].isna()
|
||||||
|
if nan_mask.any():
|
||||||
|
df = df[~nan_mask]
|
||||||
|
if len(df) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
# 添加代码列
|
# 添加代码列
|
||||||
df["code"] = code
|
df["code"] = code
|
||||||
|
|
||||||
@@ -189,8 +215,8 @@ class YFinanceSource:
|
|||||||
"Volume": "volume",
|
"Volume": "volume",
|
||||||
})
|
})
|
||||||
|
|
||||||
# 确保索引是日期格式
|
# 确保索引是日期格式(保留交易所本地日期,避免 UTC 转换导致跨日偏移)
|
||||||
df.index = pd.to_datetime(df.index, utc=True).tz_localize(None).normalize()
|
df.index = self._normalize_index(df.index)
|
||||||
df.index.name = "date"
|
df.index.name = "date"
|
||||||
|
|
||||||
# 添加代码列和标记
|
# 添加代码列和标记
|
||||||
|
|||||||
@@ -40,6 +40,22 @@ def _http_get(url: str, params: dict = None, timeout: int = 120) -> requests.Res
|
|||||||
return _session.get(url, params=params, timeout=timeout)
|
return _session.get(url, params=params, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_json(obj):
|
||||||
|
"""Recursively replace NaN/Inf with None in-place so json.dump produces valid JSON"""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
for k, v in obj.items():
|
||||||
|
if isinstance(v, float) and (math.isnan(v) or math.isinf(v)):
|
||||||
|
obj[k] = None
|
||||||
|
elif isinstance(v, (dict, list)):
|
||||||
|
_sanitize_json(v)
|
||||||
|
elif isinstance(obj, list):
|
||||||
|
for i, v in enumerate(obj):
|
||||||
|
if isinstance(v, float) and (math.isnan(v) or math.isinf(v)):
|
||||||
|
obj[i] = None
|
||||||
|
elif isinstance(v, (dict, list)):
|
||||||
|
_sanitize_json(v)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Pure functions: momentum
|
# Pure functions: momentum
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -888,6 +904,7 @@ class SimpleRotationStrategy:
|
|||||||
},
|
},
|
||||||
'days': days_out,
|
'days': days_out,
|
||||||
}
|
}
|
||||||
|
_sanitize_json(detail)
|
||||||
with open(detail_path, 'w', encoding='utf-8') as f:
|
with open(detail_path, 'w', encoding='utf-8') as f:
|
||||||
json.dump(detail, f, ensure_ascii=False, indent=2)
|
json.dump(detail, f, ensure_ascii=False, indent=2)
|
||||||
print(f" + Detail: {detail_path} ({len(days_out)} days)")
|
print(f" + Detail: {detail_path} ({len(days_out)} days)")
|
||||||
@@ -895,6 +912,7 @@ class SimpleRotationStrategy:
|
|||||||
# Metrics JSON
|
# Metrics JSON
|
||||||
metrics = self._compute_metrics(sum(1 for r in self.daily_records if r['is_rebalance']))
|
metrics = self._compute_metrics(sum(1 for r in self.daily_records if r['is_rebalance']))
|
||||||
metrics_path = output_dir / 'simple_rotation_metrics.json'
|
metrics_path = output_dir / 'simple_rotation_metrics.json'
|
||||||
|
_sanitize_json(metrics)
|
||||||
with open(metrics_path, 'w', encoding='utf-8') as f:
|
with open(metrics_path, 'w', encoding='utf-8') as f:
|
||||||
json.dump(metrics, f, ensure_ascii=False, indent=2)
|
json.dump(metrics, f, ensure_ascii=False, indent=2)
|
||||||
print(f" + Metrics: {metrics_path}")
|
print(f" + Metrics: {metrics_path}")
|
||||||
|
|||||||
Reference in New Issue
Block a user