Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/factorlab/factors/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from factorlab.factors.base import Factor

__all__ = ["Factor"]
50 changes: 50 additions & 0 deletions src/factorlab/factors/astrology/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from factorlab.factors.astrology.all_features import AllAstrologyFeatures
from factorlab.factors.astrology.aspect_dynamics import AspectDynamics
from factorlab.factors.astrology.astrology import Astrology
from factorlab.factors.astrology.base import AstrologyFactor
from factorlab.factors.astrology.bradley_siderograph import BradleySiderograph
from factorlab.factors.astrology.commodity_natal_transits import CommodityNatalTransits
from factorlab.factors.astrology.cyclical_encoding import CyclicalEncoding
from factorlab.factors.astrology.declination_aspects import DeclinationAspects
from factorlab.factors.astrology.dewey_oscillators import DeweyOscillators
from factorlab.factors.astrology.eclipse_score import EclipseScore
from factorlab.factors.astrology.essential_dignity import EssentialDignity
from factorlab.factors.astrology.gann_square_of_nine import GannSquareOfNine
from factorlab.factors.astrology.heliocentric_features import HeliocentricFeatures
from factorlab.factors.astrology.lunar_features import LunarFeatures
from factorlab.factors.astrology.mcwhirter_nodal_cycle import McWhirterNodalCycle
from factorlab.factors.astrology.midpoint_activations import MidpointActivations
from factorlab.factors.astrology.natal_transit_aspects import NatalTransitAspects
from factorlab.factors.astrology.planetary_aspects import PlanetaryAspects
from factorlab.factors.astrology.planetary_ingress import PlanetaryIngress
from factorlab.factors.astrology.planetary_speed_features import PlanetarySpeedFeatures
from factorlab.factors.astrology.price_longitude_angles import PriceLongitudeAngles
from factorlab.factors.astrology.retrograde_indicator import RetrogradeIndicator
from factorlab.factors.astrology.synodic_cycle_phase import SynodicCyclePhase


__all__ = [
"Astrology",
"AstrologyFactor",
"AllAstrologyFeatures",
"PlanetaryAspects",
"BradleySiderograph",
"RetrogradeIndicator",
"PlanetaryIngress",
"NatalTransitAspects",
"LunarFeatures",
"CyclicalEncoding",
"PriceLongitudeAngles",
"SynodicCyclePhase",
"McWhirterNodalCycle",
"DeweyOscillators",
"EssentialDignity",
"DeclinationAspects",
"EclipseScore",
"PlanetarySpeedFeatures",
"HeliocentricFeatures",
"AspectDynamics",
"GannSquareOfNine",
"CommodityNatalTransits",
"MidpointActivations",
]
119 changes: 119 additions & 0 deletions src/factorlab/factors/astrology/all_features.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from __future__ import annotations

from typing import Optional

import pandas as pd

from factorlab.factors.astrology.aspect_dynamics import AspectDynamics
from factorlab.factors.astrology.base import AstrologyFactor
from factorlab.factors.astrology.bradley_siderograph import BradleySiderograph
from factorlab.factors.astrology.commodity_natal_transits import CommodityNatalTransits
from factorlab.factors.astrology.cyclical_encoding import CyclicalEncoding
from factorlab.factors.astrology.declination_aspects import DeclinationAspects
from factorlab.factors.astrology.dewey_oscillators import DeweyOscillators
from factorlab.factors.astrology.eclipse_score import EclipseScore
from factorlab.factors.astrology.essential_dignity import EssentialDignity
from factorlab.factors.astrology.gann_square_of_nine import GannSquareOfNine
from factorlab.factors.astrology.heliocentric_features import HeliocentricFeatures
from factorlab.factors.astrology.lunar_features import LunarFeatures
from factorlab.factors.astrology.mcwhirter_nodal_cycle import McWhirterNodalCycle
from factorlab.factors.astrology.midpoint_activations import MidpointActivations
from factorlab.factors.astrology.natal_transit_aspects import NatalTransitAspects
from factorlab.factors.astrology.planetary_aspects import PlanetaryAspects
from factorlab.factors.astrology.planetary_ingress import PlanetaryIngress
from factorlab.factors.astrology.planetary_speed_features import PlanetarySpeedFeatures
from factorlab.factors.astrology.price_longitude_angles import PriceLongitudeAngles
from factorlab.factors.astrology.retrograde_indicator import RetrogradeIndicator
from factorlab.factors.astrology.synodic_cycle_phase import SynodicCyclePhase
from factorlab.factors.astrology.common import get_dates_planets


