| name | financial-astrology-pattern-search |
| description | Use when researching whether astrological aspect cycles correlate with returns across crypto, equities, ETFs, commodities, FX, or other investment assets using hermetic-alpha-library. Provides a reproducible event-study workflow with train/test validation, anti-overfitting rules, cross-asset checks, and clear publishable reporting. |
| version | 1.0.0 |
| author | Wauputra Research |
| license | MIT |
| compatibility | Requires Python 3.10+, network access for market data, and hermetic-alpha-library or a project that can install it. |
| metadata | {"tags":["research","finance","astrology","event-study","backtesting","hermetic-alpha"],"repository":"https://github.com/wauputr4/financial-astrology-skills","related_skills":["spike","github-repo-management"]} |
Financial Astrology Pattern Search
Overview
Use this skill to repeat the Bitcoin astrology-cycle experiment on other financial assets: crypto pairs, ETFs, indices, commodities, FX, or individual equities. The goal is not to produce deterministic trading signals. The goal is to dogfood hermetic-alpha-library, discover candidate timing patterns, and separate interesting hypotheses from cherry-picked noise.
Core idea:
- Fetch daily OHLCV candles for one or more assets.
- Generate planetary positions for the same date range.
- Scan astrological aspects as event windows.
- Collapse each active multi-day aspect window into its nearest-exact day.
- Compare forward returns after those exact days against the asset's own baseline return.
- Validate with chronological train/test splits and, when possible, cross-asset checks.
- Report findings as exploratory hypotheses with limitations and disclaimers.
When to Use
Use when the user asks to:
- “Cari pola astrology di ETH/SPY/gold/etc.”
- “Coba siklus besar untuk asset lain.”
- “Bandingin pattern BTC dengan saham/ETF/komoditas.”
- “Bikin kalender teori 2026–2028 untuk asset X.”
- “Cari pressure / trigger / release cycle di market.”
- “Dogfood hermetic-alpha-library ke data investasi lain.”
Do not use for:
- Live trading advice or entry/exit recommendations.
- Backtests that ignore transaction costs, slippage, liquidity, or risk management.
- Claims of causality from astrology to price movement.
- Publishing confident predictions without sample size, baseline, and validation caveats.
Quick Setup
Prefer a throwaway workspace so the repo state stays clean.
TMP=$(mktemp -d /tmp/hermetic-alpha-asset-search.XXXXXX)
git clone https://github.com/wauputr4/hermetic-alpha-library "$TMP"
cd "$TMP"
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install -U pip
python3 -m pip install -e ".[dev]"
python3 -m pytest -q
If the repo already exists locally, reuse it, but still verify tests/imports before trusting results:
cd /path/to/hermetic-alpha-library
. .venv/bin/activate
PYTHONPATH=src python3 - <<'PY'
from hermetic_alpha.astro import SwissEphemerisAdapter, generate_planet_positions, scan_aspect_series
from hermetic_alpha.market.providers import YahooFinanceProvider
from hermetic_alpha.labels import add_candle_forward_returns
print('hermetic-alpha imports OK')
PY
Release Rechecks
When the user asks whether a new hermetic-alpha-library release changes prior research, first rerun the same prior experiment scripts under the new tag before comparing thesis-level conclusions. See references/hermetic-alpha-v0.1.1-recheck.md for a concrete BTC v0.1.1 recheck pattern, including the package-name gotcha (hermetic-alpha, not hermetic-alpha-library) and the caveat that the bundled real-market example uses active aspect days rather than collapsed exact-date windows.
Asset Selection
Yahoo Finance symbols are accepted by the library's current provider via YahooFinanceProvider.fetch_daily(asset, start, end).
Common examples:
- Crypto:
BTC-USD, ETH-USD, SOL-USD, BNB-USD, XRP-USD
- US broad ETFs:
SPY, QQQ, IWM, DIA
- Macro ETFs:
GLD, SLV, TLT, HYG, UUP, USO
- Gold / XAU proxies: prefer
GC=F for COMEX gold futures when XAUUSD=X fails; use GLD/IAU for ETF validation.
- Sectors:
XLE, XLK, XLF, XLU
- Index proxies:
^GSPC, ^IXIC, ^DJI (verify Yahoo availability)
- FX Yahoo style:
EURUSD=X, JPY=X, GBPUSD=X
Rules:
- Record each asset's actual returned date range and candle count.
- Avoid mixing assets with very different history lengths without noting it.
- For assets with short history, increase skepticism and require fewer but explicitly labeled candidate findings.
- For equities/ETFs, remember non-trading days create gaps. Aspect series is daily UTC; event dates should map only to available candle timestamps.
Experiment Design
Default configuration
Use this as the first pass unless the user specifies otherwise:
BODIES = ["moon", "sun", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto"]
ASPECTS = ["conjunction", "opposition", "trine", "square", "sextile"]
ORB = 2.5
HORIZONS = [3, 7, 14, 30, 60, 90]
MIN_TRAIN_EVENTS = 3
MIN_TEST_EVENTS = 2
Run two variants when time allows:
- No Moon:
sun through pluto. Cleaner for slower cycle narratives.
- With Moon: adds
moon. More signals, but higher multiple-testing and overcount risk.
For “big astrology cycles”, emphasize outer planet / slower themes:
BIG_CYCLE_BODIES = ["jupiter", "saturn", "uranus", "neptune", "pluto"]
INNER_TRIGGER_BODIES = ["sun", "mercury", "venus", "mars"]
Event construction
Avoid counting every active aspect day as a separate observation. Consecutive days around the same aspect are one event window.
Correct workflow:
- Generate positions with daily step.
- Scan aspect days with
scan_aspect_series.
- Group by ordered feature:
body_a_body_b_aspect.
- Collapse consecutive rows into a window.
- Pick the exact event date as the row with minimum
orb.
- Measure forward returns from that exact date only.
This was the key improvement in the BTC experiment: window → exact-date event instead of treating a 3–6 day aspect as 3–6 independent samples.
Labels and baseline
Use forward returns for each asset:
labels = add_candle_forward_returns(candles, HORIZONS)
For every horizon, calculate:
- baseline average return over all valid candle days
- event average return
- event median return
- bullish percentage (
return > 0)
- event min/max
- train edge vs baseline
- test edge vs baseline
Always compare an event against the same asset's baseline and same date range. Do not compare ETH event returns to BTC baseline, etc.
Train/test split
Use chronological validation:
- Long crypto history: train through
2020-12-31, test after.
- ETFs/equities with longer history: train through a reasonable midpoint or regime boundary, then test.
- Short assets: use 60/40 chronological split, but label the result weak.
Scoring heuristic:
train_edge = train_avg_return - baseline_avg_return
test_edge = test_avg_return - baseline_avg_return
same_direction = train_edge and test_edge have same sign
robustness_score = average absolute edge if same_direction, otherwise negative disagreement penalty
A pattern is more interesting if:
- Train and test edges point in the same direction.
- Median return agrees with average direction.
- Bullish percentage is directionally consistent.
- Event count is not tiny.
- It appears across related assets or at least does not invert badly out-of-sample.
Cross-Asset Workflow
For multiple assets, do not simply rank the best feature per asset and call it a theory. Use staged validation.
Stage 1 — Discover per asset
Run the exact-event search for each asset independently.
Output per asset:
- top robust bullish features
- weakest robust / bearish features
- theme aggregation by planet, aspect, pair, and horizon
- counts: candles, raw aspect days, exact windows, features scored
Stage 2 — Compare common themes
Group features into themes:
- planet tag:
saturn, venus, pluto
- aspect tag:
opposition, conjunction, etc.
- pair tag:
venus_pluto, jupiter_saturn
- planet-aspect tag:
saturn_conjunction, venus_opposition
- horizon tag:
h7, h30, h90
Look for themes where direction and horizon make conceptual sense across assets, e.g.:
- risk-on crypto responds to
venus_opposition differently from defensive bonds
saturn hard/conjunction themes may behave as pressure/cooldown in risk assets
mars inner aspects may act more like short-term triggers than long-term regimes
Stage 3 — Validate an anchored theory
If BTC suggests a theory, test it on other assets without re-selecting features.
Example anchored theory from the BTC experiment:
venus opposition with mars/jupiter/saturn/uranus/pluto → possible release/risk-on windows
mars conjunction/trine with sun/mercury/venus → possible short-term triggers
saturn conjunction with personal planets → pressure/caution windows
mercury_venus conjunction/sextile → cooldown candidates
For each new asset, classify windows using the same rules and measure returns. Do not optimize the rules per asset unless it is explicitly a second-stage exploratory pass.
Minimal Generic Script Skeleton
Use this as a starting point. Customize ASSETS, START, TRAIN_END, and output paths.
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
from datetime import date, timedelta, timezone
import json, math, statistics
from hermetic_alpha.astro import SwissEphemerisAdapter, generate_planet_positions, scan_aspect_series
from hermetic_alpha.labels import add_candle_forward_returns
from hermetic_alpha.market.providers import YahooFinanceProvider
ASSETS = ["BTC-USD", "ETH-USD", "SPY", "QQQ", "GLD", "TLT"]
BODIES = ["moon", "sun", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto"]
ASPECTS = ["conjunction", "opposition", "trine", "square", "sextile"]
ASPECT_ORBS = {a: 2.5 for a in ASPECTS}
HORIZONS = [3, 7, 14, 30, 60, 90]
START = "2014-09-17"
TRAIN_END = "2020-12-31"
MIN_TRAIN_EVENTS = 3
MIN_TEST_EVENTS = 2
@dataclass(frozen=True)
class WindowEvent:
feature: str
start_idx: int
end_idx: int
exact_idx: int
min_orb: float
max_strength: float
def pct(x):
return None if x is None else round(x * 100, 3)
def valid(v):
return isinstance(v, (int, float)) and math.isfinite(float(v))
def feature_key(e):
a, b = sorted([e.body_a, e.body_b])
return f"{a}_{b}_{e.aspect}"
def ret(labels, idx, h):
if idx < 0 or idx >= len(labels):
return None
v = labels[idx].get(f"return_{h}d")
return float(v) if valid(v) else None
def block_to_event(feature, block):
exact_idx, exact_e = min(block, key=lambda x: (x[1].orb, x[0]))
return WindowEvent(
feature=feature,
start_idx=block[0][0],
end_idx=block[-1][0],
exact_idx=exact_idx,
min_orb=float(exact_e.orb),
max_strength=max(float(e.strength) for _, e in block),
)
def build_window_events(events, timestamp_to_idx):
by_feature = defaultdict(list)
for e in events:
idx = timestamp_to_idx.get(e.timestamp)
if idx is not None:
by_feature[feature_key(e)].append((idx, e))
out = []
for feature, rows in by_feature.items():
rows = sorted(rows, key=lambda x: x[0])
block, prev = [], None
for idx, e in rows:
if prev is None or idx <= prev + 1:
block.append((idx, e))
else:
out.append(block_to_event(feature, block))
block = [(idx, e)]
prev = idx
if block:
out.append(block_to_event(feature, block))
return out
def summarize(vals):
vals = [v for v in vals if v is not None]
if not vals:
return {"n": 0, "avg_pct": None, "median_pct": None, "bullish_pct": None}
return {
"n": len(vals),
"avg_pct": pct(statistics.mean(vals)),
"median_pct": pct(statistics.median(vals)),
"bullish_pct": pct(sum(v > 0 for v in vals) / len(vals)),
}
def score_feature(feature, events, labels, candles, train_cut_idx, baseline):
train = [e for e in events if e.exact_idx <= train_cut_idx]
test = [e for e in events if e.exact_idx > train_cut_idx]
if len(train) < MIN_TRAIN_EVENTS or len(test) < MIN_TEST_EVENTS:
return None
horizons = {}
best = None
for h in HORIZONS:
train_vals = [ret(labels, e.exact_idx, h) for e in train]
test_vals = [ret(labels, e.exact_idx, h) for e in test]
all_vals = [ret(labels, e.exact_idx, h) for e in events]
train_vals = [x for x in train_vals if x is not None]
test_vals = [x for x in test_vals if x is not None]
if len(train_vals) < MIN_TRAIN_EVENTS or len(test_vals) < MIN_TEST_EVENTS:
continue
bavg = statistics.mean(baseline[h])
train_edge = statistics.mean(train_vals) - bavg
test_edge = statistics.mean(test_vals) - bavg
same = (train_edge >= 0 and test_edge >= 0) or (train_edge <= 0 and test_edge <= 0)
robustness = (abs(train_edge) + abs(test_edge)) / 2 if same else -abs(train_edge - test_edge)
horizons[h] = {
"baseline_avg_pct": pct(bavg),
"train": summarize(train_vals),
"test": summarize(test_vals),
"all": summarize(all_vals),
"train_edge_pp": round(train_edge * 100, 3),
"test_edge_pp": round(test_edge * 100, 3),
"same_direction": same,
"robustness_score_pp": round(robustness * 100, 3),
}
if best is None or (robustness, h) > best:
best = (robustness, h)
if not horizons:
return None
best_h = best[1]
return {
"feature": feature,
"event_count": len(events),
"train_events": len(train),
"test_events": len(test),
"first_exact": candles[events[0].exact_idx].timestamp.date().isoformat(),
"last_exact": candles[events[-1].exact_idx].timestamp.date().isoformat(),
"best_horizon": best_h,
"best_robustness_score_pp": horizons[best_h]["robustness_score_pp"],
"best_train_edge_pp": horizons[best_h]["train_edge_pp"],
"best_test_edge_pp": horizons[best_h]["test_edge_pp"],
"horizons": horizons,
}
def run_asset(asset):
provider = YahooFinanceProvider(timeout=30)
candles = provider.fetch_daily(asset, START, date.today().isoformat())
labels = add_candle_forward_returns(candles, HORIZONS)
timestamp_to_idx = {c.timestamp: i for i, c in enumerate(candles)}
train_cut_idx = max(i for i, c in enumerate(candles) if c.timestamp.date().isoformat() <= TRAIN_END)
adapter = SwissEphemerisAdapter()
positions = generate_planet_positions(
adapter,
candles[0].timestamp.astimezone(timezone.utc),
candles[-1].timestamp.astimezone(timezone.utc),
timedelta(days=1),
BODIES,
)
aspect_days = scan_aspect_series(positions, ASPECT_ORBS)
windows = build_window_events(aspect_days, timestamp_to_idx)
baseline = {h: [ret(labels, i, h) for i in range(len(labels))] for h in HORIZONS}
baseline = {h: [x for x in vals if x is not None] for h, vals in baseline.items()}
by_feature = defaultdict(list)
for e in sorted(windows, key=lambda x: x.exact_idx):
by_feature[e.feature].append(e)
rows = []
for feature, events in by_feature.items():
s = score_feature(feature, events, labels, candles, train_cut_idx, baseline)
if s:
rows.append(s)
rows = sorted(rows, key=lambda r: r["best_robustness_score_pp"], reverse=True)
return {
"asset": asset,
"period": {
"start": candles[0].timestamp.date().isoformat(),
"end": candles[-1].timestamp.date().isoformat(),
"candles": len(candles),
"train_end": TRAIN_END,
},
"config": {"bodies": BODIES, "aspects": ASPECTS, "orb_degrees": 2.5, "horizons_days": HORIZONS},
"counts": {"positions": len(positions), "raw_aspect_days": len(aspect_days), "exact_windows": len(windows), "features_scored": len(rows)},
"top_robust_features": rows[:20],
"weakest_robust_features": list(reversed(rows[-20:])),
"note": "Exploratory event study only; not financial advice.",
}
if __name__ == "__main__":
results = [run_asset(asset) for asset in ASSETS]
print(json.dumps({"results": results}, indent=2, sort_keys=True))
Regime-Label / Peak-Bottom Enrichment Workflow
When the user asks which aspects or themes appear during bull, bear, market peak, or market bottom regimes, switch from pure forward-return event study to a label-enrichment screen. A concrete SPX example is preserved in references/spx-regime-label-enrichment.md.
Recommended approach:
- Define market-state labels explicitly before scanning aspects.
- Bear: active decline from major all-time-high peak to major bottom inside a >=20% drawdown cycle.
- Bull: days outside active peak-to-bottom bear declines.
- Peak window: ±30 trading days around major peaks.
- Bottom window: ±30 trading days around major bottoms.
- Optional one-sided labels:
pre_peak_30td and post_bottom_30td.
- Generate astrology events exactly as usual: scan aspects, group consecutive active days, choose nearest-exact date, and map non-trading exact dates to the next available trading day within a documented lag.
- Aggregate events into class-level buckets rather than only exact features:
planet:*, aspect:*, planet_aspect:*, pair:*
- hard/soft aspect families
- outer-outer, outer-personal, personal-personal pair families
- existing theory themes such as Saturn pressure, Jupiter–Uranus major, Mercury–Venus cooldown, etc.
- For each bucket and market-state label, compute enrichment metrics:
- event count, observed events inside label, event rate, base day share, edge in percentage points, lift, and approximate binomial z-score.
- Report as enrichment / coincidence research, not prediction. Major peak and bottom labels are hindsight-defined; freeze any candidate themes before using them in future calendars.
Requester-facing summary guidance:
- Use the conversation language for chat summaries unless the requester asks otherwise.
- Use simple buckets:
bull / constructive, bear / pressure, peak-risk, bottom / reversal.
- State that SPX bull enrichment is less distinctive because SPX is mostly bull by baseline; often the useful signal is which pressure themes cluster around bear/peak windows.
- Include caveats: hindsight labels, multiple testing, small outer-planet samples, and need to validate against SPY/ES/Nasdaq/Dow.
Calendar / Forecast Workflow
Only build a future calendar after a theory is defined and validated enough to be worth watching.
- Convert robust historical themes into explicit classification rules.
- Generate future planetary aspects for a fixed period, e.g.
2026-01-01 to 2028-12-31.
- Collapse windows and pick exact dates.
- Assign categories such as:
bullish_release
bullish_trigger
pressure_warning
cooldown_warning
mixed_watch
- Add a legend explaining horizon: short trigger
3–14d, release 7–30d, pressure 30–90d.
- State clearly that this is a watchlist / theory calendar, not a trade plan.
Reporting Format
For research-paper artifacts, use English by default unless the requester explicitly requests another language. Chat progress/final summaries may follow the conversation language, but the Markdown/PDF paper body and GitHub Discussion should default to English for broader reuse.
Recommended structure:
## Eksperimen yang gw jalankan
- Asset: ...
- Periode: ...
- Aspect: ...
- Orb: ...
- Horizon: ...
- Train/test: ...
## Baseline asset
- Avg return 7d/30d/90d: ...
## Temuan paling menarik
1. Feature/theme: ...
- Event count: ...
- Train edge: ...
- Test edge: ...
- Avg/median/bullish%: ...
- Interpretasi: ...
## Yang harus dicurigai
- Sample kecil / multiple testing / regime berubah / Yahoo data bukan audit-grade.
## Teori kerja
- Pressure: ...
- Trigger: ...
- Release: ...
## Next step
- Validate ke asset lain / bikin calendar / tulis artikel.
Disclaimer: bukan financial advice; ini eksplorasi data dan dogfood library.
Comprehensive publishable research package
When the requester asks for comprehensive research, especially “format md, pdf, publish di discussion repository”, produce a repo-ready package rather than only a chat summary:
- Write a full Markdown paper with YAML frontmatter, executive summary, data/methodology, baseline, feature-level tables, theme-level interpretation, caveats, next steps, and disclaimer.
- Generate a PDF from the Markdown via the
pandoc-pdf-generation skill. For wide result tables, prefer landscape A4, small font, DejaVu Sans/Mono, TOC, and numbered sections.
- Verify the PDF artifact: check file type/size and use an available PDF inspection tool such as
mutool info / mutool draw -F txt when pdfinfo or pdftotext are unavailable.
- Commit the Markdown and PDF under a stable repo path such as
research/<asset-or-topic>-<version>/README.md plus the PDF file.
- Publish or update a GitHub Discussion that links to the committed Markdown, PDF, and commit. Before creating a new discussion, list/search recent repo discussions for the same asset/topic; if a relevant discussion already exists, update it or add a clear follow-up comment instead of duplicating threads. Include the full paper body or a substantial excerpt so the discussion is readable without downloading artifacts.
- Verify publication with real outputs: Discussion URL, HTTP 200 for discussion/README/PDF links, body length or fetched title, commit SHA, PDF file type/size/page count, and test/import result where possible.
- In the final chat response, send clickable Markdown links and attach the local
.md and .pdf when the platform supports native file delivery. Do not merely print local paths in backticks when a native attachment mechanism is available.
- If the requester says the result is “kurang komprehensif” or reminds you to “bikin discussion/update”, treat it as a workflow correction: expand the paper substantially, commit/update repo artifacts, create or update the GitHub Discussion, verify all links return HTTP 200, then resend or attach the updated MD/PDF artifacts.
For article-style outputs in a casual Indonesian voice:
- Use
gw/Lo naturally.
- Explain why the experiment is interesting even for skeptics.
- Avoid claiming “astrology predicts price”. Prefer “event windows can be tested statistically”.
- Mention limitations before readers over-trust the pattern.
- Close with practical reflection or a question, not a trading call.
Anti-Overfitting Rules
Always enforce these:
- No one-line miracle claims. Every finding needs period, sample size, horizon, baseline, and train/test direction.
- Do not optimize everything at once. If you tune bodies, orbs, aspects, horizons, and split dates until it looks good, label it exploratory.
- Collapse windows. Do not count every active aspect day as an independent event.
- Keep Moon separate. Moon creates many events and can dominate counts.
- Prefer robust direction over highest average. One giant return can inflate averages.
- Check medians. If average is strong but median is weak/opposite, say so.
- Use same-asset baseline. Different assets have different drift and volatility.
- Cross-asset validation beats prettier charts. A pattern found on BTC is more credible if tested unchanged on ETH/SPY/GLD/TLT.
- No causal language. Use “correlated with”, “coincided with”, “candidate”, “watchlist”.
- No trading advice. Never present as buy/sell/short/long instruction.
Common Pitfalls
- Yahoo symbols fail silently or have odd histories. Verify actual candle count and first/last dates for every asset.
- Equity calendars skip weekends/holidays. Map aspect timestamps to candle timestamps; if no candle exists, skip or map explicitly with a documented rule.
- Multiple testing creates false positives. Treat top features as hypotheses until validated out-of-sample.
- Small event counts are fragile. Outer-planet aspects can have very few samples. Report them as narrative hypotheses, not statistical evidence.
- Regime changes matter. BTC pre-2017, COVID era, ETF era, rate-hike era, and post-halving periods may behave differently.
- Average can lie. Always include median and bullish percentage.
- Calendar output can look too authoritative. Label future dates as watch windows, not predictions.
- Asset-specific stories are tempting. Do not retrofit mythology after seeing returns; separate discovered data from interpretation.
Verification Checklist
Before reporting: