Share Agent is an end-to-end stock analysis and automated trading platform for BIST Katılım Endeksi (XKTUM) — the Sharia-compliance index of Borsa İstanbul. Three independent trading agents (short, medium, long horizon) each capitalized at 100,000 TRY, 12 weighted technical indicators, fundamental analysis with XKTUM compliance checks, and live order execution through the Algolab (Deniz Yatırım) API. The repository is at github.com/gultekinahmetabdullah/Share-Agent. Python 3.13 / FastAPI backend, Kotlin Multiplatform + Compose mobile app (Android shipping, iOS stub ready), PostgreSQL 16 with TimescaleDB for price history, Redis cache, all of it deployed on a Hetzner CX43 server behind Traefik with auto-SSL.
This piece is about hexagonal architecture not as a diagram on a slide but as a property that earned its keep when the primary data provider went down for two days.
1. The rule that made everything else possible
The single architectural rule we enforced from commit one:
The domain layer has zero external imports.
Not “few” external imports. Zero. Not Pandas, not SQLAlchemy, not Redis,
not requests, not even pydantic. The domain/ package imports from
Python’s standard library and from itself. That is it. We have a CI lint
rule that fails the build on any external import in domain/*.
Why this matters: when the primary BIST data provider (İş Yatırım)
returned a malformed response for two days in March, the failure
surface was exactly one adapter file. The 12 technical indicators —
RSI, MACD, SMA/EMA, Bollinger Bands, Stochastic, VWAP, OBV, ADX, CCI,
MFI, ROC, Volume — kept calculating correctly because they don’t know
where their data comes from. The signal scorer kept producing -100 to
+100 scores. The portfolio agents kept executing the buy/sell rules. We
flipped a boolean and the FallbackPriceProvider chained Yahoo Finance
in front. Total downtime visible to users: 0.
Hexagonal architecture pays back exactly when reality changes.
2. Ports — the interfaces that let providers be replaceable
The domain declares three primary ports as Python ABCs:
PriceProvider— get OHLCV history, get current price.FinancialProvider— get fundamental ratios, get balance-sheet items.ShareRepository— persist shares, persist current prices, query the watchlist.
Everything in the domain that needs market data takes a PriceProvider
in its constructor. The domain never imports IsYatirimAdapter or
YahooFinanceAdapter; it only depends on the abstract base class.
The FallbackPriceProvider is the part that is genuinely satisfying.
It implements PriceProvider itself, takes a list of PriceProvider
instances in priority order, and tries them in sequence with a circuit
breaker on each. So IsYatirim is primary, YahooFinance is fallback,
and the domain code looks like this:
class SignalService:
def __init__(self, price_provider: PriceProvider, ...):
self.prices = price_provider
async def calculate_signal(self, symbol: str) -> Signal:
history = await self.prices.get_history(symbol, days=200)
...
This is dependency injection at its most boring and most useful. We
swap the wiring in main.py; nothing in domain/ changes.
3. 12 indicators, fundamental analysis, and the -100 to +100 score
The signal scorer takes the 12 technical indicator values plus fundamental ratios and a Sharia-compliance check, applies a weight vector, and returns a single score. Below -50 is strong sell; above +50 is strong buy.
The XKTUM compliance check is the part that makes this project specific to Turkey:
- Debt ratio < 33%. A Sharia-compliant company cannot be more than one-third leveraged.
- Interest-bearing income < 5%. The company cannot derive significant income from interest.
- Buffer rule. A company near the limits is flagged for manual review.
The fundamental ratios (ROE, ROA, debt/equity, net margin, current
ratio) come from KAP (Kamuyu Aydınlatma Platformu, Turkey’s regulatory
disclosure platform) via the KapAdapter — another swappable port. If
KAP changes their schema (and they have, twice in the project’s
lifetime), one adapter file changes.
The lesson: scoring systems should output numbers; classification is the caller’s job. We do not return “BUY” or “SELL” strings from the domain. We return a score and a reason. The mobile app applies its own threshold and color-codes accordingly. Different downstream consumers (the alert service, the portfolio agent, the mobile UI) want different thresholds — and the domain stays out of that fight.
4. Outbound adapters — every external integration is a file
The adapters that implement the ports:
IsYatirimAdapter— primary price data, end-of-day bars + intraday.YahooFinanceAdapter— fallback price data via the.ISsuffix (THYAO → THYAO.IS).KapAdapter— fundamental data from KAP.AlgolabAdapter— order execution through Deniz Yatırım’s Algolab API. Demo mode by default; flips to live with an env var.PostgresShareRepository— TimescaleDB for OHLCV history (price history is a hypertable; queries by time range are O(log n) on a multi-year corpus), regular Postgres for everything else.RedisCacheAdapter— symbol-list cache, expensive computation cache.
Each adapter is one file plus its tests. None of them know about the
others. The FallbackPriceProvider glues two together; the rest of the
system never sees the seam.
The thing that surprised us: intraday position prices come from Yahoo
Finance, not İş Yatırım. İş Yatırım’s API returns end-of-day bars
during market hours — i.e., yesterday’s close, until today’s close
publishes. For real-time PnL on open positions you want the live tape,
which Yahoo provides through their .IS ticker for free. So the
production wiring is: İş Yatırım for historical bars (high quality,
authoritative), Yahoo for intraday refresh (refreshed every 5 minutes
during market hours by the scheduler). Two providers, one port — no
domain code knows the difference.
5. Production deploy — Hetzner, Docker Compose, Traefik
The whole platform runs on a Hetzner CX43 (8 vCPU, 16GB RAM):
- Docker Compose with
docker-compose.prod.ymland a gitignored.env.prod. - Traefik as the reverse proxy with Let’s Encrypt auto-SSL. Routing
by
Host(\share-agent.rollingcatsoftware.com`)` labels in the compose file. - Own TimescaleDB + Redis containers, separate from the shared database on the same host. We considered consolidating; we deliberately did not. A buggy deploy of Share Agent should never be able to take down the messenger or the Quranic engine that share the host.
- CI/CD via GitHub Actions with four separate workflows:
backend.yml(Python lint via Ruff + pytest),mobile.yml(Gradle build for Android),docker.yml(build image, push to GHCR, SSH deploy to Hetzner),release.yml(release automation).
The scheduler is APScheduler running inside the FastAPI process. Eight distinct cron-like jobs handle morning signal refresh (10:05), morning trade window (10:15), afternoon refresh (14:15), afternoon trade window (14:30), pre-close snapshot (17:30), evening price ingestion (18:30), evening batch signals (18:45), plus continuous intraday price refresh every 5 minutes during market hours and stop-loss monitoring every 10 minutes. All times Europe/Istanbul, all weekdays only — Borsa İstanbul does not trade on weekends, and neither does the scheduler.
Agent state is persisted to PostgreSQL (agent_portfolios,
agent_positions, agent_trades). The agents survive container
restarts, deploys, and host reboots. The first version held state in
memory, lost it on the first deploy, and that was the last time we
held trading state in memory.
What we’d do differently
- Make the port hierarchy thinner. We have
PriceProviderdoing both history and current price. They are almost separate concerns (the history provider is heavy, batched; the current-price provider is light, latency-sensitive). Splitting would have made the Yahoo-for-intraday-only routing more obvious. - Use TimescaleDB continuous aggregates. We compute moving averages on read; we should pre-compute daily MA20 / MA50 / MA200 in continuous aggregates and read them in O(1).
- Move scheduler to a separate process. APScheduler in the FastAPI process means a deploy interrupts running jobs. A standalone scheduler service with its own queue would be cleaner.
Reading list
- Alistair Cockburn’s original Hexagonal Architecture article — it’s short, and the diagram in the article is the diagram you should draw on day one.
- Implementing Domain-Driven Design by Vaughn Vernon, ch. 4 (Architecture). The hexagonal section is the one most directly applicable.
- TimescaleDB hypertables — read this before designing your price-history schema.
Repository: github.com/gultekinahmetabdullah/Share-Agent. Live deploy: share-agent.rollingcatsoftware.com.
- architecture
- hexagonal
- trading
- fastapi