class AllAstrologyFeatures(AstrologyFactor):
"""Composite factor that returns the full astrology feature set."""

def __init__(
self,
include_natal: bool = True,
include_price_angles: bool = False,
include_heliocentric: bool = False,
include_commodity_natal: bool = False,
include_gann_so9: bool = False,
anchor_price: float = 1.0,
scale: float = 1.0,
helio_ephemeris_df: Optional[pd.DataFrame] = None,
target_series: Optional[pd.Series] = None,
**kwargs,
):
super().__init__(
description="Composite astrology factor bundle.",
tags=["astrology", "composite", "feature_bundle"],
**kwargs,
)
self.include_natal = include_natal
self.include_price_angles = include_price_angles
self.include_heliocentric = include_heliocentric
self.include_commodity_natal = include_commodity_natal
self.include_gann_so9 = include_gann_so9
self.anchor_price = anchor_price
self.scale = scale
self.helio_ephemeris_df = helio_ephemeris_df
self.target_series = target_series

def _compute_astrology(self, ephemeris_df: pd.DataFrame) -> pd.DataFrame:
dates, planets = get_dates_planets(ephemeris_df)
features = []

def _append(df: pd.DataFrame) -> None:
if df is not None and not df.empty:
features.append(df)

_append(PlanetaryAspects().compute(ephemeris_df))
_append(BradleySiderograph().compute(ephemeris_df))
_append(RetrogradeIndicator().compute(ephemeris_df))
_append(PlanetaryIngress().compute(ephemeris_df))
_append(LunarFeatures().compute(ephemeris_df))
_append(CyclicalEncoding().compute(ephemeris_df))

if self.include_natal:
_append(NatalTransitAspects(natal_date=self.natal_date).compute(ephemeris_df))

if self.include_price_angles:
for planet in ["sun", "jupiter", "saturn"]:
if planet in planets:
_append(
PriceLongitudeAngles(
planet=planet,
anchor_price=self.anchor_price,
scale=self.scale,
).compute(ephemeris_df)
)

_append(SynodicCyclePhase().compute(ephemeris_df))
_append(McWhirterNodalCycle().compute(ephemeris_df))
_append(
DeweyOscillators(
data_driven=self.target_series is not None,
target_series=self.target_series,
).compute(ephemeris_df)
)
_append(EssentialDignity().compute(ephemeris_df))
_append(DeclinationAspects().compute(ephemeris_df))
_append(EclipseScore().compute(ephemeris_df))
_append(PlanetarySpeedFeatures().compute(ephemeris_df))
_append(AspectDynamics().compute(ephemeris_df))
_append(MidpointActivations().compute(ephemeris_df))

if self.include_heliocentric and self.helio_ephemeris_df is not None:
_append(HeliocentricFeatures(helio_ephemeris_df=self.helio_ephemeris_df).compute(ephemeris_df))

if self.include_commodity_natal:
_append(CommodityNatalTransits().compute(ephemeris_df))

if self.include_gann_so9 and self.price_df is not None:
_append(GannSquareOfNine(price_df=self.price_df).compute(ephemeris_df))

if not features:
return pd.DataFrame(index=dates)

result = pd.concat(features, axis=1)
return result.loc[:, ~result.columns.duplicated()]
65 changes: 65 additions & 0 deletions src/factorlab/factors/astrology/aspect_dynamics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from __future__ import annotations

