Mercurial > lbo > hg > stex
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 +