Sıfırdan Bir Yatırım Platformu: Next.js, FastAPI ve Gemini ile “Akıllı Portföy”
Ücretsiz sunucularda, gerçek kullanıcıya açık bir finans platformu yazmak neye benziyor? Mimari kararlar, üç ayrı veritabanı ve yolda…

Sıfırdan Bir Yatırım Platformu: Next.js, FastAPI ve Gemini ile “Akıllı Portföy”
Ücretsiz sunucularda, gerçek kullanıcıya açık bir finans platformu yazmak neye benziyor? Mimari kararlar, üç ayrı veritabanı ve yolda patlayan her şey.
Neden böyle bir şey yaptım?
Yatırım yapan çoğu insanın portföyü bir Excel dosyasında ya da hiçbir yerde duruyor. Altın kaç lira, dolar ne oldu, BIST’te ne var — hepsi ayrı ayrı uygulamalarda. Üstelik “elimdeki portföy şu an ne durumda, ne yapmalıyım?” sorusuna cevap veren, Türkiye piyasasını bilen bir araç neredeyse yok.
Akıllı Portföy’ü bunun için yazdım: kripto, döviz, altın/gümüş ve BIST hisselerini tek portföyde toplayan, canlı fiyatlarla değerini hesaplayan, fiyat alarmı kuran ve yapay zekâ destekli yorum üreten bir web platformu.
Canlı: https://akilli-yatirim-platformu.vercel.app
Kaynak kod: https://github.com/omebalta559-cloud/akilli-yatirim-platformu-
Genel mimari
Proje üç parçadan oluşuyor:
frontend/ → Next.js 16 + React 19 + TypeScript + Tailwind (Vercel)
backend/ → FastAPI + SQLAlchemy + Alembic (Render, Docker)
keepalive/ → Ana backend'i uyanık tutan minik yardımcı servis (Render)
Backend tarafında modüler bir yapı kurdum — her iş alanı kendi klasöründe, kendi router / service / models / schemas dörtlüsüyle:
backend/app/modules/
├── auth/ # JWT + Google ile giriş, şifre sıfırlama
├── market_data/ # CoinGecko + Yahoo Finance fiyatları
├── portfolio/ # Varlıklar, performans, PDF/Excel rapor
├── ai_advisor/ # Gemini + RAG tabanlı danışman
├── alerts/ # Fiyat alarmları ve e-posta bildirimi
└── outreach/ # Toplu e-posta kampanyası
Bu ayrım çok erken bir karardı ve karşılığını fazlasıyla verdi: yeni bir özellik eklemek, çoğu zaman yeni bir klasör açıp main.py'a tek satır router eklemek demek oldu.
Üç farklı veritabanı, üç farklı iş
Projede tek bir veritabanı yok — çünkü üç farklı tipte veri var.
1. PostgreSQL — kalıcı ilişkisel veri
Kullanıcılar, portföydeki varlıklar, günlük portföy anlık görüntüleri (snapshot), fiyat alarmları. SQLAlchemy 2.0 + Alembic ile yönetiliyor.
2. Redis — önbellek
Piyasa verisi dışarıdan geliyor ve dış servisler ya yavaş ya da rate limit yiyor. Her fiyat isteği için CoinGecko’ya gitmek hem saçma hem imkânsız. Cache katmanında küçük ama hayat kurtaran bir detay var: bayat (stale) yedek.
STALE_TTL_SECONDS = 6 * 60 * 60
async def get_or_set(key, ttl_seconds, fetch):
cached = await _redis_client.get(key)
if cached is not None:
return json.loads(cached)
stale_key = f"{key}:stale"
try:
data = await fetch()
except Exception:
stale = await _redis_client.get(stale_key)
if stale is not None:
logger.warning("Kaynak başarısız oldu, bayat önbellek döndürülüyor: %s", key)
return json.loads(stale)
raise
payload = json.dumps(data)
await _redis_client.set(key, payload, ex=ttl_seconds)
await _redis_client.set(stale_key, payload, ex=STALE_TTL_SECONDS)
return data
Taze veri 30 saniye yaşıyor; ama CoinGecko çökerse kullanıcı hata ekranı yerine 6 saate kadar eski ama makul bir fiyat görüyor. Finansal bir uygulamada “biraz eski veri”, “hiç veri yok”tan çok daha iyi.
3. ChromaDB — vektör veritabanı
Yapay zekâ danışmanının haber hafızası burada. Ayrı bir sunucu olarak değil, gömülü (embedded) modda diske yazarak çalışıyor — ücretsiz planda ayakta tutulacak bir servis daha istemedim.
Yapay zekâ danışmanı: prompt değil, bağlam meselesi
En çok emek verdiğim kısım burası. “Gemini’ye soru sor” kısmı 10 satır; asıl iş modele ne göstereceğine karar vermek.
Danışman cevap üretmeden önce üç kaynak birleşiyor.
1) Kullanıcının gerçek portföyü + son 3 ayın grafiği
history = await market_data_service.get_price_history(ticker, "3mo", "1d")
points = history.get("points", [])
if len(points) >= 2:
change_pct = (points[-1]["price"] - points[0]["price"]) / points[0]["price"] * 100
return (
f"- {h.asset_symbol} ({h.asset_type}): {h.quantity} adet, "
f"alış fiyatı {h.purchase_price}, "
f"3 aylık değişim %{change_pct:.1f}"
)
2) Güncel ekonomi haberleri (RSS → ChromaDB → RAG)
Uygulama açılışında beş kaynaktan haber çekiliyor, vektörleştirilip ChromaDB’ye yazılıyor; soru sorulduğunda soruyla ilgili haberler bağlama ekleniyor:
NEWS_RSS_FEEDS = [
"https://www.bloomberght.com/rss",
"https://www.aa.com.tr/tr/rss/default?cat=ekonomi",
"https://www.dunya.com/rss",
"https://www.ntv.com.tr/ekonomi.rss",
"https://www.hurriyet.com.tr/rss/ekonomi",
]
3) Enflasyon gerçeği
TÜFE yıllık oranı bağlama giriyor — çünkü %20 nominal getiri, %32 enflasyonda aslında zarar. Danışmanın bunu bilmeden konuşması anlamsızdı.
Bir de kritik bir tasarım kararı: haber akışı çökerse uygulama çökmüyor, sadece daha az bilgiyle cevap veriyor.
try:
await asyncio.to_thread(ai_advisor_service.refresh_news_from_rss)
except Exception:
logger.exception("Haber akışı yenilenemedi, danışman genel bilgiyle devam edecek.")
Piyasa verisi: ücretsiz kaynaklardan gerçekçi fiyat üretmek
Türkiye’de “gram altın fiyatı” veren güvenilir ve ücretsiz bir API bulmak sandığımdan zordu. Sonunda şunu yaptım: Yahoo Finance’ten ons altın futures (GC=F) fiyatını ve USD/TRY kurunu alıp gram altını kendim hesaplıyorum.
GRAMS_PER_TROY_OUNCE = 31.1034768
# Madeni altınların milyem (saflık) ağırlıklı saf altın karşılığı
COIN_PURE_GOLD_GRAMS = {
"Çeyrek Altın": 1.75 * 0.916,
"Yarım Altın": 3.5 * 0.916,
"Tam Altın": 7.0 * 0.916,
"Cumhuriyet Altını": 7.216 * 0.916,
}
Çeyrek altın 1.75 gram ama 22 ayar; yani saf altın karşılığı 1.75 × 0.916. Bu tür "yazılım değil, alan bilgisi" detayları projenin en keyifli kısmıydı.
Kripto için CoinGecko, BIST hisseleri için Yahoo Finance (AKBNK.IS formatı) kullanılıyor.
Yolda patlayan her şey
Yazının en faydalı kısmı muhtemelen burası. Ücretsiz altyapıda çalışmak, dokümantasyonda yazmayan çok şey öğretiyor.
1. Render ücretsiz planı 15 dakikada uyuyor
Siteye giren kullanıcı 50 saniye “cold start” bekliyordu. Denediklerim:
- GitHub Actions cron — çalıştı ama uzun süre aktivite olmayınca workflow devre dışı bırakılıyor.
- Frontend’den ping — sadece sekme açıkken çalışıyor, yani kimse yokken yine uyuyor.
- Çözüm: Render’da ikinci bir minik servis (
keepalive/) açtım. İki servis birbirinin adresini biliyor ve 6–10 dakikada bir (rastgele jitter'lı) karşılıklı ping atıyor. İkisi de birbirini uyanık tutuyor.
2. Render giden SMTP portlarını (25/465/587) kapatıyor
Şifre sıfırlama e-postası smtplib ile gönderilmiyordu: Network is unreachable. Saatlerce SMTP ayarıyla uğraştıktan sonra sorunun kendi kodum olmadığını anladım. Çözüm: SMTP'yi tamamen bırakıp Brevo'nun HTTPS API'sine geçmek — 443 portu açık olduğu için sorunsuz çalışıyor.
BREVO_API_URL = "https://api.brevo.com/v3/smtp/email"
response = httpx.post(BREVO_API_URL, headers=headers, json=payload, timeout=20)
3. TCMB EVDS API’si
Enflasyon verisini TCMB’nin resmi API’sinden canlı çekmeyi denedim. evds2 → evds3 geçişi sırasında istekler yönlendirilip engelleniyordu. Sonuç: TÜFE oranını konfigürasyonda tutup elle güncelliyorum. Bazen doğru mühendislik kararı, çalışmayan entegrasyonu terk etmek oluyor.
4. ChromaDB’nin embedding modeli her deploy’da yeniden iniyordu
~79MB’lık model her build’de tekrar indiriliyor, ilk AI isteği zaman aşımına düşüp 502 veriyordu. Docker’da kalıcı bir volume ile çözüldü:
volumes:
- chroma_onnx_cache:/root/.cache/chroma
5. Paylaşımlı IP’de rate limit
Render’ın IP’si paylaşımlı olduğu için CoinGecko anonim istekleri sınıra takılıyordu. Ücretsiz Demo API anahtarı + Redis cache + stale fallback üçlüsü bu sorunu tamamen bitirdi.
Diğer özellikler
- Portföy performansı: Her gün otomatik snapshot alınıyor, zaman içindeki değişim Recharts ile grafiğe dökülüyor.
- Fiyat alarmları: “BTC 100.000 doların üstüne çıkarsa haber ver” — arka plan görevi fiyatları kontrol edip e-posta atıyor. Aynı varlığa birden fazla alarm varsa fiyat tek sefer çekiliyor (
price_cache). - Rapor çıktısı: Portföyü PDF (ReportLab) ve Excel (openpyxl) olarak indirme.
- Kimlik doğrulama: JWT + Google ile giriş, e-posta ile şifre sıfırlama.
- Blog + SEO: İçerik sayfaları, yapılandırılmış veri (structured data) ve SSS bölümü.
Ne öğrendim?
Ücretsiz altyapı bedava değil, mühendislikle ödüyorsun. Uyku modu, kapalı portlar, paylaşımlı IP’ler, rate limit’ler… Her biri ayrı bir tasarım kararı doğurdu. İşin ilginci, bu kısıtlar kodu daha dayanıklı hale getirdi: stale cache, hata durumunda çökmeyen arka plan görevleri, timeout toleransı — hepsi kısıtlar yüzünden var.
Yapay zekâ özelliğinde iş, modelde değil bağlamda. Aynı Gemini modeli, portföyü ve güncel haberleri gördüğünde bambaşka bir asistana dönüşüyor.
Dış servisler her zaman çöker. Sorulacak soru “çökerse ne olacak?” değil, “çöktüğünde kullanıcı ne görecek?”.
Linkler
Canlı platform: https://akilli-yatirim-platformu.vercel.app
Kaynak kod: https://github.com/omebalta559-cloud/akilli-yatirim-platformu-
Sorularınız veya önerileriniz varsa yorumlarda yazın — özellikle Türkiye piyasası için ücretsiz ve güvenilir veri kaynağı önerilerine açığım.
Yazı işinize yaradıysa alkışlamayı unutmayın.
메타데이터
- post_id
- bb657f5f5bdf
- slug
- sıfırdan-bir-yatırım-platformu-next-js-fastapi-ve-gemini-ile-akıllı-portföy-bb657f5f5bdf
- url
- https://medium.com/@omebalta559/s%C4%B1f%C4%B1rdan-bir-yat%C4%B1r%C4%B1m-platformu-next-js-fastapi-ve-gemini-ile-ak%C4%B1ll%C4%B1-portf%C3%B6y-bb657f5f5bdf
- canonical_url
- https://medium.com/@omebalta559/s%C4%B1f%C4%B1rdan-bir-yat%C4%B1r%C4%B1m-platformu-next-js-fastapi-ve-gemini-ile-ak%C4%B1ll%C4%B1-portf%C3%B6y-bb657f5f5bdf
- author_url
- https://medium.com/@omebalta559
- status
- ok
- fetched_at
- 2026-08-23 03:47:58