from typing import List, Optional

import pandas as pd

from factorlab.factors.astrology.base import AstrologyFactor
from factorlab.factors.astrology.common import (
ASPECT_ANGLES,
DEFAULT_ORBS,
compute_aspect_distance,
get_dates_planets,
get_planet_longitude,
)


class AspectDynamics(AstrologyFactor):
"""Applying vs separating aspect-state flags."""

def __init__(
self,
planets: Optional[List[str]] = None,
aspect_types: Optional[List[str]] = None,
orb: float = 10.0,
**kwargs,
):
super().__init__(
description="Applying/separating state for planetary aspects.",
tags=["astrology", "aspects", "dynamics"],
**kwargs,
)
self.planets = planets
self.aspect_types = aspect_types
self.orb = orb

def _compute_astrology(self, ephemeris_df: pd.DataFrame) -> pd.DataFrame:
dates, available_planets = get_dates_planets(ephemeris_df)
planets = self.planets or available_planets
aspect_types = self.aspect_types or ["conjunction", "square", "trine", "opposition"]

results = {}
for i, p1 in enumerate(planets):
lon1 = get_planet_longitude(ephemeris_df, p1).reindex(dates)
if lon1.empty:
continue
for p2 in planets[i + 1 :]:
lon2 = get_planet_longitude(ephemeris_df, p2).reindex(dates)
if lon2.empty:
continue

for aspect_name in aspect_types:
angle = ASPECT_ANGLES[aspect_name]
aspect_orb = DEFAULT_ORBS.get(aspect_name, self.orb)
dist = compute_aspect_distance(lon1, lon2, angle)
in_orb = dist <= aspect_orb

dist_change = dist.diff()
applying = (dist_change < 0).astype(int)
separating = (dist_change > 0).astype(int)

prefix = f"{p1}_{p2}_{aspect_name}"
results[f"{prefix}_applying"] = applying * in_orb.astype(int)
results[f"{prefix}_separating"] = separating * in_orb.astype(int)

return pd.DataFrame(results) if results else pd.DataFrame(index=dates)
145 changes: 145 additions & 0 deletions src/factorlab/factors/astrology/astrology.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from __future__ import annotations

from typing import ClassVar, Dict, Optional, Type, Union

import pandas as pd

from factorlab.core.base_transform import BaseTransform
from factorlab.factors.base import Factor
from factorlab.factors.astrology.all_features import AllAstrologyFeatures
from factorlab.factors.astrology.aspect_dynamics import AspectDynamics
from factorlab.factors.astrology.bradley_siderograph import BradleySiderograph
from factorlab.factors.astrology.commodity_natal_transits import CommodityNatalTransits
from factorlab.factors.astrology.cyclical_encoding import CyclicalEncoding
from factorlab.factors.astrology.declination_aspects import DeclinationAspects
from factorlab.factors.astrology.dewey_oscillators import DeweyOscillators
from factorlab.factors.astrology.eclipse_score import EclipseScore
from factorlab.factors.astrology.essential_dignity import EssentialDignity
from factorlab.factors.astrology.gann_square_of_nine import GannSquareOfNine
from factorlab.factors.astrology.heliocentric_features import HeliocentricFeatures
from factorlab.factors.astrology.lunar_features import LunarFeatures
from factorlab.factors.astrology.mcwhirter_nodal_cycle import McWhirterNodalCycle
from factorlab.factors.astrology.midpoint_activations import MidpointActivations
from factorlab.factors.astrology.natal_transit_aspects import NatalTransitAspects
from factorlab.factors.astrology.planetary_aspects import PlanetaryAspects
from factorlab.factors.astrology.planetary_ingress import PlanetaryIngress
from factorlab.factors.astrology.planetary_speed_features import PlanetarySpeedFeatures
from factorlab.factors.astrology.price_longitude_angles import PriceLongitudeAngles
from factorlab.factors.astrology.retrograde_indicator import RetrogradeIndicator
from factorlab.factors.astrology.synodic_cycle_phase import SynodicCyclePhase


