Skip to content

Commit

Permalink
Merge pull request #27 from SamuelGuillemet/develop
Browse files Browse the repository at this point in the history
🔖 Bump version to 0.3.0
  • Loading branch information
SamuelGuillemet authored Jan 13, 2024
2 parents 1096930 + 181ca03 commit 081d3a8
Show file tree
Hide file tree
Showing 123 changed files with 13,392 additions and 675 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
fetch-depth: 0

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

Expand All @@ -36,7 +36,7 @@ jobs:
cache: "gradle"

- name: Install poetry
uses: abatilo/actions-poetry@v2
uses: abatilo/actions-poetry@v3

# Cache node_modules
- uses: actions/setup-node@v4
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
Expand All @@ -69,7 +69,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3

# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
Expand All @@ -82,6 +82,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
2 changes: 1 addition & 1 deletion .github/workflows/release-on-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
run: |
mkdir -p ./version
echo "${{ needs.release-on-push.outputs.version }}" > ./version/version_number
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: version_number
path: ./version
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
![GitHub Release](https://img.shields.io/github/v/release/SamuelGuillemet/pfe-asr-broker?label=Version)
[![CodeQL](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/codeql.yml/badge.svg)](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/codeql.yml)
[![CI](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/ci.yml/badge.svg)](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/ci.yml)
[![CI - Java](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/ci-java.yml/badge.svg)](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/ci-java.yml)
[![CI - Python](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/ci-python.yml/badge.svg)](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/ci-python.yml)


# PFE Asr Broker

Expand Down
7 changes: 7 additions & 0 deletions clients/quickfix-client/.flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[flake8]
max-line-length = 120

extend-exclude =
.git
.venv
data
7 changes: 7 additions & 0 deletions clients/quickfix-client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.venv/
# .vscode/
data/*
!data/.gitkeep
**/__pycache__/
storage/**
latency*.txt
9 changes: 9 additions & 0 deletions clients/quickfix-client/.pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[MASTER]
init-hook='import sys; sys.path.append(".")'

[MESSAGES CONTROL]
disable=missing-module-docstring, missing-function-docstring, missing-class-docstring, too-few-public-methods, too-many-arguments, too-many-instance-attributes, too-many-locals, logging-fstring-interpolation, redefined-builtin, arguments-renamed

[FORMAT]
good-names=i,j,k,ex,Run,_,pk,x,y,e,f
max-line-length = 120
19 changes: 19 additions & 0 deletions clients/quickfix-client/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"configurations": [
{
"name": "Python: File",
"type": "python",
"request": "launch",
"program": "${file}",
"justMyCode": true
},
{
"name": "Main",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/broker_quickfix_client/main.py",
"console": "integratedTerminal",
"justMyCode": true
}
]
}
36 changes: 36 additions & 0 deletions clients/quickfix-client/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true,
"**/__pycache__": true,
"**/.pytest_cache": true,
"**/.mypy_cache": true,
"**/.mypy*": true,
"**/.venv": true,
"**/storage/": true,
},
"python.analysis.autoImportCompletions": true,
"python.analysis.typeCheckingMode": "basic",
"python.analysis.inlayHints.callArgumentNames": "partial",
"python.analysis.inlayHints.variableTypes": true,
"python.analysis.inlayHints.functionReturnTypes": true,
"editor.formatOnSave": true,
"isort.check": true,
"isort.args": ["--profile", "black"],
"[python]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.fixAll": "explicit"
},
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.codeLens": true
},
"search.exclude": {
"**/dist": true,
"**/.venv": true
}
}
116 changes: 116 additions & 0 deletions clients/quickfix-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Quickfix client

This repository comprises a Python-based client application designed to interact with a broker's system using the QuickFIX library. The architecture is structured as follows:

## Global Architecture Overview

### Entry Point
- **main.py**: This file serves as the entry point of the application. It initializes and starts the main functionality.

### Functionality Modules
- **application.py**: Contains the core logic of the quickfix application.
- **constant.py**: Stores constants used throughout the application.
- **decorators.py**: Defines decorators used within the application.

