gimme my money

xkcd.com

Michał "Khorne" Lowas-Rzechonek

krzysztoF "spooky" szczęsny

gimme my money

TO NIE JEST PORADA FINANSOWA

NIE JESTEM PRAWNIKIEM ANI DORADCĄ FINANSOWYM, PODATKOWYM ITP

INWESTOWANIE WIĄŻE SIĘ Z RYZYKIEM

gimme my money

TO NIE JEST też PORADA techniczna

giełda

Giełda Papierów Wartościowych

Krajowy Depozyt Papierów Wartościowych

giełda: POjęcia

KUPIĘ SPRZEDAM
45,00 41,30
42,40
41,50
42,50
43,80

arkusz

sesja

makler

transakcja i prowizja

tick: 41,30

KURS

rachunek

giełda: spekulacjA

handlować różnymi rzeczami

kupić tanio, poczekać, sprzedać drogo

jak zbyt stanieje, sprzedać po bieżącej cenie

podążać za strategią

projekt

niech komputer mi powie co kupić a co sprzedać i za ile

zlecenia złożę sobie sam

żeby miało jakieś pokrętła do zabawy

żeby jakoś wyglądało

żeby nie liczyło przez tydzień

żeby nie trzeba było się urobić przy implementacji

projekt: narzędzia

django admin (bo nie umiem w ui)

postgres (bo nie umiem w excel i nie wierzę w mongo)

websockety i knockout.js (żeby web był fancy)

zeromq (bo książkę czytałem)

projekt: Dane

problem: split/resplit

problem: wstrzymanie obrotu

ile tego właściwie jest?

dywidendy

projekt: Dane

~7.5K sesji

~250 spółek

~1.8M rekordów

AKA "nie ma sensu się przejmować"

Dane

plik csv z danymi od początku świata

import do tymczasowej tabeli

wypełnianie dziur

przekładanie do docelowych tabel

Dane: odczyt

import do tymczasowej tabeli

@contextmanager
def TemporaryTable(cursor, model, *args):
    columns = ",".join(filter(partial(str.format, '"{}"'), args))
    db_table = model._meta.db_table
    cursor.execute(f"""
        CREATE TEMPORARY TABLE "{db_table}_import"
        ON COMMIT DROP
        AS SELECT {columns}
        FROM "{db_table}"
        WHERE NULL"""
    )
    
        
with TemporaryTable(
  cursor, Quote,
  'NULL::text AS short_name', 'NULL::date AS date',
  'open', 'high', 'low', 'close', 'volume') as quote_import

Dane: odczyt

import do tymczasowej tabeli

quotes = StringIO()

for q in data:
  date = datetime.strptime(q.date, '%Y%m%d').strftime('%Y-%m-%d')
  volume = q.volume.replace('.', '')

  quotes.write("\t".join([q.short_name, date,
                          q.open, q.high, q.low, q.close,
                          volume]) + "\n")
  
quotes.seek(0)

cursor.copy_from(
  quotes, quote_import,
  columns=['short_name', 'date',
           'open', 'high', 'low', 'close',
           'volume'])

Dane: obróbka

wypełnianie dziur

cursor.execute("SELECT count(*) FROM quotes_quote_fill_gaps()")
cursor.fetchone()
create or replace function quotes_quote_fill_gaps()
returns setof integer as $$
with
    /* SQL window query */
returning id;
$$ language sql;

Dane: import

przekładanie do docelowej tabeli


cursor.execute(f"""
  INSERT INTO "{db_table}"(
    "stock_id", "session_id",
    "open", "high", "low", "close", "volume"
  )
  SELECT
    "stock"."id", "session"."id",
    "open", "high", "low", "close", "volume"
  FROM "{db_table}_import"
  LEFT JOIN "{stock_table}" AS "stock"
    ON "stock"."short_name" = "{db_table}_import"."short_name"
  LEFT JOIN "{session_table}" AS "session"
    ON "session"."date" = "{db_table}_import"."date"
  ON CONFLICT("stock_id", "session_id") DO NOTHING
""")

wizualizacja: highcharts

    let ohlc = [
            {% for quote in original.quote_set.all %}
            [
                {{ quote.session.timestamp }} * 1000,
                {{ quote.open  }},
                {{ quote.high  }},
                {{ quote.low   }},
                {{ quote.close }}
            ]{% if not forloop.last %},{% endif %}
            {% endfor %}
        ],
        volume = [
            {% for quote in original.quote_set.all %}
            [
                {{ quote.session.timestamp }} * 1000,
                {{ quote.volume  }},
            ]{% if not forloop.last %},{% endif %}
            {% endfor %}
        ],

wizualizacja: django admin

algorytm

xkcd.com

algorytm