class Astrology(Factor):
"""Factory class for astrology factor indicators."""

_METHOD_MAP: ClassVar[Dict[str, Type[BaseTransform]]] = {
"planetary_aspects": PlanetaryAspects,
"bradley_siderograph": BradleySiderograph,
"retrograde_indicator": RetrogradeIndicator,
"planetary_ingress": PlanetaryIngress,
"natal_transit_aspects": NatalTransitAspects,
"lunar_features": LunarFeatures,
"cyclical_encoding": CyclicalEncoding,
"price_longitude_angles": PriceLongitudeAngles,
"synodic_cycle_phase": SynodicCyclePhase,
"mcwhirter_nodal_cycle": McWhirterNodalCycle,
"dewey_oscillators": DeweyOscillators,
"essential_dignity": EssentialDignity,
"declination_aspects": DeclinationAspects,
"eclipse_score": EclipseScore,
"planetary_speed_features": PlanetarySpeedFeatures,
"heliocentric_features": HeliocentricFeatures,
"aspect_dynamics": AspectDynamics,
"gann_square_of_nine": GannSquareOfNine,
"commodity_natal_transits": CommodityNatalTransits,
"midpoint_activations": MidpointActivations,
"all_features": AllAstrologyFeatures,
}

_ALIASES: ClassVar[Dict[str, str]] = {
"aspects": "planetary_aspects",
"bradley": "bradley_siderograph",
"retrograde": "retrograde_indicator",
"ingress": "planetary_ingress",
"natal_transits": "natal_transit_aspects",
"lunar": "lunar_features",
"cyclical": "cyclical_encoding",
"price_angles": "price_longitude_angles",
"synodic": "synodic_cycle_phase",
"mcwhirter": "mcwhirter_nodal_cycle",
"dewey": "dewey_oscillators",
"dignity": "essential_dignity",
"declination": "declination_aspects",
"eclipse": "eclipse_score",
"speed": "planetary_speed_features",
"helio": "heliocentric_features",
"dynamics": "aspect_dynamics",
"so9": "gann_square_of_nine",
"commodity_natal": "commodity_natal_transits",
"midpoints": "midpoint_activations",
"all": "all_features",
}

@classmethod
def get_factor_metadata(cls) -> pd.DataFrame:
data = []
for alias, factor_class in cls._METHOD_MAP.items():
try:
instance = factor_class()
data.append(
{
"Alias": alias,
"Class": factor_class.__name__,
"Description": instance.description,
}
)
except Exception as exc:
data.append(
{
"Alias": alias,
"Class": factor_class.__name__,
"Description": f"Instantiation Failed: {exc}",
}
)

return pd.DataFrame(data).set_index("Alias")

def __init__(self, method: str = "all_features", **kwargs):
super().__init__(
name="Astrology",
description="A factory for astrology factors.",
category="Astrology",
tags=["astrology", "ephemeris", "cycles"],
)

method = method.lower().strip()
self.method = self._ALIASES.get(method, method)
self.kwargs = kwargs

if self.method not in self._METHOD_MAP:
raise ValueError(
f"Invalid astrology factor method '{self.method}'. "
f"Method must be one of: {list(self._METHOD_MAP.keys())}"
)

factor_class = self._METHOD_MAP[self.method]
self._factor: Factor = factor_class(**self.kwargs)

@property
def inputs(self) -> list[str]:
return self._factor.inputs

def fit(
self,
X: Union[pd.Series, pd.DataFrame],
y: Optional[Union[pd.Series, pd.DataFrame]] = None,
) -> "Astrology":
self._factor.fit(X, y)
self._is_fitted = True
return self

def transform(self, data: Union[pd.Series, pd.DataFrame]) -> pd.DataFrame:
if not self._is_fitted:
raise RuntimeError("Astrology transform must be fitted before calling transform().")

return self._factor.transform(data)
Loading