Merge origin/main into codex/roadmap-publish
This commit is contained in:
@@ -6,6 +6,93 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
def calculate_distribution_risk(row: dict, kospi_ret_5d: float) -> dict:
|
||||
close = float(row.get("Close") or row.get("close") or 0.0)
|
||||
ma20 = float(row.get("MA20") or row.get("ma20") or 0.0)
|
||||
high = float(row.get("High") or row.get("high") or close)
|
||||
low = float(row.get("Low") or row.get("low") or close)
|
||||
volume = row.get("Volume") or row.get("volume")
|
||||
avg_vol_5d = row.get("AvgVolume5d") or row.get("avg_volume_5d") or row.get("Avg_Volume_5D")
|
||||
flow_credit = row.get("FlowCredit") or row.get("flow_credit") or row.get("Flow_Credit")
|
||||
|
||||
# Coerce to float if valid
|
||||
volume = float(volume) if volume not in (None, "") else None
|
||||
avg_vol_5d = float(avg_vol_5d) if avg_vol_5d not in (None, "") else None
|
||||
flow_credit = float(flow_credit) if flow_credit not in (None, "") else None
|
||||
|
||||
price_above_ma20 = close > 0 and ma20 > 0 and close > ma20
|
||||
|
||||
score = 0
|
||||
reasons = []
|
||||
|
||||
frg5d = row.get("Frg5D") or row.get("frg_5d") or row.get("Frg_5D")
|
||||
inst5d = row.get("Inst5D") or row.get("inst_5d") or row.get("Inst_5D")
|
||||
frg5d = float(frg5d) if frg5d not in (None, "") else None
|
||||
inst5d = float(inst5d) if inst5d not in (None, "") else None
|
||||
|
||||
if frg5d is not None and inst5d is not None and frg5d < 0 and inst5d < 0:
|
||||
score += 30
|
||||
reasons.append("smart_money_outflow")
|
||||
|
||||
if volume is not None and avg_vol_5d is not None and avg_vol_5d > 0 and volume < avg_vol_5d * 0.80:
|
||||
score += 20
|
||||
reasons.append("volume_fade_after_surge")
|
||||
|
||||
if high > low and close > 0:
|
||||
upper_wick_ratio = (high - close) / max(high - low, 1.0)
|
||||
if upper_wick_ratio >= 0.45 and price_above_ma20:
|
||||
score += 15
|
||||
reasons.append("upper_wick_distribution")
|
||||
|
||||
if flow_credit is not None and flow_credit < 0.40:
|
||||
score += 20
|
||||
reasons.append("flow_credit_low")
|
||||
|
||||
ret5d = row.get("Ret5D") or row.get("ret_5d") or row.get("Ret_5D")
|
||||
ret5d = float(ret5d) if ret5d not in (None, "") else None
|
||||
if ret5d is not None and kospi_ret_5d is not None and ret5d < kospi_ret_5d - 3:
|
||||
score += 15
|
||||
reasons.append("sector_relative_lag")
|
||||
|
||||
ac_gate = row.get("acGate") or row.get("ac_gate")
|
||||
if ac_gate and "CLIMAX" in str(ac_gate).upper():
|
||||
score += 15
|
||||
reasons.append("anti_climax_gate")
|
||||
|
||||
ac_total = row.get("acTotal") or row.get("ac_total")
|
||||
ac_total = float(ac_total) if ac_total not in (None, "") else None
|
||||
if ac_total is not None and ac_total >= 2:
|
||||
score += 10
|
||||
reasons.append("ac_total_gte2")
|
||||
|
||||
val_surge_pct = row.get("valSurgePct") or row.get("val_surge_pct")
|
||||
val_surge_pct = float(val_surge_pct) if val_surge_pct not in (None, "") else None
|
||||
if val_surge_pct is not None and val_surge_pct >= 40 and price_above_ma20 and (flow_credit is None or flow_credit < 0.50):
|
||||
score += 10
|
||||
reasons.append("val_surge_no_flow_support")
|
||||
|
||||
high52w = row.get("high52w") or row.get("high_52w") or row.get("High_52W")
|
||||
high52w = float(high52w) if high52w not in (None, "") and float(high52w) > 0 else None
|
||||
near_new_high = (high52w is not None and close > 0 and close >= high52w * 0.97) or (ma20 > 0 and close > ma20 * 1.15)
|
||||
if near_new_high and volume is not None and avg_vol_5d is not None and avg_vol_5d > 0 and volume < avg_vol_5d * 0.80:
|
||||
score += 12
|
||||
reasons.append("new_high_volume_contraction")
|
||||
|
||||
if ret5d is not None and ret5d >= 5 and flow_credit is not None and flow_credit < 0.45:
|
||||
score += 10
|
||||
reasons.append("surge_weak_flow")
|
||||
|
||||
final_score = min(100, max(0, score))
|
||||
state = "BLOCK_BUY" if final_score >= 70 else "TRIM_REVIEW" if final_score >= 55 else "PASS"
|
||||
pre_dist_warning = "EARLY_WARNING" if ("new_high_volume_contraction" in reasons or "surge_weak_flow" in reasons) else "NONE"
|
||||
|
||||
return {
|
||||
"distribution_risk_score": final_score,
|
||||
"anti_distribution_state": state,
|
||||
"pre_distribution_warning": pre_dist_warning,
|
||||
"reason_codes": reasons
|
||||
}
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--json", default="GatherTradingData.json")
|
||||
@@ -18,22 +105,23 @@ def main():
|
||||
sys.exit(1)
|
||||
|
||||
raw = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
core_satellite = raw.get("data", {}).get("core_satellite", []) or []
|
||||
data_feed = raw.get("data", {}).get("data_feed", []) or []
|
||||
|
||||
# Find KOSPI ret_5d if present in macro to align with kospiRet5d
|
||||
macro = raw.get("data", {}).get("macro", []) or []
|
||||
kospi_ret_5d = 0.0
|
||||
for m in macro:
|
||||
if str(m.get("Ticker") or m.get("ticker")).strip() == "KOSPI":
|
||||
kospi_ret_5d = float(m.get("Ret5D") or m.get("ret_5d") or 0.0)
|
||||
break
|
||||
|
||||
scores = {}
|
||||
for row in core_satellite:
|
||||
ticker = row.get("Ticker")
|
||||
for row in data_feed:
|
||||
ticker = row.get("Ticker") or row.get("ticker")
|
||||
if not ticker:
|
||||
continue
|
||||
close = row.get("Close") or 0.0
|
||||
ma20 = row.get("MA20") or close
|
||||
|
||||
# Calculate distribution risk score: 0 to 100
|
||||
score = min(100, max(0, int((close - ma20) / ma20 * 200)))
|
||||
scores[ticker] = {
|
||||
"distribution_risk_score": score,
|
||||
"status": "HIGH" if score >= 50 else "NORMAL"
|
||||
}
|
||||
res = calculate_distribution_risk(row, kospi_ret_5d)
|
||||
scores[ticker] = res
|
||||
|
||||
out_path = ROOT / args.out
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -248,14 +248,58 @@ def sync_sector_insights_via_clasp_run() -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def run_pre_deploy_linter() -> bool:
|
||||
print("[deploy_gas] Running pre-deploy gas thin-adapter audit...")
|
||||
# Run auditor v1
|
||||
audit_res = subprocess.run(
|
||||
["python", "tools/audit_gas_thin_adapter_v1.py"],
|
||||
cwd=str(ROOT),
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
if audit_res.returncode != 0:
|
||||
print("[deploy_gas] Error: tools/audit_gas_thin_adapter_v1.py failed")
|
||||
print(audit_res.stdout)
|
||||
print(audit_res.stderr)
|
||||
return False
|
||||
|
||||
# Run validator v2
|
||||
validate_res = subprocess.run(
|
||||
["python", "tools/validate_gas_thin_adapter_v2.py"],
|
||||
cwd=str(ROOT),
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
print(validate_res.stdout)
|
||||
if validate_res.returncode != 0:
|
||||
print("[deploy_gas] ABORT: GAS Thin Adapter validation failed!")
|
||||
if validate_res.stderr:
|
||||
print("STDERR: " + validate_res.stderr)
|
||||
return False
|
||||
|
||||
print("[deploy_gas] Pre-deploy thin-adapter audit PASS")
|
||||
return True
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="GAS auto-deploy")
|
||||
parser.add_argument("--dry-run", action="store_true", help="List files without writing")
|
||||
parser.add_argument("--skip-push", action="store_true", help="Bundle only, skip clasp push")
|
||||
parser.add_argument("--skip-lint", action="store_true", help="Skip pre-deploy thin-adapter validation")
|
||||
parser.add_argument("--sync-sector-insights", action="store_true", help="POST sector insight JSON to a deployed GAS web app")
|
||||
parser.add_argument("--webapp-url", default=os.environ.get("GAS_WEBAPP_URL", DEFAULT_WEBAPP_URL), help="Apps Script web app URL for sync POST")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.skip_lint:
|
||||
if not run_pre_deploy_linter():
|
||||
raise SystemExit(1)
|
||||
|
||||
ok = build_deploy(dry_run=args.dry_run)
|
||||
if not ok:
|
||||
print("[deploy_gas] Some source files missing -- check warnings above")
|
||||
|
||||
@@ -8,24 +8,25 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
REQUIRED_PATTERNS = {
|
||||
".gitea/workflows/kis_data_collection.yml": [
|
||||
"secrets.KIS_APP_KEY_TEST",
|
||||
"secrets.KIS_APP_SECRET_TEST",
|
||||
"secrets.KIS_APP_KEY",
|
||||
"secrets.KIS_APP_SECRET",
|
||||
"vars.KIS_APP_KEY_TEST",
|
||||
"vars.KIS_APP_SECRET_TEST",
|
||||
"vars.KIS_APP_KEY",
|
||||
"vars.KIS_APP_SECRET",
|
||||
],
|
||||
".gitea/workflows/qualitative_sell_strategy.yml": [
|
||||
"secrets.KIS_APP_KEY_TEST",
|
||||
"secrets.KIS_APP_SECRET_TEST",
|
||||
"secrets.KIS_APP_KEY",
|
||||
"secrets.KIS_APP_SECRET",
|
||||
"vars.KIS_APP_KEY_TEST",
|
||||
"vars.KIS_APP_SECRET_TEST",
|
||||
"vars.KIS_APP_KEY",
|
||||
"vars.KIS_APP_SECRET",
|
||||
],
|
||||
".gitea/workflows/ci.yml": [
|
||||
"secrets.KIS_APP_KEY_TEST",
|
||||
"secrets.KIS_APP_SECRET_TEST",
|
||||
"vars.KIS_APP_KEY_TEST",
|
||||
"vars.KIS_APP_SECRET_TEST",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
evidence: dict[str, dict[str, bool]] = {}
|
||||
|
||||
Reference in New Issue
Block a user