anomalia

Trendy średnioterminowe zwykle się utrzymują

trendy długoterminowe zwykle się odwracają

algorytm: strategia

raz na czas, wybierz kilka spółek o najlepszej stopie zwrotu

kup mniej więcej po równo

nie handluj rzeczami które trudno sprzedać

jeśli cena spadnie za bardzo, sprzedaj od razu

unikaj płacenia nadmiernych prowizji

algorytm: wskaźniki

wskaźnik: stopa zwrotu

def rate_of_return(period):
  current = F('close')
  previous = F('close%i' % period)
  return Round((current - previous) / previous, 4)

qs = Quote.objects.annotate(
  # add columns with prices shifted by period
  **{'close%i' % i: lag(F('close'), i)
     for i in return_periods}
).annotate(
  # to build JSON on the db side,
  # both keys and values must be strings
  returns=JSONObject(
     keys=Array(*(Text(Value(i))
                  for i in return_periods)),
     values=Array(*(Text(rate_of_return(i))
                    for i in return_periods)),
  ),
)

algorytm: przepływ danych

sesje

maszynka

stan

rekomendacje

transakcje

bilans

status

implementacja: zeromq

Socket

protokół

Analogicznie jak w TCP, socket to "końcówka" połączenia

ZeroMQ automagicznie wznawia połączenia

Różne transporty: inproc, ipc, tcp

Różne rodzaje: REQ/REP, PUB/SUB

Wiadomości składają się z jednej lub więcej ramek

Znaczenie ramek zależy od rodzaju socketa

bardzo niskopoziomowe

implementacja: zeromq

req-rep

Serwer wystawia socket REP pod jakimś znanym adresem

Klient tworzy socket REQ i łączy się na znany adres

Tylko jeden klient na raz - pozostali czekają

req-router

Jeden serwer, wielu klientów

DEALER-REP

Jeden klient, wiele serwerów

implementacja: zeromq

pub-sub

Serwer wystawia socket PUB pod jakimś znanym adresem

Klienci tworzą sockety SUB i łączą się na znany adres

Klienci subskrybują klucze

Serwer wysyła klucz jako pierwszą ramkę wiadomości

implementacja: sesje

PUB

SERWER

cli

SUB

REP

REQ

opublikuj sesje

od 2022-11-01

do 2022-11-04

klienT

klienT

ok, 3 sesje

implementacja: klonowanie

PUB

SERWER

SUB

ROUTER

klienT

nowe bilanse

REQ

poprzednie, poproszę

stare bilanse

połącz

pokaż

implementacja: websocket

websocket

django admin

zeromq

JSMQ Dealer

websocket

<TYPE>

ws://mymoney/
<NAME>/<TYPE>

ZWS

<NAME>

ZeroMQ

wizualizacja: jsmq

function CloneClient(state, updates, callback) {
  let queued = [],
      subscriber = new JSMQ.Subscriber(),
      dealer = new JSMQ.Dealer();

  dealer.sendReady = () => dealer.send(new JSMQ.Message(state.req));

  dealer.onMessage = (items) => {
    items.forEach(callback);
    queued.forEach(callback);
    dealer.disconnect();
    subscriber.onMessage = callback; // update dynamically
  };

  // request state AFTER subscribe
  subscriber.sendReady = () => dealer.connect(state.url);
  subscriber.onMessage = queued.push;

  subscriber.connect(updates.url);
  subscriber.subscribe(updates.key);
};

wizualizacja: knockout.js

<script type="text/html" id="summary-widget">
  <div class="brief">
    <h1>
      <span title="returns" data-bind="text: ror"/>
      <small title="growth" data-bind="text: cagr"/>
    </h1>
    <span title="total" data-bind="text: total"/>
    <span title="stock" data-bind="text: stock"/>
    <span title="cash" data-bind="text: cash"/>
  </div>
</script>


<!--
 ko template:
 { name: 'summary-widget', data: summary }
-->
<!-- /ko -->

wizualizacja: knockout.js

function PortfolioSummaryViewModel() {
  this.ror = ko.observable('');
  this.cagr = ko.observable('');
  this.total = ko.observable('');
  this.stock = ko.observable('');
  this.cash = ko.observable('');
}

var summary = new PortfolioSummaryViewModel();

function onMessage(status) {
  summary.ror(status.ror);
  ...
}

wizualizacja: knockout.js

(function(ko) {
   ko.applyBindings(
     new PortfolioSummaryViewModel(),
     $('#suit-center')[0]
   );
}(ko));

DEMO TIME

gimme my money

xkcd.com

Michał "Khorne" Lowas-Rzechonek

krzysztoF "spooky" szczęsny

Gimme my money

By Michał Lowas-Rzechonek