Blog de développement

Lightstreamer et temps réel

Dernière Modification le :
2000-01-01

...

Mise en évidence de Ligthstreamer sur Société Générale

Le site d'un emetteur de turbo (Société Générale) publie le cours du CAC40, en temps réel. Il y a bien transit de cette information à fréquence élévée, car je la vois dans mon navigateur. C'est magique!

Dans l'inspecteur de réseau, sous firefox, je n'arrive pas a identifier le flux. Je passe à Edge.

image
On voit dans l'inspecteur de Edge, filtré par WS (WebSockets), le ligthstreamer, public, diffusé par Société Générale (01-01-2025)

Avec Edge, naviguez vers la page contenant le flux du CAC40 Le CAC40 →: h t t p s://sgbourse.fr/underlying-detail?underlyingId=579. Accéder à l'inspecteur (F-12). Les outils de développement s’ouvrent: allez dans l’onglet Réseau. Rechargez la page pour capturer toutes les requêtes. Vous verrez une liste de toutes les requêtes effectuées par la page. Appliquer le filtre WS pour isoler lightstreamer. Cliquer sur la ligne pour la griser et voir l'échange de messages.

société générale ligthstreamer basic messages
Echange de messages avec le ligthstreamer de Société Générale: session, souscription, premiers messages

wss://warrantspushserver.societegenerale.com/lightstreamer
est appelé par https://sgbourse.fr/scripts-XIVD7GUK.js
https://sgbourse.fr/main-WMNF32XU.js
https://labs.ig.com/streaming-api-guide.html


La documentation est ici :
https://sdk.lightstreamer.com/ls-python-client/2.2.0/api/index.html