### Handlers
- **handlers/**: This directory holds modules responsible for handling specific message types. `execution_report.py` focuses on handling execution reports from the broker's system.

### Utility Modules
- **utils/**: Houses utility modules utilized across the application:
- `loader.py`: Handles loading operations.
- `logger.py`: Provides logging functionality.
- `quickfix.py`: Offers utility functions for interfacing with the QuickFIX library.

### Wrappers
- **wrappers/**: Encapsulates and extends functionality from the QuickFIX library:
- `enums.py`: Houses enumerations and constants related to QuickFIX.
- `execution_report.py`: Wrapper dedicated to store execution reports as python objects.
- `new_order_single.py`: Wrapper facilitating the handling of new single orders, for example its creation and storage as a python object.

The architecture follows a modular approach, segregating functionalities into discrete modules and directories. It aims to streamline interactions with the QuickFIX library and broker system by providing wrappers and utilities while maintaining clear separation of concerns.


## Specifications

In order to interact flawlessly with the quickfix application, you can define callback functions when instantiating the application. These callbacks are defined as follows:

```python
execution_handler = ExecutionReportHandler(
on_filled_report=lambda report: logger.info(f"Filled: {report}"),
on_rejected_report=lambda report: logger.info(f"Rejected: {report}"),
)
application, initiator = setup()
application.set_execution_report_handler(execution_handler)
```

Callbacks function should take as input a python object representing the execution report. For example we have:

```python
@dataclass
class FilledExecutionReport:
order_id: int
client_order_id: int
symbol: str
side: SideEnum
type: OrderTypeEnum
leaves_quantity: int
price: float
cum_quantity: int


@dataclass
class RejectedExecutionReport:
order_id: int
client_order_id: int
symbol: str
side: SideEnum
type: OrderTypeEnum
leaves_quantity: int
reject_reason: OrderRejectReasonEnum
```

In this example, we define two callbacks, one for filled reports and one for rejected reports. These callbacks are called whenever the application receives a filled or rejected report from the broker's system.

To send new orders, you can use the following function:

```python
order = NewOrderSingle.new_market_order(0, SideEnum.SELL, 1, "ACGL")
application.send(order)
```

If you want to store the order as a python object, you can use the following function:

```python
python_order = order.get_order()
```

This function returns a `Order` dataclass object, which contains the order as a python object. This object is defined as follows:

```python
@dataclass
class Order:
order_id: int | None
client_order_id: int
symbol: str
side: SideEnum
price: float | None
quantity: int
```

---

## Installation

To install the client, you need to run the following commands:

```bash
$ poetry install
```

## Running the client

To run the client, you need to run the following command:

```bash
(.venv) $ python broker_quickfix_client/main.py
```
Empty file.
130 changes: 130 additions & 0 deletions clients/quickfix-client/broker_quickfix_client/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# pylint: disable=unused-argument,invalid-name,super-init-not-called

import logging
from time import sleep

from quickfix import (
Application,
FileStoreFactory,
Message,
MsgType,
MsgType_ExecutionReport,
MsgType_Logon,
MsgType_MarketDataSnapshotFullRefresh,
MsgType_OrderCancelReject,
Password,
Session,
SessionID,
SocketInitiator,
Username,
)

from broker_quickfix_client.handlers.execution_report import ExecutionReportHandler
from broker_quickfix_client.handlers.order_cancel_reject import OrderCancelRejectHandler
from broker_quickfix_client.utils.logger import setup_logs
from broker_quickfix_client.utils.quickfix import log_quick_fix_message, set_settings

logger = logging.getLogger("client.application")


class ClientApplication(Application):
session_id: SessionID | None = None

execution_report_handler = ExecutionReportHandler()
order_cancel_reject_handler = OrderCancelRejectHandler()

username: str | None = None
password: str | None = None

def set_execution_report_handler(
self, execution_report_handler: ExecutionReportHandler
):
self.execution_report_handler = execution_report_handler

def set_order_cancel_reject_handler(
self, order_cancel_reject_handler: OrderCancelRejectHandler
):
self.order_cancel_reject_handler = order_cancel_reject_handler

def onCreate(self, sessionId: SessionID):
pass

def onLogon(self, sessionId: SessionID):
self.session_id = sessionId

def onLogout(self, sessionId: SessionID):
pass

def toAdmin(self, message: Message, sessionId: SessionID):
log_quick_fix_message(message, "Sending")
if message.getHeader().getField(MsgType()).getString() == MsgType_Logon:
message.setField(Username(self.username))
message.setField(Password(self.password))

def toApp(self, message: Message, sessionId: SessionID):
log_quick_fix_message(message, "Sending", logging.INFO)

def fromAdmin(self, message: Message, sessionId: SessionID):
log_quick_fix_message(message, "Received")

def fromApp(self, message: Message, sessionId: SessionID):
log_quick_fix_message(message, "Received", logging.INFO)

msg_type = message.getHeader().getField(MsgType()).getString()

if msg_type == MsgType_ExecutionReport:
self.execution_report_handler.handle_execution_report(message)
elif msg_type == MsgType_OrderCancelReject:
self.order_cancel_reject_handler.handle_order_cancel_reject(message)
elif msg_type == MsgType_MarketDataSnapshotFullRefresh:
logger.info("Market data snapshot full refresh received")
else:
logger.warning(f"Unknown message type: {msg_type}")

def send(self, message: Message):
return Session.sendToTarget(message, self.session_id)

def get_session_id(self):
return self.session_id

def set_credentials(self, username, password):
self.username = username
self.password = password


def build_initiator(username: str, application: ClientApplication) -> SocketInitiator:
settings = set_settings(username)
store_factory = FileStoreFactory(settings)
initiator = SocketInitiator(application, store_factory, settings)
return initiator


def start_initiator(initiator: SocketInitiator, application: ClientApplication):
if not application.username or not application.password:
raise ValueError("Username and password must be set before starting initiator")

initiator.start()
# Wait for the session to logon before sending messages.
while not application.get_session_id():
sleep(0.1)


def setup(
username: str,
password: str,
execution_report_handler: ExecutionReportHandler | None = None,
order_cancel_reject_handler: OrderCancelRejectHandler | None = None,
):
setup_logs("client")
setup_logs("quickfix")
application = ClientApplication()

application.set_credentials(username, password)
if execution_report_handler:
application.set_execution_report_handler(execution_report_handler)
if order_cancel_reject_handler:
application.set_order_cancel_reject_handler(order_cancel_reject_handler)

initiator = build_initiator(username, application)
start_initiator(initiator, application)
return application, initiator
Loading

0 comments on commit 081d3a8

Please sign in to comment.