Tinkerings

Building a Financial Time Series Microservice with FastAPI

Date Published

## Situation


I needed a way to serve historical stock data — price series for a given ticker over a chosen lookback period — to anything that might want to consume it: a notebook, a dashboard, a backtest harness, or a future trading system. Rather than bury the fetching logic inside a script, I wanted it behind a real HTTP interface, so that the data layer could evolve without the consumers caring how it was sourced or cached.


I also wanted the option to pull from more than one data provider. Stooq is free and simple; Nasdaq Data Link (formerly Quandl) has richer datasets but requires an API key and rate limits. The system had to accommodate both without duplicating the surrounding plumbing.


## Task


Build a FastAPI microservice that:


- Exposes a REST endpoint for retrieving a stock's time series

- Abstracts the data provider behind a stable interface

- Keeps fetching, transformation, and presentation as separate concerns

- Is deployable both locally and to AWS Lambda

- Can be extended to more providers without rewriting the routers


## Action


**1. Defined an abstract interface for the data source.**

`StocksApiInterface` declares two `@abstractmethod` methods — `get__timeseries_of_stock` and `get__timeseries` — and nothing else. Every provider must honour that contract. The rest of the application depends on the interface, not on any particular implementation.


**2. Implemented two concrete providers against the same interface.**

`StooqApiManager` uses `pandas_datareader.DataReader`. `NasdaqApiManager` uses `nasdaqdatalink.get`. They do different things under the hood but expose identical public methods. The router never needs to know which one it is talking to.


**3. Selected the provider via a factory function.**

`get__stock_api` lives in the service package's `__init__.py` and returns the right implementation based on the request. The router receives it through `Depends(get__stock_api)` — the standard FastAPI dependency injection pattern — rather than constructing it inline.


**4. Injected the dependency rather than hardcoding it.**

```python

async def timeseries(

stock_name: str,

source: str,

period_back: int,

stocks_api: StocksApiInterface = Depends(get__stock_api)

):

```

The route declares what it needs; FastAPI supplies it. Swapping the provider is a one-line change in the factory, not a rewrite of the endpoint.


**5. Layered the application into routers, services, and implementations.**

`routers/` handles HTTP. `services/` holds the business logic — CSV caching, plotting, transformation. `services/stocks_api_service/implementation/` holds the provider-specific code. Each layer only knows about the layer beneath it.


**6. Used `APIRouter` with prefixes to keep the surface organised.**

`base_router`, `crud_router`, and `transform_router` are mounted under `/v1`. The version is explicit in the URL, which means `/v2` can be added later without disturbing existing clients.


**7. Cached fetched series to CSV.**

`csv_service.save__timeseries_csv` writes the DataFrame to disk; `read__timeseries_csv` loads it back with `index_col='Date'` and `parse_dates=True`. The provider is hit once per request, but the data is available for offline analysis, replotting, and debugging without re-querying.


**8. Separated transformation from fetching.**

`transform_service` holds the shaping logic — monthly averages, close-only series, truncated windows. The fetching layer returns a full DataFrame; the transform layer decides what subset to expose. This means a single fetch can serve several different views.


**9. Named the routers by responsibility, not by URL.**

`base_router`, `crud_router`, `transform_router`. The names describe what each router does, not what path it happens to be mounted on. A reader sees immediately where a new endpoint belongs.


**10. Returned records-oriented JSON from the endpoint.**

```python

response = timeseries.to_json(orient="records")

parsedResponse = json.loads(response)

return parsedResponse

```

`orient="records"` produces a list of objects, one per row, with column names as keys. That is the shape most frontends and notebooks want, and it round-trips cleanly through `json.loads`.


**11. Prepared for serverless with `Mangum`.**

```python

handler = Mangum(app, lifespan="off")

```

Mangum adapts an ASGI application to the AWS Lambda handler signature. Adding one line to the app means the same code runs locally under Uvicorn and on Lambda behind API Gateway — no rewriting, no separate deployment path.


**12. Managed secrets through environment variables.**

```python

load_dotenv()

API_KEY = os.getenv('API_KEY')

nasdaqdatalink.ApiConfig.api_key = API_KEY

```

The API key never appears in the code, and the same file runs in development and production without modification — the environment supplies the difference.


**13. Configured logging at the application level.**

`logging.basicConfig(level=logging.INFO)` in `app.py`, with `logging.info` calls at every provider boundary. Connection successes, failures, and dataset types are all recorded. When a provider changes behaviour or starts failing, there is already a trail.


**14. Wrote the providers as `async` throughout.**

Both `get__timeseries_of_stock` and `get__timeseries` are `async def`. The interface declares async methods, and both implementations honour that. Swapping to a genuinely asynchronous HTTP client later will not require changing the calling convention anywhere else.


**15. Provided a `run.py` entry point alongside the Uvicorn invocation.**

```python

from timeseries_processor.src.app import app

app.run(host='0.0.0.0', port=8000, debug=True)

```

For local development, `python run.py` is the one command needed. For production, `uvicorn timeseries_processor.src.app:app --host 0.0.0.0 --port 80` is documented in the README. Two clearly separated paths for two clearly separated contexts.


## Result


A small FastAPI microservice that serves stock time series over HTTP, backed by two interchangeable data providers behind a stable interface. The architecture is layered, the dependency injection is done properly, the endpoint returns records-oriented JSON, and the same code runs locally under Uvicorn or on AWS Lambda under Mangum.


The point of the exercise was to demonstrate that a data-fetching script can be turned into a proper service — with a defined interface, an injection point, a caching layer, and a transformation layer — without over-engineering any of it. The structure anticipates more providers, more transforms, and a future UI, and it does so with the tools FastAPI already provides rather than with anything custom. The next additions are straightforward: more providers implementing the same interface, more transforms behind the existing router, and a frontend consuming the JSON that `/v1/time_series/...` already returns.