Optionsdaten download von finance yahoo api mittels asyncio, aiohttp

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
pte1601@gmail.com
User
Beiträge: 1
Registriert: Samstag 16. Oktober 2021, 04:02

Hallo

ich arbeite an einem Optionsdownloader fuer finance.yahoo.com mittels asyncio, aiohttp.

Das Problem ist, bei wenigen Tickern funktioniert der Download korrekt; jedoch bei mehreren hundert werden die requests nicht korrekt abgearbeitet

- um Optionsdaten per Ticker herunterzuladen muss man zuerst die "expiration dates" per Ticker herunterladen
- danach kann man alle Optionsdaten per Ticker und expiration date herunterladen

Wo ist hier der Flaschenhals - Wer kann mir einen Tipp hier geben? Hier ist der Code:

def get_tasks(session, tickers):
tasks = []
for ticker in tickers:
print(ticker)
url_options1 = f"https://query2.finance.yahoo.com/v7/fin ... ns/{ticker}?"
tasks.append(asyncio.create_task(session.get(url_options1, headers={'User-Agent': ua}, ssl=True)))
return tasks


def get_tasks2(session, ticker, expiries):
tasks2 = []
for expiry in expiries:
url_options2 = f"https://query2.finance.yahoo.com/v7/fin ... te={expiry}"
tasks2.append(asyncio.create_task(session.get(url_options2, headers={'User-Agent': ua}, ssl=True)))
return tasks2

async def get_optiondata(tickers):

results1 = []

async with aiohttp.ClientSession() as session:
tasks = get_tasks(session, tickers)
responses = await asyncio.gather(*tasks)
for response in responses:
results1.append(await response.json())

return results1


async def get_optiondata2(ticker, expiries):

results2 = []

async with aiohttp.ClientSession() as session:
tasks2 = get_tasks2(session, ticker, expiries)
responses = await asyncio.gather(*tasks2)
for response in responses:
results2.append(await response.json())
return results2

datas = asyncio.run(get_optiondata(usTickers))

results = []
tickers = []
expiries = []

for i in range(len(datas)):
try:
symbol = datas['optionChain']['result'][0]['underlyingSymbol']
tickers.append(symbol)
if datas['optionChain']['result'][0]['expirationDates']:
expiries.append(datas['optionChain']['result'][0]['expirationDates'])

except IndexError as e:
print(f"{i} {e}")

for i in range(len(tickers)):

datas2 = asyncio.run(get_optiondata2(tickers, expiries), debug=True)

results = []
volumeCalls, volumePuts, volumeCallsTotal, volumePutsTotal = 0, 0, 0, 0

for j in range(len(datas2)):
if len(datas2) > 1:
try:
volumeCalls = get_option2(tickers, datas2[j]['optionChain']['result'][0]['options'][0]['calls'], 'Call')
print(volumeCallsTotal)
volumeCallsTotal += volumeCalls
except IndexError:
print(f"{tickers} Index Error")
Benutzeravatar
sparrow
User
Beiträge: 4187
Registriert: Freitag 17. April 2009, 10:28

Was heißt denn "wird nicht korrekt abgearbeitet"?
Vielleicht möchte Yahoo nicht, dass du das tust, was du tust?
August1328
User
Beiträge: 65
Registriert: Samstag 27. Februar 2021, 12:18

Ich sehe es wie sparrow, der Flaschenhals wird wahrscheinlich Yahoo sein.

Nach meiner Erfahrung gibt es keine Möglichkeit mehr, kostenlos und im großen Stil Handelsdaten abzufragen. Selbst wenn man ein Depot bei einem größeren Broker wie Interactive Brokers hat und reihenweise diese Daten abruft, gibt es ein Maximum von 100 gleichzeitigen Abfragen, mehr funktioniert nicht oder wird ausgebremst. Größere Mengen sind möglich, aber das kostet extra im Monat. Wenn Du live Daten willst, kostet das ebenfalls extra.

Ich bekomme jeden Abend die kompletten Handelsdaten bestimmter Aktien und Optionen per mail zugeschickt, auch das kostet ein paar $ im Monat. Ich brauche diese Daten als Grundlage für mein Trading Skript, weil tägliche neue Aktiensymbole hinzukommen, verschwinden oder in das bzw. aus dem Raster fallen...

Du könntest probieren eine Pause/Limit in Dein Skript einzubauen, um die Beschränkungen von Yahoo zu umgehen. Das kann dann aber bedeuten, daß Deine Strategie ggf. nicht mehr funktioniert...
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@pte1601@gmail.com: Anmerkungen zum Code:

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase).

Namen sollten nicht aus kryptischen Abkürzungen bestehen. Wenn man `USER_AGENT` meint, sollte man nicht nur `ua` schreiben.

Statt den Header für die User-Agent bei jedem Aufruf anzugeben, würde man das besser einmal bei der Erstellung des `ClientSession`-Objekts machen. Und davon auch nur *eines* erstellen und wiederverwenden.

Die Basis-URL sollte man als Konstante herausziehen, damit man sicher ist, das die überall gleich ist, und das man die auch einfach und sicher an *einer* Stelle im Code ändern kann, wenn sich daran mal etwas ändern sollte.

Das kopieren und leicht anpassen, und dabei nummerieren von Namen solltest Du Dir ganz schnell abgewöhnen. Mal davon abgesehen überlege mal ganz gründlich warum Du eine Funktion kopierst und dann *in* der kopieren Funktion Nummern an *lokale* Namen anhängst/erhöhst. Falls Du meinst das habe irgendeinen Effekt, solltest Du die entsprechenden Abschnitte in Deinem Lernmaterial über Namensräume und Sichtbarkeiten bezüglich Funktionen durcharbeiten.