Lightstreamer a publié une nouvelle bibliothèque sdk (https://github.com/Lightstreamer/Lightstreamer-lib-client-haxe) pour se connecter aux serveurs Lightstreamer version 7.4.0 et plus.

https://www.lightstreamer.com/sdks/ls-generic-client/2.2.0/TLCP%20Specifications.pdf

Premiers pas: Lightstreamer, versions et documentations

En 2025, commencer avec Lightstreamer peut être très différent de ce que cela a pu être par le passé. Historiquement, LS est conçu pour le Web, donc Java, Javascript. Une bibliothèque officielle Python est apparue récemment, ce qui rendrait obsolète les bibliothèques non-officielles, du moins pour les serveurs LS postérieurs à la version .

La version la plus récente, au 01/01/2025, est : 2.2.0
Les exemples (quand on en trouve...) , eux, lui sont antérieurs et ne fonctionnent donc pas. Un exemple en Python est donné sur GitHub Lire →.
La librairie la plus récente semble être: lightstreamer.client.ls_python_client_haxe.
J'ai eu du mal à l'installer, sans doute à cause de changement de noms au travers des versions.
Le Client SDK Lightstreamer sont écrits en Haxe, un langage de programmation libre qui permet la compilation d'une base de code unique vers des cibles multiples. Le projet sur GitHub Lire →.
Pour Python Lire → ; La documentation Lire →.
J'ai aussi trouvé utile d'avoir la spécification générique sous PDF LightStreamer_TLCP Specifications_060125 (dans le ZIP de ressources)

Ma première connection Python - Lightstreamer

Un équivalent à 'Hello World" serait de réaliser sa première connexion au serveur de démo, fourni par Ligthstreamer soi-même. C'est mon script: Lightstreamer\stock_list_demo.py


from lightstreamer.client.ls_python_client_haxe import *
from subscription_listener import *
import time

def wait_for_input():
    input("{0:-^80}\n".format("HIT CR  (= RETURN) TO UNSUBSCRIBE AND DISCONNECT FROM LIGHTSTREAMER"))

loggerProvider = LSConsoleLoggerProvider(LSConsoleLogLevel.WARN)
LSLightstreamerClient.setLoggerProvider(loggerProvider)
lightstreamer_client = LSLightstreamerClient("http://push.lightstreamer.com", "DEMO")
lightstreamer_client.connect()

time.sleep(1)
print(f"Status : {lightstreamer_client.getStatus()}")

# Making a new Subscription in MERGE mode
subscription = LSSubscription(mode="MERGE",items=["item1", "item2", "item3", "item4","item5", "item6", "item7", "item8","item9", "item10", "item11", "item12"],
    fields=["stock_name", "last_price", "time", "bid", "ask"])
subscription.setDataAdapter("QUOTE_ADAPTER")

# Adding the subscription listener to get notifications about new updates
subscription.addListener(SubListener())

# Registering the Subscription
lightstreamer_client.subscribe(subscription)

wait_for_input()

# Unsubscribing from Lightstreamer by using the subscription as key
# lightstreamer_client.unsubscribe(subscription)
# Disconnecting
# lightstreamer_client.disconnect()

Ce tableau montre ce qui est utile et nécessaire au client Python, version 2.2.0 (Janv. 2025). Pour les versions antérieures sera un peu différent!

ParamètreDescription
LS_modeMode de souscription (MERGE, DISTINCT)
LS_itemsItems souscrits
LS_fieldsChamps de données
LS_adapterNom de l'adaptateur de données
image
Lighstreamer demo avec Python, affichage sur la console CMD

Lighstreamer démo, IG, SocGen: tous différents

IG fournit une doc complète pour le client Python qui convient à SA Version du Streamer. SG, c'est encore autre chose! Donc pour SG, on revient à la base: WebSocket et cela marche très bien!

Serveur Demo SG BNP IG
Serveur très récent ancien très ancien récent
Protocole LS LS/WebSocket WebSocket LS/WebSocket
Session Auto. LS_session ? ?
Souscription LS_mode, LS_items,
LS_fields, LS_adapter
LS_mode, LS_schema,
LS_id, LS_data_adapter
?

LightStreamer, dans sa version moderne a une étape create_session avec create_session.txt. On ne la voit pas, mais on s'en doute, car l'échange entre Lighstreamer et le navigateur commence par un envoi contenant LS_session. Il vient bien de quelque part! et il est antérieur à l'échange...
Quand on inspecte la page d'acceuil on voit un create_session.js. On examine ce que POSTe ce script.

le LS_cid est un identifiant nécessaire. "pcYgxn8m8 feOojyA1T681f3g2.pz479mDv" est généralement utilisé
LS_user et LS_password: inutiles ici car ressource publique.
LS_polling est TRUE : Demande une connexion par groupée (pooled).
Sans trop chercher à comprendre (voir TLCP Specification de 27/5/2020) je reprends à l'identique : LS_op2: create, LS_phase: 201, LS_cause: new.api, LS_polling: true, LS_polling_millis: 0, LS_idle_millis: 0, LS_cid: pcYgxn8m8 feOojyA1T681f3g2.pz479mDv, LS_adapter_set: ProxyIcomAdapter, LS_container: lsc

Le LS_CID a probablement été généré lors de sa programmation Jouer avec → initiale et est utilisé, inchangé, par SG. On le considère 'stable' car il est déjà cité dans un post datant de 2022 Lire → et aussi ici, dans un travail de Marc-Alexander Richts Lire →. C'est un peu piégeux car on pourrait croire qu'il faille récupérer ou générer ce CID: il n'en est rien...

Les difficultés sont consignées dans un mémoire universitaire PDF (dont je mets une traduction en français dans le ZIP de ressources). Je n'ai pas rencontré les mêmes points durs: le registre des 'valeurs' est constitué à partir d'une base fournie par SG au format Excel (trivial avec QSV vs celle fournie par L&S en PDF!). M-A. Richts a éessayé de comprendre les Javascripts de création/souscription. Je me suis contenté de les imiter. Des restrictions sur les bits d'entête n'ont pas été rencontrées chez SG (heureusement, sinon, j'aurais eu du mal) Au total, M-A. Richts montre que c'est faisable mais fastidieux. C'est exact, mais une fois qu'on l'a on a accès au temps réel!

ParamètreDescription
LS_op2Type d'opération : create
LS_phasePhase de la connexion : 201
LS_causeCause de la création de session : new.api
LS_pollingMode de connexion : true
LS_polling_millisIntervalle de polling : 0
LS_idle_millisTemps d'inactivité : 0
LS_cidClient ID spécifique (doit être envoyé)
LS_adapter_setNom de l'adaptateur utilisé : ProxyIcomAdapter
LS_containerConteneur côté client : lsc

Pour la souscription, des serveurs différents utiliseront des noms différents pour l'ensemble d'adaptateurs, les éléments, les champs et l'adaptateur de données, qui peuvent être choisis librement.

Le Connecteur WebSocket

On ouvre la session, ensuite, le connecteur WebSocket est simple à manipuler. Je commence par une requête générale pour récupérer les headers et cookies, et ne pas être embété.

import websocket
# Connexion WebSocket avec les bons en-têtes
ws = websocket.WebSocketApp(url,header=custom_headers(headers),on_open=on_open,on_message=on_message)
ws.run_forever()

Dans "on_open" on définit les paramètres de la connection, sa validation, puis de la souscription.
Dans on_message, on valide le premier message, on souscrit, on filtre un peu les messges recus: en effets, le serveur envoie des messages qui ne nous concernent pas. Il envoie aussi parfois des cotations erronées, faciles à filtrer. Cela peut être du à une gestion de 'phase' en Javascript: point que j'ai négligé (je ne fais pas de gestion de phase) et surmonté avec le filtre.

Ouvrir la Session

On envoie à "create_session.js" la même charge que celle récupérée sur l'inspecteur de Edge. Je ne cherche que à recupérer le "session_id". Le reste ne me préoccupe pas, pas même la phase, n'ayant pas vu de difference entre la gérer et ne pas la gérer.

# Étape 1 : Créer une session via POST
url_http = "https://xxx/lightstreamer/create_session.js"
headers_http = {
    "Content-Type": "application/x-www-form-urlencoded", "Origin": "https://xxx.fr",
    "Referer": "https://xxx.fr/", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
    "Accept": "*/*", "Cache-Control": "no-cache", "Pragma": "no-cache" }
data = {
    "LS_op2": "create",
    "LS_phase": "201",
    "LS_cause": "new.api",
    "LS_polling": "true",
    "LS_polling_millis": "0",
    "LS_idle_millis": "0",
    "LS_cid": "pcYgxn8m8 feOojyA1T681f3g2.pz479mDv",
    "LS_adapter_set": "ProxyIcomAdapter",
    "LS_container": "lsc"
}
response = requests.post(url_http, headers=headers_http, data=data)
# Extraire le SessionId de la réponse

if "start('" in response.text:
    session_id = response.text.split("start('")[1].split("'")[0]
else:
    print("Erreur : la réponse ne contient pas 'start('.")
    exit ()

print(f"SessionId : {session_id}")

De façon étonnante, et contrairement à la documention Lightstreamer, une fois la session crée, le serveur de la banque ne renvoie pas un message tel que CONOK (connexion Okay), mais plus simplement un javascript (que l'on va simplement ignorer) puis un message "bw(0.0)" (pour Notification de bande passante ?). La séquence javascript puis "bw(0.0)" tient lieu de CONOK. Pour bien faire, il faudrait gérer un booléen étagé, pour être le plus orthodoxe possible. Pour un premier jet, on s'en passe.

Faire la souscription

A la première (et seulement la première) réception du bw(), on envoie le message de souscription qui est juste un copie/collé, simplifié de ce que l'on voit dans l'inspecteur


subscription_msg = (
    "control\r\n"
    "LS_mode=MERGE&"
    "LS_id=FRF_CAC.CBUL&"
    "LS_schema=BIDTIME%20BID&"
    "LS_data_adapter=DEFAULT&"
    "LS_snapshot=true&"
    "LS_table=1&"
    "LS_req_phase=2&"
    "LS_win_phase=1&"
    "LS_op=add&\r\n"
)

On note que les parametres sont ici LS_mode, LS_id, LS_schema et non pas LS_mode, LS_items et LS_fields, comme dans la documentation de la version 'moderne' du LigthStreamer. LS_data_adapter est à l'identique et important.

https://www.produitsdebourse.bnpparibas.fr/underlying-detail/?underlying=CAC40&u=255 https://www.produitsdebourse.bnpparibas.fr/js/main.min.js?v=10.0.1-3 wss://websockets.produitsdebourse.bnpparibas.fr/QuotesForUnderlyings