changeset 0:3508454a9a1a

Initial commit with basic server
author Lewin Bormann <lbo@spheniscida.de>
date Thu, 28 Feb 2019 14:31:53 +0100
parents
children f867f7c3c2db
files server/server.py testclient.py
diffstat 2 files changed, 190 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/server.py	Thu Feb 28 14:31:53 2019 +0100
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+"""The server generates stock data and distributes it to clients."""
+
+import arguments
+import json
+import random
+import sys
+import time
+ 
+from PyQt5.QtWidgets import QApplication, QMainWindow, QMenu, QVBoxLayout, QSizePolicy, QMessageBox, QWidget, QPushButton
+from PyQt5.QtGui import QIcon
+
+import zmq
+
+_random = random.Random()
+_random.seed(1)
+# Maximum initial stock value in cents.
+_maxinitvalue = 10000
+_maxhistory = 100
+
+class Stock:
+    symbol = ''
+    # Stock value in cents
+    _current_value = 0
+    _last_values = []
+    
+    # Random walk coefficients
+    _stddev = 0
+
+    def name():
+        """Generates a stock-ticker-like name."""
+        return ''.join([chr(int(_random.random()*26)+0x41) for i in range(0, 4)])
+
+    def __init__(self, name):
+        self.symbol = name
+        self._stddev = _random.random() / 10
+        self._current_value = _random.random() * _maxinitvalue
+
+    def next_price(self):
+        """Calculates a (random) next price based on the current price and history."""
+        dev = 0.02*self._current_value or 1
+        new_value = int(_random.normalvariate(self._current_value * 1.001, dev))
+        new_value = abs(new_value)
+        self._last_values.append(self._current_value)
+        self._current_value = new_value
+        if len(self._last_values) > _maxhistory:
+            self._last_values = self._last_values[1:]
+        return new_value
+
+    def current_value(self):
+        return self._current_value
+
+class StockData:
+    _data = {}
+
+    def __init__(self, data):
+        self._data = data
+        self._data['_stockdata'] = True
+
+    def data(self):
+        return self._data
+
+    def serialize(self):
+        return json.dumps(self._data)
+
+    def write(self, dst):
+        return json.dump(self._data, dst)
+
+    def deserialize_from(jsondata):
+        """Parse StockData from JSON data. Raises an exception if JSON is invalid or the object is malformed."""
+        data = json.loads(jsondata)
+        if data is not dict or '_stockdata' not in data:
+            raise ValueError('JSON object is not a valid StockData serialization')
+        _data = data
+
+class Stocks:
+    _stocks = []
+
+    def __init__(self, stocks=None):
+        """Takes [Stock]."""
+        self._stocks = stocks
+
+    def generate(self):
+        next = {}
+        for s in self._stocks:
+            next[s.symbol] = s.next_price()
+        return StockData(next)
+
+
+class Server(arguments.BaseArguments):
+    _doc = """
+    Usage:
+        stex-server [options]
+
+    Options:
+        -a --address=<address>  Listen on address.
+        -p --port=<port>        Listen on port.
+        --stocks=<stocks>       Number of stocks to generate.
+        --stocklist=<stocks>    List of ticker symbols to generate stocks for.
+        --interval=<interval>   Interval in ms to publish stock data (default 500)
+        --help                  Print help.
+    """
+
+    _stocks = Stocks(None)
+
+    def __init__(self, zctx, callback=None):
+        """callback is called with a StockData object every time new data are available."""
+        super(arguments.BaseArguments, self).__init__(doc=self._doc)
+        if self.help or None:
+            print(self._doc)
+            sys.exit(0)
+
+        socket = zctx.socket(zmq.PUB)
+        socket.setsockopt(zmq.IPV6, 1)
+        socket.bind('tcp://{}:{}'.format(self.address or '[::]', self.port or '9988'))
+        self._socket = socket
+        self.init_stocks()
+
+    def init_stocks(self):
+        stocklist = []
+        if self.stocklist:
+            stocklist = self.stocklist.split(',')
+        elif self.stocks and int(self.stocks) > 0:
+            stocklist = [Stock.name() for _ in range(0, self.stocks)]
+        else:
+            stocklist = [Stock.name() for _ in range(0, 10)]
+
+        stocklist = [Stock(name=s) for s in stocklist]
+        self._stocks = Stocks(stocklist)
+
+    def run(self):
+        interval = int(self.interval or 500)
+        while True:
+            time.sleep(interval / 1000.)
+            nextdata = self._stocks.generate()
+            print("DEBUG: {}".format(nextdata))
+            self._socket.send_string(nextdata.serialize())
+
+def main():
+    ctx = zmq.Context()
+    s = Server(ctx)
+    s.run()
+
+if __name__ == "__main__":
+    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/testclient.py	Thu Feb 28 14:31:53 2019 +0100
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+
+import zmq
+
+from matplotlib import figure
+from matplotlib.backends import backend_cairo
+
+ctx = zmq.Context()
+sock = ctx.socket(zmq.SUB)
+sock.setsockopt(zmq.IPV6, 1)
+sock.connect('tcp://[::1]:9988')
+sock.setsockopt_string(zmq.SUBSCRIBE, '')
+
+history = {}
+i = 0
+
+
+def draw_symbols():
+    for sym, hist in history.items():
+        fig = figure.Figure()
+        ax = fig.add_subplot(111)
+        ax.grid(True)
+        ax.set_xlim(0, len(hist))
+        ax.set_ybound(0, 100)
+        ax.plot([i for i in range(0, len(hist))], hist)
+        backend_cairo.FigureCanvas(fig).print_png('{}.png'.format(sym))
+
+while True:
+    i += 1
+    msg = sock.recv_json()
+    msg.pop('_stockdata')
+    for sym, val in sorted(msg.items()):
+        print(' {}: {:.2f}'.format(sym, val / 100.))
+        if sym in history:
+            history[sym].append(val/100.)
+            if len(history[sym]) > 500:
+                history[sym] = history[sym][1:]
+        else:
+            history[sym] = [val/100.]
+    print('')
+
+    if i > 25:
+        draw_symbols()
+        i = 0
+