Real outputs of the engine, computed when this page was built, with the request and its SHA-256: send the same
request to the API and you get the same hash. The strategies in the code show the mechanics of using a planetary line
or an event as a feature; they are illustrations, not trading signals, and no performance figure is shown.
Install: pip install "astro-agents @ git+https://github.com/aidatatools-dev/astro-agents#subdirectory=clients/python" · all files: clients/python/examples
1. Gann planetary lines as a time series
Geocentric longitudes (unwrapped degrees), weekly, 2022-2024: Jupiter, Saturn and the Mean of 5
(Jupiter..Pluto). The retrograde loops are visible as the dips. Hover a week for its values.Table view
POST /v1/finance/planetary-lines · result_sha256 3cf4463614a1eddd640ec73c9603bd85f666e8739a5d87903d115518869cbacd
2. A feature for vectorbt: the heliocentric Jupiter-Saturn angle
The same request's heliocentric columns, folded to 0-180°. The shaded band is the example rule's
"within 30° of a square" condition; the vectorbt script turns it into entries and exits on your candles.Table view
UTC
helio Jupiter-Saturn angle
2022-01-01
24.3657°
2022-01-29
26.0255°
2022-02-26
27.6899°
2022-03-26
29.3585°
2022-04-23
31.0308°
2022-05-21
32.7065°
2022-06-18
34.3850°
2022-07-16
36.0661°
2022-08-13
37.7491°
2022-09-10
39.4336°
2022-10-08
41.1191°
2022-11-05
42.8052°
2022-12-03
44.4914°
2022-12-31
46.1771°
2023-01-28
47.8619°
2023-02-25
49.5453°
2023-03-25
51.2268°
2023-04-22
52.9059°
2023-05-20
54.5820°
2023-06-17
56.2548°
2023-07-15
57.9238°
2023-08-12
59.5884°
2023-09-09
61.2483°
2023-10-07
62.9029°
2023-11-04
64.5519°
2023-12-02
66.1948°
2023-12-30
67.8312°
2024-01-27
69.4607°
2024-02-24
71.0829°
2024-03-23
72.6974°
2024-04-20
74.3040°
2024-05-18
75.9022°
2024-06-15
77.4919°
2024-07-13
79.0726°
2024-08-10
80.6441°
2024-09-07
82.2062°
2024-10-05
83.7586°
2024-11-02
85.3011°
2024-11-30
86.8336°
2024-12-28
88.3558°
from astro_agents import Client
from _candles import load_candles
client = Client()
candles = load_candles("BTC-USD", "2022-01-01", "2024-12-31").set_index("open_time")
lines = client.planetary_lines(start="2022-01-01", end="2024-12-31", bodies=["jupiter", "saturn"],
averages=["mean_of_5"], unwrap=True)
# Align the daily line values on the candles (both indexed by UTC day)
close = candles["close"]
feat = lines.reindex(close.index.floor("D"), method="ffill")
feat.index = close.index
# Example feature: the heliocentric Jupiter-Saturn angle, folded to 0-180 degrees
angle = (feat["helio.jupiter"] - feat["helio.saturn"]) % 360
angle = np.minimum(angle, 360 - angle)
# Example rule: long while the angle is within 30 degrees of a square (90), flat otherwise
inside = (angle - 90).abs() < 30
entries = inside & ~inside.shift(1, fill_value=False)
exits = ~inside & inside.shift(1, fill_value=False)
pf = vbt.Portfolio.from_signals(close, entries, exits, fees=0.001, freq="1D")
print(pf.stats()[["Total Return [%]", "Max Drawdown [%]", "Total Trades", "Sharpe Ratio"]])
print("buy & hold:", f"{close.iloc[-1] / close.iloc[0] - 1:+.1%}")
print("line data hashes:", lines.attrs["result_sha256"])
3. Event calendar onto OHLC candles
Eclipses and stations in 2024, to the minute (UTC):
POST /v1/finance/events · result_sha256 a85fc3159ecd8f06a1ecaf8869a1698c45fd06197fd9aa4fcaa83b230504deb1
from astro_agents import Client, merge_events, verify_dataframe
from _candles import load_candles
client = Client()
candles = load_candles("BTC-USD", "2022-01-01", "2024-12-31")
events = client.events("2022-01-01", "2024-12-31", include=["station", "eclipse", "ingress"])
assert verify_dataframe(events) # every result hash recomputed offline
# Each event onto the candle whose day contains it (events are timed to the UTC minute)
day = events["time"].dt.floor("D").astype(candles["open_time"].dtype)
per_day = events.groupby(day)["key"].size()
df = candles.copy()
df["n_events"] = df["open_time"].dt.floor("D").map(per_day).fillna(0).astype(int)
# Or the latest event at or before each candle, in one call:
latest = merge_events(candles, events, on="open_time", tolerance="3D")
df["ret_next"] = df["close"].pct_change().shift(-1)
# Average next-day return on event days vs other days
print(df.groupby(df["n_events"] > 0)["ret_next"].describe())
# Mercury stations: the next 5 days' return after each one
stations = events[(events["type"] == "station") & (events["body"] == "mercury")]
fwd = []
for t in stations["time"]:
window = candles[candles["open_time"] >= t.floor("D")].head(6)
if len(window) == 6:
fwd.append(window["close"].iloc[-1] / window["close"].iloc[0] - 1)
print(f"Mercury stations: {len(fwd)}, mean 5-day return {pd.Series(fwd).mean():+.2%}")
# Keep the provenance with the study: same request, same bytes
print("result hashes:", events.attrs["result_sha256"])
4. backtrader: a planetary line as an extra data line
from astro_agents import Client
from _candles import load_candles
client = Client()
candles = load_candles("BTC-USD", "2022-01-01", "2024-12-31").set_index("open_time")
lines = client.planetary_lines(start="2022-01-01", end="2024-12-31", bodies=["mars"], perspective="geocentric")
df = candles.join(lines[["geo.mars"]].reindex(candles.index.floor("D")).set_axis(candles.index), how="left")
df = df.rename(columns={"geo.mars": "mars"}).ffill()
df.index = df.index.tz_convert(None) # backtrader wants naive datetimes
class AstroData(bt.feeds.PandasData):
lines = ("mars",)
params = (("mars", "mars"),)
class MarsSignChange(bt.Strategy):
"""Example: enter when Mars changes zodiac sign (every 30 degrees), exit 10 bars later."""
def __init__(self):
self.entry_bar = None
def next(self):
sign_now, sign_before = int(self.data.mars[0] // 30), int(self.data.mars[-1] // 30)
if not self.position and sign_now != sign_before:
self.buy()
self.entry_bar = len(self)
elif self.position and len(self) - self.entry_bar >= 10:
self.close()
cerebro = bt.Cerebro()
cerebro.adddata(AstroData(dataname=df))
cerebro.addstrategy(MarsSignChange)
cerebro.broker.setcash(10_000)
cerebro.broker.setcommission(commission=0.001)
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
cerebro.run()
print(f"final value: {cerebro.broker.getvalue():,.2f} (start 10,000)")
print("line data hashes:", lines.attrs["result_sha256"])
5. Freqtrade: events and lines in populate_indicators
"""Minimal Freqtrade strategy with Astro Agents data as features (a data source, not a signal).
Copy into user_data/strategies/, set ASTRO_CREDITS_TOKEN in the bot's environment, and add
`astro-agents` to the bot's Python environment (pip install astro-agents).
freqtrade backtesting --strategy AstroDataStrategy --timeframe 1d --timerange 20230101-20241231
The calendar and the lines are fetched once per pair and range, then cached in memory (the same
request always returns the same bytes, so caching on the request is safe). The entry/exit rules
below only show where the columns plug in; replace them with your own.
"""
from functools import lru_cache
import pandas as pd
from freqtrade.strategy import IStrategy
from pandas import DataFrame
from astro_agents import Client
_client = Client()
@lru_cache(maxsize=32)
def _events(start: str, end: str) -> pd.DataFrame:
return _client.events(start, end, include=["station", "eclipse", "ingress"])
@lru_cache(maxsize=32)
def _lines(start: str, end: str) -> pd.DataFrame:
return _client.planetary_lines(start=start, end=end, bodies=["jupiter", "saturn"], perspective="heliocentric")
class AstroDataStrategy(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1d"
minimal_roi = {"0": 0.10}
stoploss = -0.08
startup_candle_count = 2
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
start = dataframe["date"].min().strftime("%Y-%m-%d")
end = dataframe["date"].max().strftime("%Y-%m-%d")
day = dataframe["date"].dt.floor("D")
ev = _events(start, end)
if not ev.empty:
per_day = ev.assign(day=ev["time"].dt.floor("D")).groupby("day")["key"].size()
dataframe["astro_events"] = day.map(per_day).fillna(0).astype(int)
stations = ev[ev["type"] == "station"].assign(day=lambda d: d["time"].dt.floor("D"))
dataframe["astro_station"] = day.isin(set(stations["day"])).astype(int)
else:
dataframe["astro_events"] = 0
dataframe["astro_station"] = 0
lines = _lines(start, end)
dataframe["helio_jupiter"] = day.map(lines["helio.jupiter"]).ffill()
dataframe["helio_saturn"] = day.map(lines["helio.saturn"]).ffill()
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Placeholder rule (illustration only): enter on a day with no event after a day with a station
dataframe.loc[(dataframe["astro_station"].shift(1) == 1) & (dataframe["astro_events"] == 0),
"enter_long"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[dataframe["astro_station"] == 1, "exit_long"] = 1
return dataframe
6. Quickstart notebook
quickstart.ipynb: positions, lines, events, merge and verification.
An open-source Pine Script v5 indicator: Gann planetary lines (geo + helio, any step from 30° to 360°, projected
forward), plus ingresses, stations and eclipses marked on the chart from an exact DE440 table (1990-2040).
Pine source · README and accuracy report.
Every example uses data for your own analysis. Nothing here is a trading signal, a forecast or advice.