`get_tasks()` und `get_tasks2()` sowie `get_optiondata()` und `get_optiondata2()` machen jeweils fast das selbe. Die Unterscheiden sich nur durch die Daten, die in die URL eingebaut werden. *Das* sollte man übrigens den `get()`-Aufruf machen lassen, und kann man den Code so auf Funktionen aufteilen, dass man nicht nahezu identische Kopien da stehen hat und synchron pflegen muss.

`results` wird im Hauptprogramm zweimal als Liste definiert und beide male nicht verwendet. Selbst wenn so eine Liste danach noch verwendet würde, sind *beide* sicher falsch. Denn entweder ist die erste Definition unbenutzt, oder der Code ist ”kaputt” weil diese Liste durch die zweite Definition überschrieben wird.

Wo wir bei ”kaputt” sind: das sind die beiden Schleifen im Hauptprogramm im Zusammenspiel garantiert. Die zweite Schleife macht den Eindruck, das jeweils ein Element aus `tickers` und eines aus `expieries` am jeweiligen Index zusammengehören. Das ist aber nicht garantiert, denn der Code in der ersten Schleife bietet gleich zwei Möglichkeiten, dass in `expieries` weniger Elemente landen als in `tickers`, womit sich nachfolgende Elemente in `expieries` natürlich gegenüber dem jeweiligen `ticker` am gleichen Index verschieben.

Damit so etwas gar nicht erst passieren kann, verwaltet man zusammendene Daten nicht in ”parallelen” Datenstrukturen die inkonsistent werden können, wenn man nicht aufpasset, sondern fasst zusammengehörende Daten erst zusammen, und steckt die dann in die gleiche Datenstruktur. Hier zum Beispiel Tupel aus Ticker und zugehörigen „expieries“ in *einer* Liste.

``for i in range(len(sequence))`` ist in Python ein „anti-pattern“ wenn dann mit `i` der Reihe nach auf Elemente von `sequence` zugegriffen wird. Man kann in Python *direkt* über die Elemente von Sequenzen iterieren, ohne den Umweg über einen Laufindex. Falls man zusätzlich eine laufende Zahl zu den Elementen braucht, gibt es `enumerate`. Bei der ersten Schleife finde ich den Index in einer Fehlermeldung aber auch deutlich weniger interessant als zum Beispiel die Daten, die das betrifft.

Wenn man tatsächlich mal nicht vermeiden konnte ”parallele” Listen zu haben, dann benutzt man auch keinen Laufindex, sondern die `zip()`-Funktion, um über die Elemente zusammen zu iterieren.

Bei der ``for j``-Schleife ist das ``if`` auf der falschen Ebene. Statt für jedes Element zu prüfen ob man etwas mit dem Element machen will gehört nur *in* die Schleife, wenn die Bedingung nicht bei jedem Durchlauf garantiert zum selben Ergebnis kommt. Dann zieht man das *vor* die Schleife, weil man dann ja *einmal* für *alle* Elemente entscheiden kann, ob man etwas mit jedem Element tut oder nicht.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import asyncio

import aiohttp

USER_AGENT = "User-Agent"
BASE_URL = "https://query2.finance.yahoo.com/v7/finance/options"
US_TICKERS = ["..."]


def get_task(session, ticker, parameters):
    return asyncio.create_task(
        session.get(f"{BASE_URL}/{ticker}", params=parameters, ssl=True)
    )


async def get_optiondata(session, ticker, arguments):
    return [
        await response.json()
        for response in await asyncio.gather(
            *(
                get_task(session, ticker, parameters)
                for parameters in arguments
            )
        )
    ]


def get_option_chain_result(data):
    return data["optionChain"]["result"][0]


async def main():
    async with aiohttp.ClientSession(
        headers={"User-Agent": USER_AGENT}
    ) as session:

        tickers_and_expieries = []
        for data in get_optiondata(
            session, ticker, ({} for ticker in US_TICKERS)
        ):
            try:
                result = get_option_chain_result(data)
                tickers_and_expieries.append(
                    (result["underlyingSymbol"], result["expirationDates"])
                )
            except IndexError as error:
                print(f"{error}: {data}")

        for ticker, expiries in tickers_and_expieries:
            datas2 = get_optiondata(
                session, ticker, ({"date": expiry} for expiry in expiries)
            )
            volume_calls = volume_calls_total = 0
            if len(datas2) > 1:
                for data in datas2:
                    try:
                        volume_calls = get_option2(
                            ticker,
                            get_option_chain_result(data)["options"][0][
                                "calls"
                            ],
                            "Call",
                        )
                        print(volume_calls_total)
                        volume_calls_total += volume_calls
                    except IndexError:
                        print(f"{ticker} Index Error")


if __name__ == "__main__":
    asyncio.run(main())
Bei `datas2` und `datas` in der zweiten Schleife braucht man unbedingt bessere Namen, denn ”Daten” ist ja irgendwie alles. Das hilft dem Leser nicht beim Verständnis.

Und `get_option2()` sieht mit der angehängten 2 auch wieder verdächtig nach Code aus der kopiert und angepasst wurde, und in der Form nicht existieren sollte.

Es würde vielleicht aus Sinn machen die Werte aus den JSON-Objekten in tatsächliche Python-Objekte umzupacken oder zu verpacken, die mehr Domänenwissen besitzen und den der die Daten verarbeitet dann etwas kürzer und verständlicher zu machen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten